Commit graph

4151 commits

Author SHA1 Message Date
yucheng-berri
e1717c5e9c
fix(proxy): return the real status code when a credential update is rejected (#36166)
* fix(proxy): return the real status code when a credential update is rejected

update_credential ended its except clause with 'return handle_exception_on_proxy(e)'. Returning the exception makes it the response body, so FastAPI answers 200 and every rejection on this route reads as a successful write to any caller that checks the status; the admin dashboard's API client is one. Patching a name that does not exist answered 200 with the real 404 buried in the body.

The sibling handlers in this file already raise. The route had no test coverage, which is why it survived.

* Update tests/test_litellm/proxy/credential_endpoints/test_endpoints.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-08-07 08:02:52 +00:00
Harry Qian
da443d1266 test(proxy): lock in query-param validation across fastapi param types
Guards _declared_query_params against a regression in the get_flat_params
migration: the flatten step returns path, query, header and cookie params
together, so a dropped ParamTypes.query filter would wrongly treat path or
header names as declared query params and accept unknown ones. Removing the
filter fails these tests.
2026-08-07 15:36:50 +08:00
mateo-berri
a0e35990cf test: rename openai files common utils test to a unique basename 2026-08-06 23:15:48 -07:00
LHMQ878
55b52970ea fix(proxy): preserve OpenAI WS query params and provider auth
Forward realtime model query string, keep OPENAI_API_KEY (forward_headers=False),
satisfy ruff strict gates, sync dashboard OpenAPI types, and cover the behavior in tests.
2026-08-07 11:09:27 +08:00
Devin AI
8d6247c9c1 fix(proxy): send SSE keepalive pings on OpenAI-shaped streaming routes
Streaming /chat/completions and /v1/responses emit nothing, not even response headers, until the upstream yields its first chunk, so an ingress with an idle read timeout (nginx proxy-read-timeout) drops long time-to-first-token streams.

Reuses the existing Anthropic keepalive wrapper with a configurable ping payload, emitting an SSE comment on the OpenAI-shaped routes so conformant clients ignore it. Off unless litellm_settings.sse_keepalive_ping_interval_seconds is set.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-07 03:00:04 +00:00
LHMQ878
8d6b8d2ce9 fix(proxy): register WebSocket passthrough for OpenAI prefixes
create_websocket_passthrough_route existed but /openai and
/openai_passthrough only registered HTTP methods, so WS upgrades were
rejected at routing. Add catch-all websocket routes mirroring the HTTP
passthrough target construction.

Fixes #36088
2026-08-07 10:47:42 +08:00
mateo-berri
02d60847ee Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_list_batches_resolves_unified_ids
# Conflicts:
#	enterprise/litellm_enterprise/proxy/hooks/managed_files.py
2026-08-06 19:00:51 -07:00
Yassin Kortam
2b38991df9
fix(a2a): stop writing per-caller state onto the shared cached httpx client (#35978)
create_a2a_client took the raw client off a process-wide cached handler and
called headers.update() on it, then leaned on folding the header set into the
cache key (through the unrelated disable_aiohttp_transport field) to keep one
caller's credentials away from the next.

Per-caller headers now ride with each request through the a2a SDK's call
context, and the agent card fetch gets them through resolver_http_kwargs, so
the shared client is never written to and its cache key no longer varies by
header set. Since the proxy puts a fresh trace id in every request's headers,
that key previously changed on every call, giving each request its own httpx
client and flushing the 200-entry client cache that every other provider
shares. All A2A callers on one timeout now reuse a single pooled client.

Sharing that client also means sharing its httpx cookie jar, which httpx fills
from every Set-Cookie and replays on any later request to a matching domain, so
one agent's session cookie would arrive at another agent on the same host. The
pooled client now carries a cookie policy that stores and sends nothing, which
neither litellm nor the a2a SDK relies on: the SDK's auth interceptor skips
cookie-borne API keys outright.
2026-08-06 18:58:26 -07:00
yucheng-berri
988ee8b85d
fix(proxy): promote caller metadata trace fields into litellm_metadata (#35866)
* fix(proxy): promote caller metadata trace fields into litellm_metadata

Routes in LITELLM_METADATA_ROUTES keep the caller's metadata as a provider
passthrough field and track proxy state in litellm_metadata, which is the dict
the logging integrations read. The caller's trace_id, session_id, trace_user_id
and trace_metadata therefore never reached any callback on /v1/responses,
/v1/messages, /v1/batches or /v1/files, and mask_input / mask_output were
dropped with them so a caller asking for redaction had their prompt logged in
full.

Promote an explicit allow-list of those fields from the requester_metadata
snapshot into litellm_metadata, never overwriting a value already set so
header-derived ids keep precedence. Trace-mutation controls (existing_trace_id,
update_trace_keys) and trace_public are deliberately excluded: langfuse applies
them to an arbitrary caller-chosen trace with no ownership check. tags is
excluded because per-tag budget enforcement runs earlier, at auth time.

This covers providers with a native Responses API config. Providers reaching
/v1/responses through the chat-completions bridge need the companion change to
get_litellm_params.

* ci: retrigger workflows
2026-08-06 17:07:43 -07:00
devin-ai-integration[bot]
b7749f67f1
fix(proxy): warn at startup when max_budget is set but no database is connected (#36041)
* warn at startup when a proxy-wide budget is set but no DB is connected

litellm.max_budget is only enforced via DB-loaded global spend, so a DB-less proxy silently ignores it. Log a one-time startup warning.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(proxy): inject max_budget into DB-less budget warning

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(proxy): cover DB-less budget warning startup call

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(proxy): pin DB-less budget warning call site

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(proxy): stabilize budget warning call-site pin

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: tin <tin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-06 16:27:41 -07:00
Mateo Wang
0c3017e1de
Merge pull request #35371 from rimysore/fix-managed-batch-cross-provider-fallback
fix(batches): prevent managed file fallbacks
2026-08-06 11:40:38 -07:00
yuneng-jiang
63c639f18b
Merge pull request #36062 from BerriAI/litellm_/lucid-pike-b1ee0e
fix(proxy): allow non-admins to reach /user/daily/activity/aggregated
2026-08-06 10:54:20 -07:00
mateo-berri
845680ed1d test(proxy): unit test batch file id mapping helpers directly 2026-08-06 10:50:09 -07:00
Mateo Wang
73ea5e5602
Merge pull request #36048 from BerriAI/litellm_cancelled_batch_unified_output_ids
fix(batches): persist managed file ids for cancelled/failed/expired batches
2026-08-06 10:40:07 -07:00
yuneng-jiang
48cb89dba7
Merge pull request #36061 from BerriAI/litellm_/blissful-elion-1e2003
fix(proxy): stop resolving the UI session sentinel team on /search_tools/list
2026-08-06 10:04:44 -07:00
Devin AI
5a5bb8c9d8 fix(proxy): stop /{provider}/v1/files from capturing /openai_passthrough
The native files and batches routes declare /{provider}/v1/... and their routers are mounted before the passthrough router, so /openai_passthrough/v1/files and /openai_passthrough/v1/batches matched them with provider="openai_passthrough" and 500'd on the LlmProviders lookup instead of reaching openai_proxy_route.

Move the dedicated /openai_passthrough prefix onto its own router mounted ahead of the batches and files routers. /openai/... and every other provider prefix keep their current behavior.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-06 15:29:39 +00:00
mateo-berri
f9d48bd47c
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_reduce_any_types
# Conflicts:
#	basedpyright-code-budget.json
#	type-discipline-budget.json
2026-08-06 11:48:17 +00:00
Mateo Wang
729bec69f5
Merge pull request #36014 from BerriAI/litellm_scan_only_tool_results
feat(guardrails): add scan_only_tool_results to scope unified guardrails to tool results
2026-08-06 02:17:29 -07:00
mateo-berri
14d4897e55 fix(guardrails): refuse scan_only_tool_results combos that scan nothing
Prompt Security drops tool and function rows unless check_tool_results
is on, so it now reports scan-only support from that setting and the
registry refuses the pairing at boot. Pairing scan_only_tool_results
with skip_tool_message_in_guardrail excludes every message, so guardrail
initialization now rejects that combination too.
2026-08-06 01:58:06 -07:00
mateo-berri
7d745521bf fix(guardrails): merge synthesized tools under scan_only_tool_results and reject role-filtered no-op combos at init 2026-08-05 23:46:40 -07:00
mateo-berri
e0c4c7cee0 Merge branch 'litellm_internal_staging' into bugfix/managed-batch-cost-not-logged 2026-08-05 23:42:18 -07:00
Yuneng Jiang
d5a471b2a7
test(proxy): type the search tool test helpers and record lookups with AsyncMock
Replaces the hand-rolled recording double with AsyncMock so the awaited team ids
come from await_args_list instead of a mutated list, and annotates the response
factory now that SearchToolInfoResponse is imported at module level.
2026-08-05 23:40:16 -07:00
Yuneng Jiang
eea292abba
fix(proxy): allow non-admins to reach /user/daily/activity/aggregated
The aggregated route was missing from LiteLLMRoutes.self_managed_routes
while its paginated sibling /user/daily/activity was listed, so auth
rejected every internal user with a 401 before the handler ran. That
route backs the default "Your Usage" view in the dashboard, which left
the main Usage page broken for non-admin users.

The handler already self-scopes: it checks admin view first, then falls
back to require_caller_user_id_for_non_admin, defaults a missing user_id
to the caller's own, and returns 403 when a non-admin asks for someone
else's data. Listing the route restores reachability without widening
what a caller can read.

check_route_access matches exactly (plus explicit wildcards), so the
parent entry never covered the /aggregated sub-path.
2026-08-05 23:24:37 -07:00
Yuneng Jiang
54e9964eb8
fix(proxy): stop resolving the UI session sentinel team on /search_tools/list
Every Admin UI session key is stamped with the reserved team id
`litellm-dashboard`, which never has a row in LiteLLM_TeamTable, so the
team lookup in _filter_visible_search_tools raised 404 and the endpoint
returned 500 for every non-admin dashboard session.

Skip the lookup for that sentinel and scope the caller by its key-level
allowlist alone, matching how MCP and agent permission checks already
treat it. A real team id is still resolved, and a genuine lookup failure
now surfaces with its own status instead of being masked as a 500.
2026-08-05 23:22:11 -07:00
tin-berri
86890654c5
fix(proxy): include today's UTC bucket when a daily activity range ends at the caller's current day (#36051)
* fix(proxy): include today's UTC bucket when a daily activity range ends at the caller's current day

* fix(proxy): gate the current-UTC-day extension behind an opt-in param sent by the cost optimization dashboard

* fix(ui): label cost optimization savings dates as UTC days
2026-08-05 22:33:54 -07:00
mateo-berri
e2cb01c87c
fix(types): correct annotations that were false about their runtime values
An adversarial review of the previous commit found annotations that
described what the code wished were true rather than what flows through.
A false annotation is worse than the Any it replaced, since it launders a
wrong assumption past the type checker.

- purview: `_resolve_user_id` claimed every request-body value was a
  Mapping, contradicting `_resolve_trusted_user_id` one method over, which
  types the same argument `Mapping[str, object]`. `_should_block` claimed
  every Graph response value was a sequence of str->str mappings and was
  not assignable from its own producer's return type.
- cato: `_CatoAnalyzeResponse.required_action` was required and
  non-nullable while the API returns null, as seven fixtures in the
  guardrail's own suite assert. `analysis_result` had the same problem.
  The streaming hook narrowed an override parameter below what
  `ProxyLogging` actually passes it.
- marketplace: `_PluginRecord.manifest_json` was `str` against a nullable
  column. Making it honest surfaced a latent crash, covered below.
- ownership: two functions took an attribute Protocol while their own
  bodies branch on `isinstance(response, dict)`, which no Protocol can
  satisfy.
- openapi generator: `paths` claimed every path-item value was an
  operation, though path items also carry `parameters`, `summary` and
  `$ref`.
- custom openapi spec: a TypedDict asserted a shape that the function
  returns raw Pydantic sub-schemas out of. Reverted to Any, which is
  imprecise but not false.

`get_marketplace` did an unguarded `json.loads` on the nullable
`manifest_json` inside an `except json.JSONDecodeError`, which cannot
catch the TypeError a NULL raises, so one NULL row 500s the endpoint. It
now skips the plugin like the file's other two read sites already do, with
a regression test that fails without the guard.

Where honesty cost precision, precision lost. `_should_block` went back to
its original signature entirely: the narrowing needed to type it turned a
fail-closed DLP control fail-open, because the TypeError it used to raise
on a malformed response reached `except Exception` and became a 400.
2026-08-06 04:37:48 +00:00
mateo-berri
c2998dea75 fix(guardrails): guard tools write-back under scan_only_tool_results and warn on role-filtered no-op scans 2026-08-05 20:49:15 -07:00
mateo-berri
5339ec50e7 fix(batches): persist managed file ids for cancelled/failed/expired batches
When the batch cost poller found a batch in a terminal failed, expired, or
cancelled state it wrote the provider response straight to the managed object
table, so the stored blob kept raw provider file ids and a raw batch id. Since
the row is final after batch_processed=True and the read paths only resolve
existing managed ids, every later GET /batches/{id} and GET /batches leaked
raw provider output and error file ids that clients cannot fetch through the
proxy. The terminal branch now normalizes the response with
ensure_batch_response_managed_file_ids before persisting, minting managed ids
under the batch owner's identity

POST /batches/{id}/cancel had the same gap: it called update_batch_in_database
without the caller's auth context, so a cancel response that already carried
provider file ids could never mint managed ids. The endpoint now forwards
user_api_key_dict
2026-08-05 20:42:20 -07:00
Mateo Wang
b617e672e3
Merge pull request #36024 from BerriAI/litellm_anthropic_sse_keepalive
fix(proxy): send keepalive pings on anthropic messages SSE streams during upstream silence
2026-08-05 19:41:35 -07:00
Mateo Wang
60d9e6012c
Merge pull request #35999 from BerriAI/litellm_guardrails_v1_messages_tool_traffic
fix(guardrails): scan /v1/messages tool traffic
2026-08-05 18:08:47 -07:00
Mateo Wang
b9b239b0fb
Merge pull request #35980 from BerriAI/litellm_content_filter_post_mcp_call
fix(guardrails): allow litellm_content_filter to run on post_mcp_call
2026-08-05 18:08:02 -07:00
mateo-berri
097c03eebb fix(proxy): tolerate non-scalar sse keepalive interval config shapes 2026-08-05 17:33:09 -07:00
mateo-berri
6ca120a674 fix(proxy): coerce and validate the sse keepalive ping interval from config 2026-08-05 17:23:39 -07:00
Souravrajvi0
388943ac17
fix(proxy): register managed batch output files on terminal retrieve (#34092)
Terminal batch retrieve could return the raw provider output_file_id, which
skips managed-file ownership checks on /v1/files/{id}/content and lets any key
on the proxy download another user's batch output.

Retrieve now registers the missing managed-file row before responding, and
attributes ownership to the durable batch owner rather than the retrieving
caller, so output and error ids always come back as unified managed ids.

Fixes #33989
2026-08-05 16:38:49 -07:00
tin-berri
55e666a05f
feat(complexity_router): report LLM classifier cost per request via routing_decision and x-litellm-classifier-cost header (#36015) 2026-08-05 16:27:32 -07:00
ryan-crabbe-berri
7e8d0d3130
fix: rebuild models_by_provider in add_known_models so cost map reloads reach wildcard expansion (#36010)
* fix: rebuild models_by_provider in add_known_models so cost map reloads reach wildcard expansion

* fix: refresh models_by_provider in place so captured references survive reloads
2026-08-05 16:24:43 -07:00
mateo-berri
131339d8e5 fix(proxy): send keepalive pings on anthropic messages SSE streams during upstream silence 2026-08-05 16:15:04 -07:00
Yuneng Jiang
298fb8ce56
fix(health): drop a stored-credential reference along with the credentials it names
A connection test that redirects the destination already leaves the configured
credentials behind. It kept litellm_credential_name, which names the same stored
secrets and is resolved further down the call, so the reference is now dropped
with them. A request that sets no connection fields of its own is unaffected,
which is how the Admin UI tests a configured model.
2026-08-05 16:07:04 -07:00
Yuneng Jiang
59173c3a20
feat(health): let allow_client_side_credentials re-enable configured-credential reuse
The proxy-wide opt-in that already governs callers supplying their own
connection parameters now also governs whether a connection test may pair a
request-supplied endpoint with the configured deployment's credentials. Off by
default, which keeps configured credentials scoped to the endpoint the
configuration names; on, the previous merge behaviour is available unchanged.
2026-08-05 15:58:49 -07:00
Yuneng Jiang
b468acb31c
fix(health): stop inheriting configured credentials when a connection test sets its own
A request that supplies its own connection fields describes a connection of its
own, so the configured deployment's credentials are no longer merged underneath
it. Anything the request leaves unset still comes from the configuration, so
naming a configured model and testing it as configured is unchanged, and adding
a second deployment for an already-configured name works as before.

Replaces the earlier outright rejection, which also refused requests that
supplied a complete connection of their own.
2026-08-05 15:49:41 -07:00
Yuneng Jiang
e5effcb861
fix(health)!: let configured deployment parameters win over request overrides
When a connection test names a model that resolves to a configured deployment,
that deployment's routing and credential parameters are authoritative. A request
supplying a complete connection of its own is unaffected.

BREAKING CHANGE: /health/test_connection no longer lets a request replace the
routing or credential parameters of a configured model it names. Supply the full
connection parameters instead of naming a configured model.
2026-08-05 15:17:43 -07:00
Yuneng Jiang
5b2c92d749
fix(proxy)!: parse bracket-notation form metadata the same way its JSON form is parsed
Multipart callers express nested metadata as flat bracket-notation keys, which
reach the request-body check as literal keys rather than as a metadata dict.
The check now rebuilds them with the same helper the endpoints use, so both
encodings are handled identically and cannot drift apart.

BREAKING CHANGE: a multipart field such as `litellm_metadata[api_base]` is now
subject to the same request-body parameter rules as its JSON equivalent. Set
`general_settings.allow_client_side_credentials`, or the deployment's
`configurable_clientside_auth_params`, to keep passing these.
2026-08-05 15:17:43 -07:00
Yuneng Jiang
fc4be70a37
fix(proxy)!: share one destination check between body and path-supplied model
The URL-destination check previously ran over request-body fields only. The
per-field logic moves into reject_url_valued_destination(field, value) so a
deployment name resolved from the request path runs the same check against the
same admin allowlist.

BREAKING CHANGE: a deployment name supplied in the request path that parses as
an http/https destination is now refused. Add the host to
`provider_url_destination_allowed_hosts` in litellm_settings to keep it working.
2026-08-05 15:17:43 -07:00
mateo-berri
f16f3e23cd fix(tool_permission): fail closed on unverifiable SSE streams and end the turn when every tool call is denied
An SSE stream that cannot be positively identified as Anthropic (no
parseable message_start event) now blocks instead of passing through
unscanned, closing the bypass where any raw-SSE backend skipped tool
permission checks entirely. Buffered chunks are joined back into one
stream before parsing, so events split across network chunk boundaries
assemble correctly instead of being silently dropped. Rewrite mode now
resets finish_reason to stop when no tool call survives, so the
re-encoded Anthropic stream reports stop_reason end_turn and clients do
not wait for a tool result that never comes
2026-08-05 15:06:51 -07:00
Yassin Kortam
c3a8962c00
fix(proxy): only treat a recoverable database outage as grounds to serve without one (#35864)
`is_database_connection_error` answered True for any `PrismaError` it did not
recognize, on the reasoning that an unclassified failure might be an outage and
the safer default was to keep serving. That default is inverted for faults that
never resolve. A query engine that is missing or version-skewed, a malformed
generated query, or a misused transaction all satisfied the predicate, so with
`allow_requests_on_db_unavailable` enabled the proxy would absorb one, boot
clean, and keep issuing fallback identities for as long as the process ran.

The predicate is now an allowlist: the httpx transport errors, prisma's
`EngineConnectionError`, and a `no_db_connection` ProxyException. That is what a
real outage produces, since the query engine is a local HTTP server and an
unreachable database surfaces as a transport failure against it, so the
high-availability path is unchanged. Anything unrecognized is now treated as
permanent and surfaces instead of being absorbed.

Deciding whether to serve without a database and deciding what to tell the
caller are different questions, so they no longer share a predicate.
`is_database_infrastructure_error` keeps the previous broad behavior and now
backs the reporting and recovery paths: service-unavailable classification, the
access-group endpoint's status mapping, and the health watchdog's reconnect
trigger. Their behavior is unchanged. Without that split, a permanently faulted
engine would have started reporting as an authentication failure, sending an
operator after a credential problem that does not exist.
2026-08-05 14:15:13 -07:00
mateo-berri
bee787b4b5 fix(guardrails): scan /v1/messages tool traffic
Guardrails silently skipped three surfaces on the Anthropic Messages
path, so an agent loop driven by /v1/messages ran unguarded:

- The Anthropic input translation never walked tool_result blocks, so
  content returned by a local tool (a curl, a file read, an MCP call)
  reached the model unscanned in both the string and list content
  shapes, images inside a tool_result included.
- tool_permission only understood ModelResponse, so an Anthropic
  non-streaming response or a raw SSE stream carrying tool_use blocks
  passed through with no rule ever evaluated.
- ContentFilterGuardrail scanned inputs["texts"] but never
  inputs["tool_calls"], so the arguments a model proposes for a tool
  call went unchecked.

Tool call arguments are parsed as JSON before filtering so a MASK
action rewrites the value and leaves the payload valid JSON; non-JSON
arguments fall back to scanning the raw string. Denied tool_use blocks
are dropped from the Anthropic content array and replaced with a text
block, and stop_reason resets to end_turn when nothing tool-shaped
survives.
2026-08-05 14:11:27 -07:00
tin-berri
32deaff015
feat(spend): rebuild the auto-router benchmarks backend as a per-session rollup (#35910)
Folds every successful auto-routed request into LiteLLM_AutoRouterSession with one
conditional upsert at spend-write time, classifying each turn (same model, first
visit, return to tier, out of order) against the row's own columns so nothing is
read before the write. The upsert's placeholders and argument tuple both derive
from the transaction dataclass's own field order, so the SQL and the call site
cannot drift apart. GET /auto_router/benchmarks aggregates the rollup, grouped
by the full (router, type) identity, and never scans LiteLLM_SpendLogs. A turn's
cache interaction is derived once from its usage record (savings.py owns the
extraction; compute_savings_spend derives cache reads from usage_object itself),
hits are counted order-independently so the overall hit rate matches its covered
denominator, caller-chosen session ids are bounded before entering the primary
key, and a poisoned statement drops only its own session's remaining turns.
Return misses inside the recorded TTL are named for what the telemetry shows
(within_ttl) rather than a presumed cause, since a provider can evict early.
Savings ride each router's derived baseline by default, so the response carries
no deployment-wide baseline label. Rollup retention has its own
maximum_autorouter_session_retention_period setting, pattern-identical to the
spend-logs knob and running in the same cleanup job on its own cutoff. Every
drain trigger sizes the queues through one owner and the enqueue honors
disable_spend_logs beside the tool-usage queue it mirrors.
2026-08-05 20:06:32 +00:00
Mateo Wang
c6dbf48944
Merge pull request #35916 from BerriAI/litellm_passthrough_live_credentials
fix(proxy): resolve pass-through credentials live from router deployments
2026-08-05 12:57:46 -07:00
Yassin Kortam
347798b80e
fix(router): keep custom model_info across a price data reload (#35491)
A price data reload replaced litellm.model_cost wholesale, discarding every
runtime registration: the deployment model_info the Router registers from
model_list, and pricing overrides passed to litellm.register_model. Custom
model groups lost max_input_tokens / max_output_tokens in /model_group/info,
and a deployment whose backend model is in the catalog silently reverted to
upstream values. Runtime registrations are now recorded and replayed on top of
the freshly fetched catalog.

Router._pre_call_checks resolved the per-deployment model name only after the
model-info lookup, so an unregistered model left it unset and the supported
params check ran against the bare model group name, raising "LLM Provider NOT
provided" out of deployment selection. The name is now resolved first, and an
unresolvable provider skips that check rather than failing the request.

Resolves LIT-4675
2026-08-05 19:56:50 +00:00
Yassin Kortam
7ac1085931
fix(auth): return 403 from the OAuth2 enterprise gate (#35838)
The enterprise gate on the OAuth2 auth path raised a bare `ValueError`,
which the terminal handler in auth_exception_handler.py converts to a 401.
Every sibling enterprise gate answers 403, including `_premium_user_check`
and the SSO gate. A 401 tells the client its credential was wrong and to
retry with a better one, and no credential can satisfy that while the
install is unlicensed, so it invites a retry loop that can never succeed.

It now raises a 403 `ProxyException` shaped like the SSO gate. Two response
fields move with it: the `Authentication Error, ` prefix goes away, since
the catch-all built that around `str(e)` and a `ProxyException` is re-raised
unmodified, and `param` becomes `premium_user`, naming the condition an
operator has to clear.

The gate's own text also gains the sentence break it was missing. The
message concatenated straight onto `CommonProxyErrors.not_premium_user`,
rendering as "premium usersYou must be a LiteLLM Enterprise user".
2026-08-05 12:53:56 -07:00