chore(ci): promote internal staging to main - #32709
Merged
Merged
Conversation
…am budgets An org admin updating a team budget from the Hub UI was rejected with 401, because the route gate only recognizes an org admin when the request body carries organization_id while the UI sends team_id. For /team/update, resolve the target team's organization_id from team_id before the gate runs, so an org admin of the team's own org clears the org-scoped branch without the client passing organization_id. Team admins and cross-org admins stay denied at the gate, and callers that already pass organization_id are unaffected, so the existing /team/update authorization matrix is unchanged
The in-workflow regex list was hard to maintain and, because it runs on pull_request, could be modified by the same PR it inspects. Path gating for the OSS daily branches now lives in repository branch protection settings, so this workflow keeps only the OSS-safe checks: the hardcoded-secret test and ruff
…a no-auth warning Adds the two client-forwarded token modes to the MCP server create and edit form auth dropdowns, and shows a warning when true_passthrough is selected: the gateway performs no admission auth for that server, so callers reach the upstream without a LiteLLM key and per-key/per-team rate limits and spend tracking do not apply. The warning is a shared component so the two forms cannot drift on the copy.
…en modes
true_passthrough and oauth_delegate persist no upstream credentials, so
the create/edit forms had no way to preview tools or configure the tool
allowlist: tools/list went upstream unauthenticated and came back 401.
This reuses the existing OAuth authorize machinery in browser-only mode
for those two auth types: the admin authorizes against the upstream
(DCR/PKCE, with optional client credentials for IdPs without dynamic
registration), the token lands in sessionStorage exactly like the
legacy PKCE-passthrough path, and the tools preview forwards it via the
per-server x-mcp-{alias}-authorization header, which the passthrough
resolver arm already accepts. Nothing is written to the server row or
the per-user credential store; the create payload keeps excluding
credentials for these auth types via AUTH_TYPES_REQUIRING_CREDENTIALS.
The tools preview endpoint now also extracts the Authorization header
for the two new auth types so the browser-held token reaches the
passthrough arm during create-time previews.
…client-forwarded token modes
The server detail page's Tool Testing Playground gated its browser-held
token handling on the legacy PKCE-passthrough shape, so a
true_passthrough or oauth_delegate server listed tools unauthenticated
and surfaced 'Failed to fetch MCP tools' with no way to authorize. The
playground now treats both modes as browser-held-token servers: it
reads the sessionStorage token established by the create/edit
browser-only Authorize, forwards it via the x-mcp-{alias}-authorization
header, evicts it on a 401, and shows its own Authorize gate when the
token is absent.
That gate's flow uses the gateway's relayed authorize/register/token
endpoints with the real server id, which previously 400ed for anything
but oauth2. Those endpoints now also accept the client-forwarded token
modes (the minted token is upstream-audienced and browser-held; DCR
persistence stays off on this path), and registry builds run the same
RFC 9728/8414 endpoint discovery for these modes that oauth2 rows get,
since their rows never store an authorization_url.
…ient-forwarded servers The DB build already discovers authorization/token endpoints for true_passthrough and oauth_delegate rows; the config.yaml load path kept the oauth2-only gate, so a YAML-defined server in either mode could not use the relayed authorize flow unless the YAML declared authorization_url. Both paths now share the same auth type set.
…ting browser-authorize tokens
Two correctness fixes for the client-forwarded token modes.
The preemptive-401 connect gate for true_passthrough and oauth_delegate
only inspected the request-wide Authorization, so a caller who bound the
upstream token via the per-server x-mcp-{alias}-authorization header (the
mandatory shape in a multi-server aggregate, where the request-wide
Authorization is withheld) was spuriously 401'd at connect even though
egress already honors that header. The gate now recognizes the per-server
header for both modes via a shared helper, mode-correctly: true_passthrough
treats any Authorization or the per-server header as the upstream token,
oauth_delegate keeps requiring a distinct x-litellm-api-key so a lone
Authorization consumed for admission is never mistaken for an upstream
token. The preemptive raise is also gated to single-server scopes so a
multi-server aggregate degrades gracefully (the listing absorbs a
per-server failure) instead of one missing token 401-ing the whole connect.
The browser-only Authorize flow was writing the upstream access and refresh
token to LiteLLM_MCPUserCredentials, contradicting the modes' persist-nothing
contract: the temp OAuth-relay server was cached with a hardcoded oauth2
auth_type, so needs_user_oauth_token was true and the token exchange stored
it. The create and edit forms now send the real auth_type for these modes,
so the temp server is not oauth2, needs_user_oauth_token is false, and the
exchange skips storage while still returning the token to the browser
session.
Adds mcp_auth_mode and mcp_server_resource to StandardLoggingMCPToolCall so a relayed passthrough/delegate request can be attributed in an audit to its mode and its upstream target without logging any credential. Both are non-sensitive metadata derived from the resolved server; the admission and upstream tokens stay SecretStr and are never logged.
…orization bypass Two review findings on the passthrough modes. The tool-call log records the upstream MCP server URL as mcp_server_resource, which is persisted in spend-log metadata and sent to logging callbacks. A URL carrying embedded userinfo or a secret query parameter would leak into logs, so the value is now redacted to its bare resource identifier (scheme + host + path); userinfo, query string, and fragment are stripped before it is logged. The listing fan-out withholds the request-wide Authorization from a true_passthrough / oauth_delegate server when another server in scope also consumes it, so one bearer is not replayed across upstreams. The later server.extra_headers copy loop did not honor that decision: a server listing Authorization in extra_headers would re-copy the withheld bearer from raw_headers. The withhold decision is now computed once and applied to both the forwarding branch and the extra_headers loop.
The edit form's getTemporaryPayload read the server's stored auth_type instead of the value the admin selected in the dropdown, so an admin who switched an existing oauth2 server to true_passthrough (or oauth_delegate) and ran the browser authorize flow built the temporary OAuth-relay server as oauth2. That made needs_user_oauth_token true and persisted the token to the DB, contrary to the mode's browser-held contract, and left it inconsistent with onTokenReceived and the submit payload, both of which already read the form value. It now reads values.auth_type, matching the create form.
…through modes The 'auth_type is true_passthrough or oauth_delegate' check was duplicated inline across both server forms' browser-authorize temp payloads, the edit form's onTokenReceived and tool-preview gate, PassthroughAuthorizeSection, and mcp_tools' usesBrowserHeldToken. Extracted a single isClientForwardedTokenMode helper in types.tsx and routed every site through it so the set of client-forwarded modes lives in one place and cannot drift. Also replaced a pre-existing nested ternary in the authorize button label surfaced by touching the file.
…th types The config-YAML loader and the DB loader each defined their own local tuple (oauth2, true_passthrough, oauth_delegate) to decide which auth types trigger upstream OAuth endpoint discovery, under two different names. Hoisted them to a single module constant _UPSTREAM_OAUTH_DISCOVERY_AUTH_TYPES so the two load paths cannot drift on which modes get discovery.
…r the pass-through modes The create form wrote the upstream token obtained by Authorize & Fetch into form.credentials for every mode, so for true_passthrough / oauth_delegate the browser-held token leaked into the OAuth flow's getCredentials (preview requests) and the redirect-persist cache, and was a step away from server-level credential persistence. onTokenReceived now early-returns for the client-forwarded modes, holding the token only in local state for preview (mirroring the edit form), instead of writing it into form.credentials.
The create/edit forms passed several large object literals inline as arguments (persist-state JSON.stringify, storeMCPOAuthUserCredential, setToken, transport-clear setFieldsValue), tripping local/no-large-inline-object-arg. Assigned each to a named variable at the call site - a pure, behavior-preserving refactor verified by the create/edit suites - which lowers the whole-tree count so the eslint baseline is 512 rather than being raised to accommodate them.
…ed refs (#32401) * fix(ui): forward refs through ui primitives and fail tests on swallowed refs Under React 18 a ref passed to a plain function component is dropped with only a dev console warning, so Base UI render-prop triggers composed over our shadcn-style primitives silently stop working (the tooltip just never opens; ui/badge.tsx hit exactly this on the shared DataTable branch). Label, Separator, Skeleton, UiLoadingSpinner and the Table family now use React.forwardRef like Button and Input already did, a contract test pins ref delivery for each, and setupTests turns React's ref warning into a test failure so the next primitive that swallows a ref fails CI instead of shipping a dead tooltip * fix(ui): include captured ref warnings in the tripwire error The afterEach tripwire threw a fixed message and discarded the collected React warnings, so a failure never said which component swallowed the ref. Append the captured warnings (component name + stack) to the thrown error.
…32576) The App Router migration is complete: every page is a path route and the legacy `?page=` switch is gone from the index. This closes it out. The `/ui/` index (page.tsx) kept its own duplicate copy of teams state, a teams fetch, and keys/addKey plumbing solely to feed a second `UserDashboard` render for the `invitation_id` case. That was redundant: `ApiKeysDashboard` already renders `UserDashboard` sourcing its own data, so the index is thinned to just render `<ApiKeysDashboard />`. The login redirect, the legacy `?page=` deep-link redirect for old bookmarks, and the post-login return-URL handling stay on the index. The invitation entry point now resolves in one place. Modern invitation links already point at the dedicated `/onboarding` route; the dashboard layout now redirects legacy `/ui/?invitation_id=` links there too (via `migratedHref`, the same base-aware redirect the index uses for `?page=`), instead of re-rendering that route's page component inline. This removes an import of one route's `page.tsx` into another module, and lets the now-unreachable `if (invitation_id) return <Onboarding/>` branch in the shared `user_dashboard.tsx` be deleted along with its dead `Onboarding` import and `searchParams` read. A layout test asserts the redirect and fails if it regresses. `legacyPageHref` and the sidebar's migrated-vs-legacy href fallback are left in place; they are still live for the parent-category nav nodes (agentic, tools, experimental, settings) that are not page routes. eslint-metrics.json is resynced: -2 no-explicit-any from the removed `any` casts, plus pre-existing drift the gate requires the snapshot to match.
…ModifyResponseException (#32289) * fix(guardrails/bedrock): honor disable_exception_on_block by raising ModifyResponseException The Bedrock-specific GuardrailInterventionNormalStringError predates the unified guardrails refactor and no proxy code path handles it, so a block with the flag set surfaced as an uncaught Exception -> HTTP 500 in pre_call mode and was silently discarded in during_call mode (model call proceeded in the parallel asyncio.gather; the block hook's data["mock_response"] mutation happened after route_request had already unpacked kwargs). Convert the block to ModifyResponseException at the raise site inside make_bedrock_api_request. That exception is the industry-standard proxy contract already caught in proxy_server, anthropic_endpoints, response_api _endpoints, and pass_through_endpoints; it turns into a 200 response with finish_reason=content_filter and the block message as content, which is exactly what the flag was documented to yield. Post-call blocks attach the LLM response to original_response so the synthetic reply reports the upstream call's real token usage instead of zero. Deletes the now-orphaned GuardrailInterventionNormalStringError class and the dead create_guardrail_blocked_response / mock_response plumbing in the Bedrock hooks; updates the existing tests that had locked in the buggy contract. Resolves LIT-4186 * chore(guardrails/bedrock): drop dead str branch in _update_messages_with_updated_bedrock_guardrail_response Follow-up to the disable_exception_on_block fix. That method used to receive either a BedrockGuardrailResponse or a plain string (the block message, when the flag was set). Now that a block always raises ModifyResponseException before this method runs, the string branch is unreachable; tighten the type to BedrockGuardrailResponse and delete the guard. * fix(guardrails/bedrock): streaming post_call block yields synthetic stream instead of surfacing as SSE 500 Regression from the LIT-4186 refactor: pre-refactor, the streaming post_call iterator caught GuardrailInterventionNormalStringError locally and replaced the assembled response with a synthetic content-filter message, then re-emitted it as chunks via MockResponseIterator. After the refactor the exception was re-raised as ModifyResponseException, which async_streaming_data_generator serializes as a proxy 500 error frame because the SSE response headers are already flushed by the time the block fires. Non-streaming paths still let ModifyResponseException propagate to the endpoint handler (which converts it into a 200). Streaming can't do that, so keep the local synthesis: on the exception, rebind the assembled response to a ModelResponse whose single choice carries the block message as content and finish_reason=content_filter, and let the downstream MockResponseIterator emit it as chunks. Same shape a non-streaming block produces. Adds a mapped-file regression test that mutation-kills the raise behavior and locks in the synthetic-stream contract. * fix(guardrails/bedrock): preserve upstream usage on streaming post_call block Non-streaming post_call blocks report the upstream LLM call's real token usage via ModifyResponseException.original_response, which the endpoint handler unwraps through _blocked_response_usage. Streaming post_call synthesizes its own ModelResponse locally (the exception can't escape the SSE generator), and previously left .usage unset, so the client saw accurate billing on non-streaming blocks and zero on streaming blocks -- silent revenue leak. Copy the assembled response's .usage onto the synthetic block response before yielding. Pre-refactor code had the same gap (create_guardrail_blocked_response never set usage); this is a net improvement, not a regression fix.
…ctive auth type The edit form decided auth mode from the saved mcpServer.auth_type in fetchTools while the authorize flow used the current form value, so a token authorized after switching the form to a client-forwarded mode was never forwarded as the x-mcp header until the server was saved. A shared getEffectiveAuthType (form value falling back to the saved record) is now the single decision point for token receipt and tool loading The save path classified the staged token with getMcpOAuthMode, which returns null for true_passthrough and oauth_delegate, so the staged token was dropped on save instead of being committed to sessionStorage the way the create form's submit path does. The passthrough branch now also covers the client-forwarded modes; the token still never enters the server row
#32670) create_model now waits until the new deployment is servable on the data plane (polls /v1/models) before returning, instead of assuming /model/new makes it instantly callable. On a split control/data-plane proxy the gateway only sees a model after its next DB reload, so an immediate call raced the reload and 400'd with "Invalid model name passed" (embeddings, responses, messages, ocr, ...). It also stops pinning model_info.id to the model_name, letting the proxy assign a unique model_id. Re-registering a fixed-name deployment (the batch suite's openai-batch et al.) after a failed teardown no longer collides on the model_id unique constraint (prisma UniqueViolationError surfaced as the generic 500 "Failed to add model to db", erroring every batch_lifecycle case at setup)
… in ModifyResponseException streaming path (#32665) * fix(guardrails/bedrock): honor disable_exception_on_block by raising ModifyResponseException The Bedrock-specific GuardrailInterventionNormalStringError predates the unified guardrails refactor and no proxy code path handles it, so a block with the flag set surfaced as an uncaught Exception -> HTTP 500 in pre_call mode and was silently discarded in during_call mode (model call proceeded in the parallel asyncio.gather; the block hook's data["mock_response"] mutation happened after route_request had already unpacked kwargs). Convert the block to ModifyResponseException at the raise site inside make_bedrock_api_request. That exception is the industry-standard proxy contract already caught in proxy_server, anthropic_endpoints, response_api _endpoints, and pass_through_endpoints; it turns into a 200 response with finish_reason=content_filter and the block message as content, which is exactly what the flag was documented to yield. Post-call blocks attach the LLM response to original_response so the synthetic reply reports the upstream call's real token usage instead of zero. Deletes the now-orphaned GuardrailInterventionNormalStringError class and the dead create_guardrail_blocked_response / mock_response plumbing in the Bedrock hooks; updates the existing tests that had locked in the buggy contract. Resolves LIT-4186 * chore(guardrails/bedrock): drop dead str branch in _update_messages_with_updated_bedrock_guardrail_response Follow-up to the disable_exception_on_block fix. That method used to receive either a BedrockGuardrailResponse or a plain string (the block message, when the flag was set). Now that a block always raises ModifyResponseException before this method runs, the string branch is unreachable; tighten the type to BedrockGuardrailResponse and delete the guard. * fix(guardrails/bedrock): streaming post_call block yields synthetic stream instead of surfacing as SSE 500 Regression from the LIT-4186 refactor: pre-refactor, the streaming post_call iterator caught GuardrailInterventionNormalStringError locally and replaced the assembled response with a synthetic content-filter message, then re-emitted it as chunks via MockResponseIterator. After the refactor the exception was re-raised as ModifyResponseException, which async_streaming_data_generator serializes as a proxy 500 error frame because the SSE response headers are already flushed by the time the block fires. Non-streaming paths still let ModifyResponseException propagate to the endpoint handler (which converts it into a 200). Streaming can't do that, so keep the local synthesis: on the exception, rebind the assembled response to a ModelResponse whose single choice carries the block message as content and finish_reason=content_filter, and let the downstream MockResponseIterator emit it as chunks. Same shape a non-streaming block produces. Adds a mapped-file regression test that mutation-kills the raise behavior and locks in the synthetic-stream contract. * fix(guardrails/bedrock): preserve upstream usage on streaming post_call block Non-streaming post_call blocks report the upstream LLM call's real token usage via ModifyResponseException.original_response, which the endpoint handler unwraps through _blocked_response_usage. Streaming post_call synthesizes its own ModelResponse locally (the exception can't escape the SSE generator), and previously left .usage unset, so the client saw accurate billing on non-streaming blocks and zero on streaming blocks -- silent revenue leak. Copy the assembled response's .usage onto the synthetic block response before yielding. Pre-refactor code had the same gap (create_guardrail_blocked_response never set usage); this is a net improvement, not a regression fix. * fix(proxy): capture logging_obj before post_call_failure_hook pops it in ModifyResponseException streaming path post_call_failure_hook removes litellm_logging_obj from request_data before iterating callbacks (it's not serialisable). The streaming branch of the ModifyResponseException handler read it from _data after that call, so it always received None and CustomStreamWrapper.__init__ crashed with AttributeError: NoneType has no attribute model_call_details. Capture it before the hook runs so the streaming path gets a valid object. Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com> * test(proxy): add regression for streaming ModifyResponseException logging_obj capture Covers the bug where logging_obj was read from request_data after post_call_failure_hook had already popped it, causing CustomStreamWrapper to crash with AttributeError. Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com> * test(proxy): drive real chat_completion in ModifyResponseException streaming logging_obj regression The original test inlined the fix pattern (capture before pop) in its own body rather than calling the actual chat_completion handler in proxy_server.py, so a revert of the fix left the test passing. Confirmed via mutation check: reverting the two-line source fix and re-running left the test green. Rewrite the test to drive chat_completion directly: - patch _read_request_body so chat_completion sees the seeded dict - patch ProxyBaseLLMRequestProcessing.base_process_llm_request to raise ModifyResponseException with the same request_data - patch proxy_logging_obj so post_call_failure_hook mutates the dict the way production does (pops litellm_logging_obj) - intercept CustomStreamWrapper.__init__ and assert logging_obj is the non-None object seeded in request_data Mutation-verified: reverting the source fix now surfaces the exact production crash inside CustomStreamWrapper's __init__ (AttributeError: NoneType has no attribute model_call_details) rather than a silently-passing test. Addresses Greptile P1 on PR #32665. --------- Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
…okenMode helper The helper extraction missed this call site, leaving an inline duplicate of the two-mode check that could drift from the shared definition
ci: add OSS daily branch workflow
* fix: rust ocr tests finally pass * fix: move realtime dir * fix(realtime): normalize azure realtime api_base to host for Foundry endpoints The azure realtime handler appended the realtime path to api_base verbatim, so a Foundry base carrying a project path (.../api/projects/<name>) produced an invalid realtime URL and the websocket handshake hung. Normalize api_base to scheme and host before building the realtime path so both Azure OpenAI and Foundry bases connect Point the e2e realtime azure deployment at the GA gpt-realtime model and stop passing the os.environ refs the realtime path never unwraps, resolving them from the gateway env by name instead. Drop the local docker-compose scaffolding from the tree * test(e2e): add Gateway.list_files and list_fine_tuning_jobs for the discovery suite The discovery endpoints suite calls client.gateway.list_files and list_fine_tuning_jobs, which did not exist on Gateway, so both tests errored with AttributeError before reaching the proxy. Add the two GET wrappers using the existing FileListResponse / FineTuningJobsResponse models * revert(realtime): drop azure realtime api_base host-normalization The azure realtime handshake failure was a config issue, not a litellm bug: the realtime base was set to the Azure AI Foundry project endpoint (.../api/projects/<p>), but the OpenAI-compatible realtime route lives at the resource root. litellm correctly appends the realtime path to whatever base it is given, so pointing the realtime deployment at the resource root is the fix and no core change is needed * fix(ocr): route azure_ai doc-intelligence to its own endpoint at the source get_llm_provider inherits AZURE_AI_API_BASE into api_base for every azure_ai/* OCR model, but Azure Document Intelligence is a separate resource reached via AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT, so doc-intelligence requests went to the wrong host. Stop inheriting the azure_ai base for doc-intelligence models so api_base stays unset and both the rust bridge and the python get_complete_url fall back to the document-intelligence endpoint. This drops the earlier _rust_bridge_api_base reorder, which only covered the rust path and let the env silently override an explicit api_base * refactor(ocr): consolidate azure doc-intelligence detection; keep explicit api_base Extract is_azure_document_intelligence_model as the single source of truth for the azure_ai doc-intelligence sub-route so the check is no longer duplicated across _prepare_ocr_request and _rust_bridge_api_base, and gate the dynamic_api_base suppression on the caller not supplying an api_base so an explicit endpoint is always honoured. Restore xai to the realtime PROVIDERS as a documented disabled entry instead of dropping it silently, and add a regression test pinning doc-intelligence api_base resolution. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Mubashir Osmani <mubashir@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…nt edits and emit context-management-2025-06-27 beta (LIT-3393) (#32658) * fix(bedrock-invoke): retain clear_tool_uses_20250919 context_management edits and emit context-management-2025-06-27 beta (LIT-3393) Copy of #29206 by oss-agent-shin, rebased onto litellm_internal_staging so CircleCI can run. Bedrock InvokeModel supports automatic tool-call clearing (clear_tool_uses_20250919) under the context-management-2025-06-27 beta, but LiteLLM stripped the edit and dropped the beta header, causing a Bedrock 400. This maps bedrock.context-management-2025-06-27 to itself in anthropic_beta_headers_config.json (bedrock_converse stays null) and rewrites _filter_context_management_for_bedrock_invoke around an allowlist of supported edit types that keeps each supported edit and adds its matching beta. * test(bedrock-invoke): restore beta-headers config cache with a shared fixture in LIT-3393 tests Greptile flagged that three of the four new tests reloaded the module-level beta-headers config into local mode without restoring it on teardown, leaking state into later tests in the same process. Move setup/teardown into a local_beta_headers_config fixture used by all four tests. --------- Co-authored-by: oss-agent-shin <ext-agent-shin@berri.ai>
…reaking DB e2e tests (#32653)
…ion on the tools preview Authorization doubles as the admission fallback when x-litellm-api-key is absent, so a caller who authenticated the preview request that way had their LiteLLM key forwarded to the upstream as the oauth2/client-forwarded token. The preview now forwards Authorization only when the primary admission header is present, which is how the dashboard has always sent it; with no primary header there is no upstream token on the request at all. Applies to oauth2 and both client-forwarded modes; parametrized regression test plus the admission header added to the existing extraction tests to mirror the real UI request shape
…tadata The redacted resource kept the path, but hosted MCP servers routinely embed the credential in the path (for example /mcp/s/<token>/mcp), and mcp_tool_call_metadata is readable by a caller who can invoke the tool, so the path leaked the upstream credential into spend logs. Only scheme, host, and port are logged now
…y gate and tools preview The gateway authorize/token/register gate and the preview header extraction each carried their own inline copy of the oauth2 + client-forwarded mode set, which could drift from the discovery constant the registry builders use; all three surfaces mean the same thing (modes that run the upstream OAuth browser flow), so they now read the one constant
feat(mcp/ui): expose true_passthrough and oauth_delegate auth types with a no-auth warning
…relevant field changes An admin who ran Authorize & Fetch and then changed a field that determines which upstream OAuth token gets minted kept using the stale token for tool preview, sessionStorage, and (on the backend) the stored per-user credential and its cache. Grounded in RFC 8707/8693 and the MCP auth spec, a token is bound to one tuple: resource/audience (url), OAuth mode/grant (auth_type, oauth_flow_type), the authorization-server endpoints, and the OAuth client + scopes. A shared getOAuthAuthorizationIdentity captures exactly those fields; transport (http/sse on the same url is the same audience) and delegate_auth_to_upstream (a downstream-usage toggle never sent to the authorize request) are excluded. UI: both the create and edit forms now discard the held token (React state / sessionStorage / hook, plus the fetched token + DCR client in form.credentials) whenever the identity diverges from the one it was authorized against, re-applying the admin's in-flight edit so it is never wiped. The check lives in one shared helper so the two forms cannot drift. Backend: editing an MCP server now compares the pre/post identity and, on a mint-relevant change, purges every stored per-user OAuth credential for the server (DB row + per-user token cache) so no user forwards a token minted for a resource/AS/client that no longer matches. Best-effort; a purge failure never fails the update.
…er-user token store Review follow-ups on the stale-token invalidation. The backend identity now decrypts client_id and client_secret before comparing: the stored values are NaCl-encrypted with a fresh nonce on every write, so comparing ciphertext flagged every routine save as a mint-relevant change and purged per-user tokens that were still valid. The identity also gains spec_path, the audience for OpenAPI servers, and parses credentials stored as a JSON string The purge now routes each (user, server) through the manager's invalidate_user_oauth_token_cache, which becomes the single invalidation point covering both the legacy per-user token cache and the v2 per-user OAuth token store; previously the purge evicted only the legacy cache while the revoke path evicted only the v2 store, so each path left the other cache serving a replaced token until its TTL. A credential row racing in between the find and the delete is now detected via the delete_many count and logged; its cache entry expires by TTL On the dashboard, CLEARED_ON_INVALIDATION and the staleness check move to types.tsx as the single shared implementation for both forms. The edit form's transport handler now rechecks the identity after its programmatic setFieldsValue calls, which antd does not report through onValuesChange, so a token no longer survives a transport switch that clears the mint target. The create form rebuilds formValues from the post-reset form state after an invalidation instead of publishing the pre-reset snapshot, so the tool preview can no longer refetch with the discarded DCR client. Both transport handlers now share the recheck, which also stops the create form from over-invalidating on an http to sse swap that keeps the same url and therefore the same audience
…discipline The purge takes an injectable invalidate_token_cache callable defaulting to the manager's shared invalidation, and MCPServerManager takes an injectable per_user_token_cache alongside the existing per_user_oauth_token_store, so tests inject fakes instead of monkeypatching the global manager and the module-level cache. The new identity helpers drop Any for object throughout
…ilure cannot fail the edit The snapshot read only feeds the stale-token purge decision; leaving it unguarded meant a failed read would 500 an edit whose update would have succeeded, and it broke test_edit_mcp_server_redacts_credentials, whose mocked prisma is not awaitable on the un-patched get_mcp_server path. A failure now logs and skips the purge, consistent with the purge half already being best-effort. Adds the first endpoint-level coverage of the edit purge wiring: purge on a mint-relevant change, no purge when the identity is unchanged, and edit success with purge skipped when the snapshot read raises
…validation test The staged access token never reaches formValues (it is not a registered form field), so the assertion could not fail; the DCR client pair is the leak the test actually pins, proven by the mutation run
…ate caches on server delete LiteLLM_MCPUserCredentials stores BYOK API keys in the same column as per-user OAuth tokens, so the purge on a mint-relevant config change now deletes only rows whose payload decodes as an OAuth2 credential, each by its (user_id, server_id) pair, instead of every row for the server. An api_key server whose url changes purges nothing. delete_mcp_server now also invalidates each enumerated user's cached token so a re-created server reusing the id cannot serve tokens minted for the deleted one, and both cache drops are best-effort
…zation identity The identity used to pick the audience from spec_path only when values.transport was OPENAPI, but the create form keeps transport in component state rather than form values, so spec_path edits on OpenAPI servers never invalidated a held token. Comparing url and spec_path independently mirrors the backend's mcp_oauth_token_identity and fires regardless of whether transport is present. Invalidation now also wipes only credentials; the admin-typed endpoint fields are kept
…dit form For authorization_code the edit preview listed tools by server_id only, relying on the stored per-user DB credential, so a token authorized in the edit session gave an empty preview until the admin saved; the create form previews the identical state through the config-based preview endpoint, which takes the token explicitly. The edit fetch now routes through that same endpoint when a staged interactive token is held, built from the form values with the saved record as fallback, and keeps the by-server_id listing for every other case
The create and edit submit paths for true_passthrough and oauth_delegate persist only the tool configuration: the parametrized create test authorizes, disables the allowlist, and asserts nothing is persisted before submit, then that the create payload carries allowed_tools but no credentials and no occurrence of the token anywhere in the serialized payload, no per-user DB credential is written, and the token is committed to sessionStorage only, keyed to the created server. The edit save test gains the same serialized-payload assertion
…ction The field doc still said scheme + host + path while the redactor now strips the path along with userinfo, query, and fragment, since hosted MCP servers routinely embed the credential in the path
The per-row delete_many loop becomes a single delete filtered to the enumerated OAuth users' (user_id IN, server_id) pairs; same rows deleted, same BYOK-sparing precision, same count-mismatch detection, one round-trip instead of N
…; invalidation clears the tool list The preview endpoint infers client_credentials when the inherited client_id, client_secret, and token_url are all present (common once DCR or discovery filled them) and then strips the forwarded bearer to preview as M2M, so the staged interactive token was silently unused; sending oauth2_flow=authorization_code bypasses the inference. spec_path now rides along so OpenAPI servers take the spec-based preview path the create form gets. clearHeldOAuthToken also empties the tool list, mirroring the create form's clearTools, so a preview fetched with the discarded token never lingers while the refetch is in flight
#32704) Colocation follow-up to the App Router migration: move each page's owned components out of the shared src/components dump and into its route segment's _components/ folder, draining the shared bucket. Convention: a component used by exactly one segment goes in that segment's _components/ (private, matching Next's _ route-exclusion); a component shared by 2+ segments stays in @/components. No new _shared/ folder. Rename-in-place (segment already had a local components/ folder): - api-reference (also relocates the shared CodeBlock, used by playground and cost-tracking, to @/components/CodeBlock) - memory, budgets, access-groups - caching, projects, guardrails-monitor Extract from src/components (page view lived in the shared dump): - AdminPanel -> admin-panel, organizations -> organizations, general_settings -> router-settings, usage -> old-usage Each folder/view was verified to have no importer other than its own page (cross-checked across src, tests, and e2e_tests). Relative imports inside moved single files are rewritten to absolute @/components/*; colocated tests move with their subject and have their vi.mock paths rewritten to match. Grandfathered lint suppressions (tremor, react-hooks, and similar, all pre-existing) are re-keyed to the new paths with counts unchanged. No behavior change.
…dation fix(mcp): invalidate a browser-authorized upstream token when a mint-relevant field changes
…ts-1d4b26 fix(proxy): resolve team org from team_id so org admins can update team budgets
azure batch used azure/gpt-4.1-mini-batch; gpt-4.1-mini is deprecating (2026-11-04) and can no longer be deployed, so point it at gpt-5.4-mini (Global Batch) and bump the api_version to 2025-04-01-preview. Requires an Azure Global Batch deployment named gpt-5.4-mini-batch plus AZURE_API_BASE/AZURE_API_KEY on the proxy. xai/grok-4-1-fast-non-reasoning is deprecated (2026-05-15); update the commented xai realtime provider and the coverage-matrix doc to xai/grok-4-1-fast.
Contributor
|
Too many files changed for review. ( Bypass the limit by tagging |
|
|
yuneng-berri
enabled auto-merge
July 10, 2026 02:02
shin-berri
approved these changes
Jul 10, 2026
tin-berri
approved these changes
Jul 10, 2026
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Relevant issues
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Type
🆕 New Feature
🐛 Bug Fix
🧹 Refactoring
📖 Documentation
🚄 Infrastructure
✅ Test
Changes