Commit graph

110 commits

Author SHA1 Message Date
Joshua Valluru
ee42fc8f06 fix: preserve replay generator typing and cover switchyard boundaries 2026-10-31 16:46:04 -07:00
Nate Armstrong
c43a1addf2
refactor(router): extract PythonCore behind RouterCore and pass the router its config (#49119)
* refactor(router): move litellm/router.py to litellm/router_python_core.py

A pure rename with no content change, so git and review tools track the router body as a move.
The next commit turns it into PythonCore and adds the Router facade back at litellm/router.py

* refactor(router): extract PythonCore behind RouterCore and pass the router its config

The routing logic moves unchanged from litellm/router.py into PythonCore in
litellm/router_python_core.py. Router becomes a facade that builds a RouterConfig, picks a
RouterCore with make_core and delegates every member to it. The core no longer reads or writes
litellm.* directly: process settings come from RouterDynamicConfig, and inference calls, sleeps,
callback lists and the global cache go through PythonHost. A CI check bans litellm.* in the router
modules, and the proxy's construction sites build the config outside the router

Settings are static: the constructor arguments and changeset values are copied when the router is
built or the change applied, and a write to a setting attribute becomes apply(ChangeSet(assign=...)).
A Router subclass's core class also subclasses the facade, so an override's super(Sub, self) works,
and an instance-level patch of a core method is kept on the facade so nested patches unwind.

tests/unit/test_router/core tests the core in isolation: ScriptedHost serves every host operation
(scripted inference answers, recorded sleeps, settings, callbacks, cache), and make_core runs each
test once per core in CORE_BUILDERS
2026-10-31 12:54:23 -07:00
Kerry Lu
6f86b117b5
fix(moderations): stop the router image check from 500ing on a list input (#49454)
* fix(moderations): stop the router image check from 500ing on a list input

* fix(moderations): give the list cast its cast-ok reason

* fix(moderations): type the responses input guard and keep SDK compaction items

* fix(moderations): import the compaction item through litellm.types and mark the TypeIs import

* fix(moderations): sort the compaction item import

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-31 12:05:13 -07:00
yucheng-berri
efc2304b7e
fix(streaming)!: fail a stream that ends without a finish reason (#47665)
* fix(streaming): fail on clean EOF without finish reason

* fix(streaming): cover shared OpenAI-compatible EOF paths

* test(router): complete mocked streaming responses

* fix(streaming): keep the EOF error message and cover Anthropic clean EOF

The request-less RemoteProtocolError tripped exception mapping into
'The .request property has not been set.' for non-OpenAI providers.
Adds Anthropic and generic-chunk unit tests, an integration test across
chat, the /v1/messages bridge and the /v1/responses bridge, and moves
main's token-estimate test onto a finished stream with a sibling that
checks the cut stream still bills the estimate on its failure log.

* fix(streaming): count a finish reason any chunk sent, keep LangGraph async streams whole

Gemini marks every chunk unfinished and a later usage chunk reset
intermittent_finish_reason, so a finished Gemini stream failed at EOF.
LangGraph's __aiter__ opened a new aiter_lines on every wrapper call,
which hit StreamConsumed after the first chunk and now surfaced as a
false EOF error instead of a silent truncation.

* fix(streaming): keep the synthetic stop for a stream that sent no chunks

Gemini's OpenAI-compatible endpoint answers with only [DONE] when
reasoning uses up max_tokens. Failing every openai/azure stream that
sent nothing turned that 200 into a 500, while every other provider
already kept the stop for an empty stream. Adds integration cells for
Gemini's trailing usage chunk, multi-event LangGraph runs, a mocked
deployment and the [DONE]-only answer.

* test(streaming): keep pytest.raises to one statement, drop the spend read for an empty stream

An empty stream writes no spend row on main either, so the [DONE]-only
cell checks only what the client sees.

* fix(streaming): read the chunk's finish reason for the sticky flag

* fix(agentcore): end a stream on messageStop when the runtime sends no metadata or final message

* test(streaming): pin the logged-chunks guard, inject the LangGraph client, use current model names

* fix(agentcore): say why the end-of-stream flags rebind

* test(streaming): cover an AgentCore runtime that ends on messageStop through the proxy

* test(streaming): cover SDK clients, request fallbacks and a concurrent burst for a cut stream

* test(agentcore): inject the async transport instead of patching the aiohttp flag

* test(guardrails): end the decision model chat fixture with a finish reason, as a real stream does

* test(streaming): shape the AgentCore and LangGraph fixtures like captures from a real runtime and server

* test(streaming): keep each stream's ending and land every spend row once through a spend log outage

* test(streaming): end each stream by its provider's finish after a proxy worker is killed mid burst

* test(streaming): run the outage test as one burst before and one during the outage instead of a capped wave loop

* test(streaming): connect the outage test's proxy to the relay without TLS so its trigger sees the spend-log insert on an ssl=on Postgres

* test(streaming): type the new stream EOF unit tests

* test(streaming): a merged reasoning stream cut mid reasoning now fails

Main's think tag test expected a stream that ends mid reasoning without a finish reason to finish with a closed think block. With this PR that stream fails, so the case moves to its own test that expects MidStreamFallbackError with the open think block as the partial content

---------

Co-authored-by: King Star <mcxin.y@gmail.com>
Co-authored-by: yucheng-berri <293926326+yucheng-berri@users.noreply.github.com>
2026-10-31 11:29:26 -07:00
joshua-berri
6e27da8af9
fix(router): inspect image content without translating routed requests (#49371)
* fix(router): inspect image content without translating routed requests

* fix(router): preserve screenshot image routing

* test: make monthly budget reset coverage deterministic

---------

Co-authored-by: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com>
2026-10-31 10:44:24 -07:00
Nate Armstrong
87d91d78b7
refactor(router): route the router family through RoutingContext and close the outside surface (#49118)
Strategies, pre-call checks, cooldown and fallback helpers, and the auto, complexity and quality
sub-routers now take a RoutingContext instead of the Router, and read the router only through
registry(), settings(), health() and the context's forwarding methods. Router-free helpers move
from router_utils to litellm_core_utils, outside callers of router state switch to entry points,
and the surface allowlist shrinks to the endpoint methods plus the entry points
2026-10-30 19:18:54 -07:00
tin-berri
632f71b850
fix(router): recover streamed context window errors before output (#49293) 2026-10-30 13:19:58 -07:00
tavren27
43c07a0f12
fix(router)!: check and count deployment itpm and otpm on every Responses WebSocket turn (#49069)
* fix(router): check and count deployment itpm and otpm on every Responses WebSocket turn

The success hook only reconciled an ITPM/OTPM reservation found in top-level
metadata or the standard logging object, but on the proxy the reservation lives
in litellm_params.metadata, so no reservation was ever settled there. The hook
now always calls the reconcile, which already returns early without a reservation
and clears what it settled, so a second success event is a no-op.

A Responses WebSocket turn after the first now has its deployment's current-minute
ITPM and OTPM counters checked and is refused with a 429 frame once a counter is at
its limit. An admitted turn carries a zero reservation on the current minute's
keys, so its actual tokens land on those counters on both the native and the
managed relay.

* fix(responses): settle the socket open's itpm/otpm reservation on the first turn only

After the first turn takes its logging copy, both websocket relays drop the
open's ITPM/OTPM reservation keys from the connection metadata, the same way
the budget reservation is detached. Later turns, unsolicited backend events,
turns after limits are removed mid-socket, and sockets with no turn admission
no longer settle or refund the open's reservation a second time.

* refactor(responses): read the itpm/otpm reservation keys from core metadata helpers instead of router_utils

* fix(responses): keep a socket open's hold for its first real turn and check tokens before request limits

A turn that will settle the socket open's itpm/otpm reservation no longer counts
that reservation against itself or swaps it for a zero one, so Codex's warmup
frame on a managed provider, or a refused first frame on the native socket, no
longer blocks the first real prompt or strands the hold for the minute.

The token check now runs before the rpm/rpd checks, so a turn refused for
tokens never uses up a request slot. Token-check tests pin the clock, and the
managed relay settle test drives a real streamed call through a custom provider.

* test(router): pin the router clock in the socket turn admission tests

Both tests seeded this minute and the next to dodge a minute boundary.
They now use the file's pinned-minute fixture and seed only that minute,
so neither depends on when the test runs.

* fix(router): check a responses socket's open without reserving and read shared token counters first

A Responses WebSocket open now runs the same itpm/otpm check a turn runs and
reserves nothing. It used to reserve an estimate that falls back to the
model's max output, so a socket could not open under any otpm below that.
The open still records a zero reservation for its own minute, so the first
frame, which the relay forwards without a turn check, counts its actual
tokens like every later turn.

This drops the plumbing that only carried the open's hold into the first
turn. The turn admission callable takes no arguments again, and the native
relay detaches only the budget reservation, as before this change.

The itpm/otpm turn check reads Redis first and falls back to this
instance's in-memory count only without Redis or when the circuit breaker
is open. These counters go down when a call settles, so an in-memory copy
at the limit could refuse turns after the shared count dropped. The tpm
check keeps its in-memory shortcut, since its counter only grows within a
minute.

Responses usage that reports input_tokens_details.cached_tokens now leaves
cached input out of itpm, as chat usage already did.

* fix(router): leave a socket turn's cached input out of itpm

The native Responses WebSocket relay logs a turn with its list of events,
so the deployment rate limit hook finds no usage on the response and falls
back to the standard logging payload. That fallback read cached input from
metadata.cache_read_input_tokens, a key the payload's cleaned metadata never
carries, so every cached token of a socket turn was counted against itpm.

The fallback now reads the cached count from the payload's usage_object,
the same usage the logging path already built for cost, so a native socket
turn counts only its uncached input. The managed relay and HTTP
/v1/responses already counted correctly through the response's own usage

* fix(router): mark only responses socket opens as check-only and count a turn on the minute it finishes

The check-only branch for a socket open keyed on a websocket value in the request kwargs, which any HTTP body can carry and which the realtime route also sets. The router now passes an opens_responses_socket flag into set_io_token_rate_limit_request_kwargs only for its _aresponses_websocket call, and the flag lives in the same context value as the request kwargs, so no request body can set it and realtime opens reserve and get refused as before

A zero reservation holds nothing to refund, so a socket turn now counts its real tokens on the minute the call finishes, read from the clock at settle time. A non-zero reservation still settles on the keys it reserved on so its refund hits the same counter

When the shared Redis read returns nothing, the check now falls back to this instance's in-memory count instead of admitting the turn with no count at all

* fix(router): keep io token reservation keys out of the metadata sent to providers

The itpm/otpm pre-call check wrote its reservation keys into both metadata and litellm_metadata. On the routes the router serves through its generic helper (Responses, Anthropic Messages, and the rest), metadata is the client's own field and goes to the provider as is. So _litellm_itpm_cache_key and _litellm_otpm_cache_key, which carry the deployment id and the model name, reached the provider, came back in the echoed response metadata, and counted toward the provider's 16-key cap. The managed Responses socket relay merged each turn's reservation into the frame's metadata the same way, and a native socket opened with metadata sent the keys on every turn through its request defaults

The router now passes the limiter the metadata field it uses for its own fields on that call (metadata for chat, litellm_metadata for the generic routes), and the reservation goes only there. metadata_channel is a required keyword, so a new caller has to name the field it writes. The managed relay puts each turn's reservation only on litellm_metadata

With the reservation only on litellm_metadata, a retried call on the generic routes refunded its first attempt twice and pushed the deployment's counters below zero. The failure hook read the reservation from the logging copy in litellm_params.metadata and cleared only that copy, so the router's litellm_metadata still held it and the retry refunded it again. The hooks now also read and clear litellm_params.litellm_metadata, which is the router's own dict

The proxy does not strip client keys that start with _litellm_ on the way in, so dropping that prefix at the provider boundary would also drop keys clients send today

The io token tests now run Redis in process with fakeredis instead of a TCP fake server

* test(responses): count socket turns through real logging callbacks instead of a Logging subclass

The native relay test now hands the rate limit check to a real Logging object as its success and failure callback, so the turn's tokens go through the same dispatch path production uses.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-30 07:48:32 -07:00
devin-ai-integration[bot]
0070ce23b3
fix(router)!: apply content_policy_fallbacks when router settings update at runtime (#45428)
* fix(router): apply content_policy_fallbacks when router settings update at runtime

* test(router): annotate the fallback regression test's locals as Final

* fix(proxy): validate router_settings.content_policy_fallbacks on /config/update like its siblings

* test(proxy): annotate the content_policy_fallbacks config update test locals as Final

* fix(proxy): keep litellm_settings content policy fallbacks, accept dict targets, skip invalid stored rules

A runtime content_policy_fallbacks rule now layers over the litellm_settings rules instead of replacing
them, so models the runtime rules do not name keep their file fallbacks. Fallback targets may be
deployment dicts as well as model names. A stored rule that fails validation is logged and skipped at
sync, so the other router settings still apply. Per-user token discovery now reads every fallback key
from key and team router_settings.

* fix(proxy): validate content policy fallback rules one by one and keep a config-owned list alone

An invalid stored rule is now skipped on its own instead of dropping every runtime rule, a router_settings list from the config file stays the router's only rules across DB syncs, and GET /fallback names the model of a dict target.

* fix(proxy): answer an error for a string-valued fallback rule on GET /fallback instead of its characters

* fix(proxy): keep config-file content-policy rules as Router init accepted them and log a skipped rule once per change

* fix(proxy): log a skipped content-policy rule whenever the saved rules change

* fix(proxy): keep general fallbacks when no content-policy rule is saved, name a model-less target's model, and take only model names in key rules

* fix(proxy): reject an empty dict target in a key's content-policy rule

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-30 04:27:11 -07:00
simonp9
d1758cda91
fix(router)!: keep a deployment's own model_id over the request body's (#49045)
* fix(router): keep a deployment's own model_id over the request body's

A request body model_id overrode the deployment's pinned model_id, so a client could move SageMaker chat and embedding calls to another inference component. The router now drops the request's model_id when the deployment sets one.

* fix(router): let deployments opt back in to a client model_id and stop client component headers on SageMaker chat

Drop the request's model_id after the router defaults are applied, so a default_litellm_params model_id cannot win
over the deployment either, and apply the same precedence on the sync text_completion path. A deployment that lists
model_id in configurable_clientside_auth_params keeps the client's value. SageMaker chat drops any case variant of a
client's X-Amzn-SageMaker-Inference-Component header when the deployment sets a component

* fix(router): keep a SageMaker deployment's component when it pins it through extra_headers

* fix(router): read the SageMaker component header as a pin only on SageMaker deployments

* fix(sagemaker): send the inference component only as a header, never as a body model_id

SageMaker picks the component from X-Amzn-SageMaker-Inference-Component, so the chat
container body and legacy parameters no longer carry model_id. Nova endpoints accept a
fixed body field list, so a pinned Nova deployment could reject the extra field.

* fix(health): probe header-pinned SageMaker deployments at their component

Health checks bypass the router, so a legacy sagemaker/ deployment pinned only
through its inference component header sent no component on /health while routed
requests now send it. Also update the two SageMaker integration tests that still
asserted the old precedence and the dropped body field.

* fix(health): apply the router's model_id pin rule to health probes

Health probes now share the router's pin function, so a deployment that lets
clients set model_id gets no header pin and its own model_id wins without
reading the header. Header keys that are not strings are skipped when
reading the inference component header instead of failing the call.

* fix(sagemaker): keep header keys that aren't strings and report a string opt-in in health checks

The chat transformation now keeps extra_headers keys that aren't strings, so such a deployment fails the way it did before instead of answering 500, and the model_id pin checks configurable_clientside_auth_params with isinstance, so a string there reaches the health probe's own error capture again.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-30 02:40:01 -07:00
paveln83
b91fc92b6d
fix(router): cap the default OTPM reservation and reconcile token counters after each call (#49054)
* fix(router): cap the default OTPM reservation for requests without max_tokens

* fix(router): reconcile ITPM and OTPM reservations after every successful call

* fix(router): read the default OTPM reservation from the served model and make its fraction configurable

The default reservation now looks up max output from litellm_params.model, as main does, so a pricing base_model never changes it. NO_MAX_TOKENS_TPM_RESERVATION_FRACTION sets the fraction for both the router check and the key and team TPM floor. A max_tokens that is not a number or string counts as no cap instead of raising inside the check.

* fix(router): refund a failed attempt's io token reservation in make_call

The proxy passes one logging object to every retry, and it runs failure
callbacks only for the first attempt, so the last attempt's ITPM and
OTPM reservation stayed held until the minute ended. Each attempt that
raises now gives its reservation back before the retry or fallback

* fix(router): count Anthropic cache writes once in ITPM reconcile

Native /v1/messages usage reports input_tokens without cache reads and writes, so the reconcile now maps it through AnthropicConfig.calculate_usage before subtracting cached tokens.

* fix(router): use a deployment max_tokens, charge late usage to its finish minute, and read the reservation fraction as a fraction

* fix(router): reserve with the deployment cap only through the param the request's API sends

* fix(router): never let a deployment's output cap raise the reservation above the default

* test(router): move the reservation fraction env var test to the integration suite

The unit test started a child interpreter, which the unit guide forbids. The integration test boots a real proxy with the env var set and checks admission against the held reservation.

* test(router): retry the reservation fraction scenario when the minute rolls over

The OTPM counter is keyed by minute, so a rollover between the held stream and the capped requests gave the capped request a fresh counter. The control request now leaves room for usage the first attempt left in the new minute.

* test(router): give the rollover retry its own deployment

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-29 20:32:04 -07:00
quietjules
d302096b5a
feat(lite): offer each model group's reasoning efforts in pi, OpenCode and Codex (#48949)
* feat(lite): offer each model group's reasoning efforts in pi, OpenCode and Codex

lite pi, lite opencode and lite codex now read supported_reasoning_efforts from /model_group/info and write them into each client's model config. pi gets reasoning and a thinkingLevelMap, OpenCode gets reasoning_options plus one variant per effort, and Codex fallback entries get supported_reasoning_levels with a default level. The proxy-served Codex catalog on /v1/models?client_version= offers the same levels

Codex before 0.138.0 fails the whole catalog on an effort it cannot name, so those releases, and an unreadable version, get only the six efforts every release parses. A group with unknown efforts keeps today's output, and a group with none is written as non-reasoning

The strength order and nearest_declared_reasoning_effort move to an SDK-free module so the lite CLI, which loads without initializing the SDK, can import the Codex catalog

* fix(lite): describe team models by their public name and keep Codex's none default

/model_group/info now looks up a team's own deployments when the caller's
team serves the requested name, so a team model reports its reasoning levels.
Codex fallback entries default to none when the group lists it, and the proxy
builds the Codex catalog in a worker thread

* fix(lite): round Codex's default level down and describe a team's shared-name model from its own deployments

Move the reasoning strength helper to litellm_core_utils so lite and the Codex catalog import it without the SDK or the router, and let make lint-format-check-changed diff the working tree so a staged rename passes.

* fix(lite): keep pi's system role on reasoning model groups

pi switches its system prompt to the developer role for any model marked reasoning, so an operator's
cache_control_injection_points entry for role system stopped matching on Claude groups. lite pi now sets
compat.supportsDeveloperRole false on those entries, so pi sends system as it did before.

* fix(codex): treat a client_version too long to parse as an old Codex and keep undescribed stock levels

A client_version past Python's integer digit limit made the Codex catalog answer 500. It now gets the
levels every Codex parses. A stock Codex level with no description validates again, as on base, and
lite codex treats undecodable codex --version output as an unknown version

* fix(lite): keep REASONING_EFFORT_STRENGTH_ORDER importable from router_utils and skip the Codex sync on undecodable debug models output

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-29 18:15:44 -07:00
Nate Armstrong
ead221995e
refactor(router): move router callers onto the entry points and invert the proxy imports (#48847)
Code outside the router now reaches it only through the entry points
(registry, settings, health, select, apply, observe, register_plugin, retire)
or the endpoint methods; the outside-member allowlist shrinks from 139 to 45.
litellm/router.py no longer imports litellm.proxy: it asks the installed
RouterHost (litellm/router_host.py) for vertex pass-through credentials,
member auto-router authorization and auto-router prompt compression, and the
proxy installs its host on import. Call sites where a model name can be
missing keep the old answer instead of reaching an entry point with None.
2026-10-29 17:26:33 +00:00
Nate Armstrong
a659e8be70
refactor(router): add router entry points and keep the old members as a deprecated mixin (#48840)
Router gains eight entry points (registry, settings, health, select, apply/aapply,
observe, register_plugin, retire) in litellm/router_api.py. The 123 public members
outside code used become DeprecatedRouterMembers (litellm/router_deprecated.py):
same signatures and results, a DeprecationWarning naming the replacement and a use
counter for callers outside the router. Router internals move to _core_ names;
subclass overrides of old members are still called by the router.
2026-10-29 17:02:41 +00:00
Mateo Wang
6cdb9959df
feat(router): opt in to non-chat deployment filtering (#46257)
* feat(proxy): reject chat routes on models declared non-chat via model_info.supported_endpoints

A model whose every deployment sets a non-chat model_info.mode and a supported_endpoints list with no chat endpoint now answers /v1/chat/completions, /v1/messages and /v1/responses with a 400 before any provider call. Models without both fields keep today's behavior.

* fix(proxy): let fallbacks answer and cover more chat routes in the non-chat model check

The check now steps aside when a request, key, team or router fallback would answer, skips such models in the rate-limit fallback loop, ignores mode and supported_endpoints copied from the cost map by old Admin UI saves, treats a blank mode as unset, and also covers /v1/completions, the Responses WebSocket, Gemini generateContent and /v1beta/interactions

* fix(proxy): only step aside for fallbacks that can answer chat, gate the Responses WebSocket after alias resolution, and compare declarations to the loaded catalog

* fix(proxy): gate the Responses WebSocket with the router after aliases resolve, and ignore client-set internal fallback flags

* fix(proxy): walk fallback groups breadth first and release reservations on a rejected WebSocket

Each fallback group is looked up once however often the request repeats it, and a WebSocket closed by the endpoint gate runs the failure hook so rate-limit reservations are released

* fix(proxy): walk fallback layers without recursion and gate the compact Responses route

The fallback walk now folds one layer at a time, so the recursion check passes, and /v1/responses/compact on a model declared non-chat gets the same 400 as the other chat-shaped routes

* fix(proxy): keep chat on rows whose cost map entry serves chat

A row that carries the cost map key of an entry that serves chat counts as declaring nothing, so a catalog edit that adds a chat path never turns old Admin UI copies into 400s

* fix(proxy): check chat sent to a deployment id or backend model name

* fix(proxy): check a model the read-through loads from the database

* fix(proxy): step aside for fallbacks the check cannot follow and wildcards that serve a backend name

A fallback target with no deployments of its own, a client routing stamp, and a wildcard that does not match the requested backend name no longer turn a working request into a 400

* fix(proxy): ignore a client model_group that is not a string when building rate-limit headers

A client metadata.model_group sent as an int or a list crashed the error handler with a 500 once the embedding-only check rejected the call before routing. The header helper now treats a non-string stamp as no model group. Drops the pattern clause in the backend-name lookup, since the group lookup already returns wildcard matches. Adds the third audit round's routing-stamp cells.

* fix(proxy): keep a non-string client model_group out of spend rows

* fix(proxy): validate standard fallbacks before the typed fallback-group lookup

* feat(router): opt in to non-chat deployment filtering

* fix(router): skip unresolved local rate-limit fallbacks

* fix(router): ignore caller-provided endpoint selector values

* fix(router): normalize declared chat endpoint prefixes

* test(router): cover preflight error-value behavior

* test(router): derive replay metadata defaults from model info

* fix(router): preserve fallback aliases and check synchronous text calls

* test(routing): rewrite the WebSocket model with a pre-call callback, since main no longer runs guardrails at socket admission

* test(routing): keep the endpoint gate's owned proxies on the worker's database

* fix(router): keep the endpoint gate on model_info patches that omit it and on same-group stream retries

A partial PATCH of model_info reset a stored enforce_supported_endpoints true back to false, because the
field defaulted to False and the update merges model_dump(exclude_none=True). The field now defaults to None
so an omitted flag keeps the stored value

The same-group retry of a stream dropped before content called the provider without the endpoint selection
context, so it could land on an enforced embedding-only deployment of the same group. It now runs inside
endpoint_selection like every other router attempt

* fix(router): probe rate-limit fallbacks through get_routable_deployments so the router surface does not grow

Main's router fence lets code outside the router use only listed Router members and
router_utils modules. The proxy's fallback probe now calls the listed
get_routable_deployments with the request's call type, and the gate's 400 moves to
litellm.exceptions as EndpointNotSupportedByModelError. It stays separate from main's
UnsupportedEndpointError, which the router retries on the group's other deployments,
while the gate refuses the whole group

* test(responses): cover the WebSocket error frame for a model that refuses the endpoint

* fix(proxy): let the rate-limit fallback probe try a fallback this worker has not loaded yet

The probe now skips a fallback only when the endpoint check removed every deployment it has, so a model added on another worker still reaches route_request and its database lookup

* fix(proxy): keep key router settings off the Responses WebSocket, read unrooted or single-string endpoint lists, and add status to the refusal frame

* fix(router): warn when enforce_supported_endpoints has no usable list and match chat endpoints regardless of case, trailing slash, or Gemini path

* test(router): type the load-warning test's caplog and router

* test(routing): match the endpoint gate's integration tests to base fallback behavior on unloaded fallbacks and the Responses WebSocket

---------

Co-authored-by: Mateo <mateo@Mateos-MBP-2.localdomain>
2026-10-29 08:33:36 -07:00
Otis Cui
c13ecb3085
fix(exceptions)!: honor status code when mapping OpenAI invalid_request_error exceptions (#32537)
* fix: openai exception mapper ignored status code for invalid_request_error

_map_openai_exception coerced anything whose body carried type
invalid_request_error into BadRequestError, guarded only by excluding the single
phrase Incorrect API key provided, and this ran before the status-code checks.
OpenAI and openai-compatible providers return 401 and 404 bodies with type
invalid_request_error, so an auth failure like Invalid API key mapped to a 400
instead of AuthenticationError, and a 404 mapped to BadRequestError instead of
NotFoundError. That changes the status returned to proxy clients and router
retry, cooldown, and fallback handling. Mirror the Azure mapper by only coercing
to BadRequestError when status_code is None or 400, so 401 and 404 fall through
to their existing handlers.

* fix(exceptions): map OpenAI-family invalid_request_error 404s to NotFoundError

Move the contributor's test to the mapped unit test file, assert the raised
class, status code and message, and cover the 400 and status-less cases that
must still raise BadRequestError

Add a regression test for the flow the bug was found on. Retrieving a missing
OpenAI vector store, sync or async, now raises NotFoundError with status 404

The router test that pinned an OpenAI 404 typed invalid_request_error as
BadRequestError now expects NotFoundError

* fix(a2a): answer a provider NotFoundError as JSON-RPC invalid params

* fix(exceptions)!: limit the invalid_request_error status guard to 401 and 404

Every other status keeps raising BadRequestError as on main, so 402-415, 429 and 5xx bodies typed invalid_request_error keep their class and router retry behavior. A2A goes back to main's mapping, which answers NotFoundError and AuthenticationError as JSON-RPC internal errors.

* fix(exceptions): keep the provider error fields on mapped 401 and 404 invalid_request_errors

Raise AuthenticationError and NotFoundError straight from the invalid_request_error
branch with the provider body, so code, param and type survive and the proxy's
error.type matches main. The Rust mapper copy skips 401 and 404 the same way.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-29 08:05:47 -07:00
Mateo Wang
65d912fc2e
feat(responses): support Codex client tool_search on Anthropic and other bridged providers (#48972)
* feat(responses): support Codex client tool_search on Anthropic and other bridged providers

* test(router): cover get_search_tool_support provider defaults and model_info overrides

* fix(responses): keep tools loaded by earlier Codex tool searches across previous_response_id

* perf(responses): read stored turns once on client tool search continuations

* fix(router): avoid OAuth refreshes when listing search capabilities

* test(cli): expect disabled search on fallback Codex models
2026-10-29 00:29:40 -07:00
rj-cairn
5eb1042bbf
fix(token_counter): count OpenAI models newer than tiktoken's table with o200k_base (#48728)
* fix(token_counter): count OpenAI models newer than tiktoken's table with o200k_base

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

* fix(token_counter): keep tiktoken's encoding for suffixed variants of models it knows

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

* test(token_counter): move the o200k provenance note into the assert message

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

* fix(token_counter): strip openai/ and azure/ prefixes before picking the encoding

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

* test(rust_bridge): azure/gpt-4o now resolves to o200k_base

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

* fix(token_counter): check the provider prefix through _fix_model_name to keep the type gate flat

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

* test(token_counter): check o200k on the gpt-6 models tiktoken 0.14 still does not know

* fix(token_counter): count Azure deployment names and OpenAI alias groups with their OpenAI tokenizer

* fix(token_counter): count mixed, alias, and base-model deployments with every served model's tokenizer

A group that serves OpenAI and non-OpenAI models now reserves at least each served model's count, model_group_alias names count as their target group, custom Azure deployment names count with their base model, and azure_ai/, chatgpt/ and similar prefixes resolve to the listed OpenAI model.

* fix(token_counter): keep mixed groups on OpenAI tokenizers, cache served models, and count context checks, failed rows and stream fallback with the served model

* fix(token-counter): keep mixed groups at their old reservation and count new OpenAI release names with o200k

A group that serves an OpenAI model next to another vendor now counts its own name the way tiktoken reads it as written, plus its OpenAI models, so it reserves what main did instead of the lower o200k count

base_model picks the tokenizer for estimated usage only when it names an OpenAI model, now also in the Cursor stream counter and the output-limit response

Unlisted gpt-5+ and o-series names count with o200k_base, and wildcard routes run the context-window check with the matched OpenAI model

* fix(token-counter): count an alias to a group no OpenAI model serves by the alias as written

Keeps the count main gives such an alias, so its budget reservation and rate limit match main and no other vendor's tokenizer loads for it. Also sorts the import the main merge left unsorted.

* test: type the new token-counting tests' parameters

* fix(token-counter): count a release-named group no OpenAI model serves as main does, and snapshot the OpenAI model set before iterating it

* fix(token-counter): read served models through the router's allowlisted deployments_named, and coordinate the registration race test

* perf(token-counter): classify each distinct served model once when walking a model group

Budgeted requests walk every deployment in their model group, since the
router's cached copy is outside the router surface the proxy may use.
Classifying each distinct served model once instead of once per
deployment cuts a 200-deployment group from 2.3-4.5 ms to about 1.4 ms
with the same result.

---------

Co-authored-by: mateo <mateo@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-29 00:08:12 -07:00
Kerry Lu
56e60d3b7d
feat(router): daily_reset_time picks when deployment rpd and tpd daily limits start over (#48957)
* feat(router): reset_time picks when deployment rpd and tpd daily limits start over

reset_time is "HH:MM" in UTC next to rpd/tpd in litellm_params or model_info. The daily cache keys are dated by now minus that offset and the daily 429 retry-after counts to the next reset, so a free tier whose provider quota resets at 08:00 UTC comes back at 08:00 instead of midnight. Without it the window stays the UTC calendar day.

* chore(ui): regenerate schema.d.ts with reset_time

* refactor(router): rpd, tpd and reset_time are read from litellm_params only

* feat(router): rename reset_time to daily_reset_time, drop the request-sent value, accept only HH:MM

* chore(ui): regenerate schema.d.ts for daily_reset_time

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-28 20:20:01 -07:00
Nate Armstrong
7650f766b0
refactor(router): fence the router surface and document the router-core boundary (#48839)
Add rustdoc overviews for a new litellm-router-core crate and for
litellm-model-catalog, a module docstring on litellm/router.py, and a
shrink-only allowlist of the Router members and router_utils modules used
outside the router, checked in CI by router_surface_check.py.

Move the router-free helpers out of litellm/router_utils: batch_utils to
litellm_core_utils, and the auto-router naming contract, tuning baseline
and baseline request next to the auto-router plugin. No behaviour change.
2026-10-29 00:01:52 +00:00
Kerry Lu
96fe78f96e
fix(router): reject string fallback targets at Router init (LIT-10238) (#48943)
* fix(router): reject string fallback targets at Router init (LIT-10238)

* fix(router): keep direct target fallback entries when rejecting string targets

* test(router): set string fallbacks after init in cooldown tolerance tests

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-28 16:32:45 -07:00
Kerry Lu
0f786fa1c7
feat(router): add deployment rpd and tpd daily limits to enforce_model_rate_limits (#48875)
* feat(router): add deployment rpd and tpd daily limits to enforce_model_rate_limits

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

* fix(router): route rate limit cache increments through typed helpers

* chore(ui): regenerate schema.d.ts for deployment rpd and tpd fields

* test(types): add rpd and tpd to the owned litellm_params inventory

* fix(router): run daily rpd/tpd checks after the minute checks and increment rpm and rpd in one cache trip

* fix(router): flatten the async rpm reject branch for the ruff strict gate

* fix(router): hand rpm and rpd back on any rate limit reject

A request the rpm, rpd or tpd check rejects no longer keeps its request counts, so retries against a deployment whose rpd is spent cannot fill its rpm and block the in-group fallover with the minute 429.

* test(router): a minute-rejected request no longer keeps its rpm count

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-28 15:52:44 -07:00
Mateo Wang
567383c503
feat(proxy)!: model family and access group entries in model_max_budget (#47722)
* feat(ui): let the per-model budget field take a model pattern or access group

* feat(proxy): let model_max_budget entries name a model family or access group, shared across replicas

* fix(proxy): open each shared model budget window once for every replica, and match patterns in linear time

Without Redis, the first replica to charge an entry in a new window now opens it in the window spend table and
the others join it, so no flush replaces spend another replica already added. Budget patterns match by splitting
on `*` instead of a regular expression, a refused request names the entry that refused it, and the key form no
longer offers a typed pattern another row already budgets.

* fix(proxy): read the shared budget window from the writer, and keep concurrent first charges on one replica

Opening a shared window now reads the row from the writer, since a lagging read replica could hand a replica its
own start and let a flush replace the others' spend. A charge that waited on the open no longer resets a local
window another charge on the same replica opened meanwhile. Window tests use fixed timestamps.

* fix(proxy): stop a non proxy admin from lifting a team model cap with a looser group or pattern entry

Group and pattern entries now take precedence over other entries, so a team update by anyone but a proxy admin
must keep each new or changed entry within every existing cap it can take precedence over. A pattern can match a
provider-prefixed or wildcard-routed name, so any two patterns count as overlapping, and a model or group entry
counts as outranking every pattern.

* fix(proxy): narrow model budget pattern overlap, keep team caps under key patterns, and accept only typed patterns in the UI

* fix(proxy): keep model family budgets through database blips, team models and key entries

A failed window open no longer restarts the window at this replica's clock, so it cannot wipe shared spend. Spend charged meanwhile stays local and joins the shared window once the database answers

A key entry named after a model replaces only the team entry of that name, so team pattern and group caps still apply. Team-scoped models are gated and charged under their public name, and group lookups see team deployments

The team update guard compares caps by window length and only ranks patterns that can match the same name, including Bedrock vendor patterns behind a bedrock wildcard route

* fix(proxy): refuse team entries that take precedence over a capped model_max_budget entry

A non-proxy-admin could add a carve-out (an exact model, a narrower pattern or
a group) at or under the family cap, and its own counter then multiplied the
team's spend on that family. Any new entry that takes precedence over a capped
entry now needs a proxy admin, and group members include the team's own public
model names so team deployments cannot hide a group overlap.

* test(router): cover team_public_model_names_of

* docs(proxy): name pattern and access group keys in model_max_budget docstrings

* fix(proxy): name the entry holding the cap in the team model budget raise refusal

* fix(proxy): check precedence when a team edit makes an unenforced model_max_budget entry take effect

* fix(proxy): let an org admin give a legacy model_max_budget row a budget_duration at the same or a lower cap

* fix(proxy): keep a legacy model_max_budget row's stored cap when an org admin gives it a budget_duration
2026-10-28 15:40:57 -07:00
Mateo Wang
dfd9ad4103
fix(responses)!: bill partial usage on mid-stream failures and report bridged /v1/messages failures to the proxy (#47537)
A Responses API stream that fails partway now bills the usage it already consumed, ends with an error instead of a clean stop, and honors stream_timeout between chunks. Bridged /v1/messages calls report those failures to the proxy's failure hooks, so spend, budgets and logs see them
2026-10-28 13:32:09 -07:00
tin-berri
ca6c830221
fix(proxy): account for media in combined TPM reservations (#48865) 2026-10-28 12:39:41 -07:00
shrey
a45c2023ce
fix(audio): return 400 for unsupported transcription providers (#47971) 2026-10-28 19:20:17 +00:00
simonp9
78d6c1aa82
fix(router): pass lifecycle events through on background OpenAI responses streams that cannot fall back (#48817)
* fix(router): pass lifecycle events through on background OpenAI responses streams that cannot fall back

The Router held response.created, response.queued and response.in_progress until the first output event on every /v1/responses stream, so a background run showed the client no response id while it was queued or reasoning. A native OpenAI stream whose upstream echoes background: true now releases those events as they arrive when no fallback, order level or weighted failover could replace it, and once the id is out a later pre-output error is raised instead of starting a second run behind it.

* test(router): type background stream test params and wait on the upstream pause instead of a clock

* fix(router): keep the content-policy note on a released background stream error

* fix(responses): return the routed id from cancel and add the router note only before output

acancel_responses and cancel_responses now re-encode the reply id with the deployment, like get. The released stream's content-policy note follows the held path, which adds it only before output text. Renames _anthropic_messages_order_levels to _stream_order_levels.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 12:15:02 -07:00
Kerry Lu
ca4a2ba006
fix(router): drop the deployment rpm filter from enable_pre_call_checks (#48873)
enable_pre_call_checks filtered deployments whose in-memory minute count
reached litellm_params.rpm, undocumented since 0d1cca9aa0. Deployment rpm
and tpm stay routing weights unless optional_pre_call_checks includes
enforce_model_rate_limits, which is now the only deployment rate-limit path.

Resolves LIT-10233

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-28 12:13:56 -07:00
tin-berri
8e2e99670d
fix(router): continue Responses mid-stream fallbacks on models without assistant prefill (#47672)
* fix(router): avoid assistant prefill in Responses fallbacks

* fix(router): keep prefill for Claude fallbacks that take it and skip constrained continuations

A Responses fallback to a Claude model that takes assistant prefill now gets
the partial response as prefill again, so it continues mid-word without the
prompt-shape drift. Every other fallback keeps the request to continue.

A stream cut after text no longer continues when the request constrains its
output (a json_schema or json_object text format, or a forced tool_choice),
since a continued JSON document or tool call would not parse. The client gets
the primary's error, as on the chat path.

The fallback's re-sent leading space at the seam is dropped, and the prefill
shaping runs per deployment after the encrypted reasoning strip, so stripped
reasoning is never replayed.

* fix(router): guard SDK text_format, narrow forced tool_choice, read Claude base_model, and cap Responses continuation output

Cut Responses streams that asked for structured output through the SDK's text_format now surface the primary's
error instead of stitching two models' JSON. Only a required or named tool_choice counts as forced, so allowed_tools
in auto mode and hosted tool choices still continue. A Claude deployment named only through base_model keeps its
prefill continuation, and the continuation's max_output_tokens is lowered by what the cut stream already emitted.

* fix(router): quote the partial's ending in the continue request and surface the primary's error after a cut tool call

* fix(router): stop Responses fallbacks from continuing content-filter cuts and client-run tool calls

A content filter that cuts a /v1/responses stream mid-answer now returns its error, as on chat, instead of letting a content_policy_fallbacks backup finish the filtered text. Shell, apply_patch, computer, local_shell, and MCP approval items count as a started tool call. Chat continuation goes back to treating any tool_choice object as constraining, as on main

* fix(responses): count a stream of tool arguments as a started tool call

* fix(router): let Responses fallbacks continue under allowed_tools auto and after server-run tool calls

A Responses tool_choice of allowed_tools in auto mode no longer blocks the continuation, since the model may still answer in text. Hosted MCP calls and the bridge's server-run tool search no longer count as a started client tool call.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 04:34:40 -07:00
Aidan Singh
b97fcf1af0
fix(router): let affinity pins yield to deployments that already failed this request (#32309)
* fix(router): apply weighted-failover exclusion before deployment-affinity callback

* fix(router): let affinity pins yield to deployments that already failed this request

Moving the weighted failover exclusion ahead of the affinity check also changed what tag, order, plugin and managed-file filters saw. Restore the original filter order and have the affinity check ignore session and API key pins that point at a deployment already excluded or retry-skipped in this request. previous_response_id pins still win.

* fix(router): release affinity pins on deployments that failed this request

Delete a session or API key pin whose deployment already failed the request, so
the deployment that serves it claims the pin. Prompt caching pins skip failed
deployments too, and the proxy strips the internal failed-id fields from client
bodies.

* fix(router): keep affinity pins in place and skip failed origins in encrypted content affinity

A pin now only yields for the request whose pinned deployment failed. Deleting it moved pins after one 429 or a request-caused 400 and added a Redis call on the routing path. Encrypted content affinity also ignores deployments that failed this request, so a failed origin takes the strip-and-dispatch path.

* fix(router): keep encrypted reasoning when a retry can only go back to its origin

The encrypted content check now drops failed deployments the way the router does: a weighted failover exclusion always holds, and a retry skip that would leave nothing falls back to the whole group. The proxy also drops client-sent metadata._failover_excluded_ids, and the encrypted content tests use a real Router

* fix(router): keep encrypted reasoning when previous_response_id binds the failed origin

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 03:24:26 -07:00
Kerry Lu
8f31c7171d
fix(router): keep a lone deployment warm under router allowed_fails unless the policy names the exception (#48644)
* fix(router): keep a lone deployment warm under router allowed_fails unless the policy names the exception

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

* test(router): give cooldown fixtures a sibling deployment so allowed_fails=0 still forces a cooldown

* fix(router): keep benching a lone deployment that has a fallback group or an empty deployment policy

* fix(router): accept dictionary fallback targets in the lone deployment check

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

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 15:12:53 -07:00
Mateo Wang
14193435b8
refactor(anthropic): rename llms/anthropic/pass_through to llms/anthropic/messages (#48510)
* refactor(anthropic): rename llms/anthropic/pass_through to llms/anthropic/messages

Move the unified /v1/messages code out of a directory called pass_through, since it is not the
passthrough feature. The inner messages sub-package becomes native, and the Vertex AI partner
model experimental_pass_through package becomes messages. The old packages stay as import shims
that warn once and alias every submodule in sys.modules, so old deep imports still resolve

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

* refactor(anthropic): keep Final variables out of loops in the rename shim

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

* test(anthropic): match the warning text in the rename shim test

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

* refactor(anthropic): resolve old pass_through imports lazily through a meta path finder

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

* refactor(anthropic): mark the alias loader spec restore for the type discipline gate

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

* style(anthropic): format the renamed package shim

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

* style(anthropic): keep the rebind-ok marker on the spec restore line

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

* refactor(anthropic): carry the new package search path on alias specs

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

* test(anthropic): declare the shim test locals Final

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 10:08:25 -07:00
devin-ai-integration[bot]
76d001a7c0
fix(router): serve a team's wildcard deployment before a proxy-wide wildcard (#48490)
* fix(router): serve a team's wildcard deployment before a proxy-wide wildcard

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

* fix(router): keep the wildcard precedence helpers free of partially typed locals

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

* fix(router): call each pattern router once in the wildcard precedence helpers

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

* test(ptu): a team's wildcard reservation serves beside a proxy-wide catch-all

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

---------

Co-authored-by: joshua <joshua@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-26 17:06:09 -07:00
moe-berri
7fd8740ad5
fix(router): isolate forwarded Anthropic subscription cooldowns (#48485)
* fix: isolate forwarded Anthropic subscription cooldowns

* test: fix the clock for Anthropic cooldown regression
2026-10-26 16:54:38 -07:00
devin-ai-integration[bot]
2e27a02a9f
feat(chatgpt): forward client subscriptions through the gateway (#41522)
* feat(chatgpt): forward each Codex user's ChatGPT OAuth login through the proxy

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

* test(router): cover _model_group_llm_provider metadata resolution

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

* fix(chatgpt): keep the gateway ChatGPT login bound to the authenticator api_base

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

* refactor(chatgpt): move client credential header selection into the chatgpt provider

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

* test(chatgpt): cover credential recognition and stored account id fallbacks

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

* feat(chatgpt): support per-model chatgpt_auth_file for multiple ChatGPT accounts

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

* fix(chatgpt): resolve bare chatgpt_auth_file paths and drop redundant test docstring

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

* fix(chatgpt): pass per-model auth file to early provider detection

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

* fix(chatgpt): normalize chatgpt_auth_file before caching the authenticator

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

* fix(chatgpt): type account selection parameters

* fix(chatgpt): preserve account selection through saved prompts

* fix(chatgpt): allow administrator account configuration through the proxy

* test(chatgpt): construct immutable request parameters

* fix(router): preserve typed deployment credential resolution

* fix(chatgpt): allow admins to probe unsaved account configurations

* fix(chatgpt): retain SDK login headers during provider resolution

* fix(chatgpt): allow admins to probe unsaved account configurations

* fix(chatgpt): normalize subscription health probe input

* fix(chatgpt): normalize health probe input and cover credential destinations

* test(chatgpt): keep destination rejection assertions focused

* feat(proxy): forward client ChatGPT subscriptions on native Codex routes

* docs: lead ChatGPT forwarding with standard model setup

* fix(chatgpt): preserve selected credentials in health provider lookup

* fix(chatgpt): preserve gateway credentials and workspace selection

* refactor(chatgpt): avoid rebinding streaming metadata inputs

* fix(chatgpt): keep account lookup token argument immutable

* fix(chatgpt): scope forwarded credential routing to ChatGPT

* refactor: keep forwarded credential isolation opt-in by provider

---------

Co-authored-by: Moe Khalil <moe@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Urbanonymous <ugutitej@gmail.com>
Co-authored-by: mateo <mateo@berri.ai>
2026-10-26 15:44:05 -07:00
yucheng-berri
7045139c3a
fix(router): stop concurrent deployment deletes from crashing deployment lookups (#47680)
* fix(router): stop concurrent deployment deletes from crashing deployment lookups

delete_deployment and upsert_deployment shrink model_list before they repair
the index maps, so a lookup on another thread in that window read a stale
index: IndexError, or the neighbouring deployment and its credentials. An add
racing a delete could corrupt the index for good.

One RLock now covers model_list and the three index maps. Delete, upsert, add
and the set_model_list reset change them under it, and every lookup that turns
an index into a deployment reads under it.

Fixes #45242

* test(router): release the held delete when the read reaches the index lock

The race tests released the held write after a fixed 0.2 s, so a reader the runner
stalled past it read the repaired list and passed on unfixed code. The read now runs
on its own thread and the write stays held until the read returns or reaches the
router's index lock; the 10 s bound only fails a broken synchronization.

The integration test gains the audit cells: chat, streaming, Responses and
Anthropic messages calls (sync and async, by model name and team public name)
during a held delete, the same for upsert, add and set_model_list, a request for
the deployment being deleted, a provider error, a double delete, and thread and
async bursts during upsert churn.

* test(router): cover the early return of a delete and the team filter of get_model_ids

delete_deployment now returns None from inside the index lock, and get_model_ids by
name reads its deployments through _deployments_named; pin both: deleting an unknown
id returns None and leaves the list alone, and exclude_team_models drops only the
team deployment.

* fix(router): read deployments by name under the index lock outside the Router

The response polling provider check, the complexity router's adaptive pool and the
savings baseline turned the name index into list positions themselves, so a concurrent
delete could still hand them a stale slot. They now read through Router.deployments_named,
which takes the deployment index lock.

* fix(router): type the locked deployment lookups as DeploymentTypedDict

deployments_named and _team_deployments_named returned tuple[Any, ...]. They now return
DeploymentTypedDict rows, which is what model_list holds. The adaptive pool setup in the
Router and the complexity router drop their branches for pydantic Deployment rows, which
these lookups never return, and should_include_deployment accepts the typed row.

---------

Co-authored-by: yucheng-berri <293926326+yucheng-berri@users.noreply.github.com>
2026-10-26 15:40:19 -07:00
devin-ai-integration[bot]
34ec3fcdfa
fix(router): skip declared text-only deployments for image requests on every routing strategy (#48241)
* fix(router): skip declared text-only deployments for image requests without enable_pre_call_checks

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

* fix(router): run the vision filter after the cooldown filter so a cooled vision leg falls back to the text leg

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

* fix(router): apply the vision filter after pre-call checks and tidy the new tests

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

* test(router): mark the remaining locals in the vision filter tests Final

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

---------

Co-authored-by: joshua <joshua@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-24 16:11:33 -07:00
devin-ai-integration[bot]
4710c3b30e
fix(router): route decisions calls only to deployments whose provider supports decisions (#48175)
* fix(router): route decisions calls only to deployments whose provider supports decisions

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

* test(router): account for indirect route helper coverage

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

* test(router): pin the chat leg so the mixed-group check is deterministic

Weights pick the Bedrock leg for chat instead of relying on simple-shuffle landing on both legs. The route filter now passes the candidate list through unchanged in type, so the pre-existing selector lines lose their pyright ignores

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

---------

Co-authored-by: joshua <joshua@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-24 11:34:32 -07:00
devin-ai-integration[bot]
f29e10c0ef
fix(router): keep image requests off deployments declared supports_vision false (#48152)
* fix(router): keep image requests off deployments declared supports_vision false in pre-call checks

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

* fix(router): type the vision pre-call helpers so the basedpyright gate holds

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

* fix(router): keep the pre-call index set typed without a mutable annotation

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

* test(router): cover the vision pre-call helpers directly for the router coverage check

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

* test(router): type the vision pre-call test helpers

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

* test(router): cover the no-params and unresolvable-provider vision branches

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

---------

Co-authored-by: joshua <joshua@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-23 22:52:16 -07:00
Mateo Wang
a8113b0dd7
fix(responses)!: round-trip Anthropic compaction blocks as compaction items (#46754)
* fix(responses): round-trip Anthropic compaction blocks as compaction items

/v1/responses on Anthropic routes now returns Claude's compaction block as a
compaction output item (and streams it as output_item.added/done), and a
compaction input item is replayed to Claude as its native compaction block,
so the next turn reuses the summary instead of compacting again. Other routes
receive the summary as assistant text. The Anthropic stream handler now keeps
the summary text that arrives in compaction_delta.

Fixes #41456

* fix(responses): replay compaction blocks natively only when the request compacts

* fix(responses): carry compaction through dict-form edits, SDK replay, affinity, and guardrails

Read Anthropic's dict-form context_management, accept SDK ResponseCompactionItem
objects on replay, strip the encrypted_content affinity wrap before decoding,
scan and rewrite compaction summaries in Responses guardrails, drop the history
a summary replay replaces, send the summary as a user message so thinking
ordering holds, and send the streamed compaction .done with the newest block.

* fix(responses): count, route, guard, and redact compaction items consistently

A compaction item now reads as its summary everywhere the proxy counts or routes
a Responses request: pre-call context checks, the TPM limiter, the input token
endpoint, the stream usage estimate, and the previous_response_id session rebuild
all drop the history the item replaces. A Claude compaction item sent to a
non-Anthropic model (including a fallback) becomes a user summary message instead
of an encrypted blob the provider rejects, and Claude on Bedrock Claude Platform
takes the summary path. Output guardrails see the summary of a returned
compaction item, and logging redaction hides it.

* fix(responses): start previous_response_id replay at a stored compaction and count reservations from it

* fix(responses): keep a stored compaction summary whole in spend logs

* fix(responses): keep stored compaction items summary-only, redact compaction_delta, and finish compaction before reasoning

A spend-log row now stores a top-level compaction input item as its summary alone, so extra keys in a
client-built item no longer reach the database whole. Message redaction also nulls the streamed
compaction_delta, and the streaming bridge sends the compaction item's done event before the reasoning
item that follows it. Adds the audit's integration tests for compaction items on the wire.

* test(responses): cover stored compaction items, compaction_delta redaction, and compaction done order

Adds integration cells for the summary-only spend row of a caller-sent compaction item and its previous_response_id replay, a redacted chat stream row that holds no streamed compaction_delta, and a compaction then thinking stream whose done events follow output order.

* fix(spend_logs): honor the redaction header on failure rows for litellm_metadata routes

A failed /v1/responses or /v1/messages call keeps its proxy request headers in
litellm_metadata, but the failure row read redaction headers from metadata only,
so x-litellm-enable-message-redaction never redacted those rows.

* fix(spend_logs): lift only the redaction opt-in header into failure rows

The failure hook on litellm_metadata routes copied every real request header into the caller's own metadata dict, so the stored request body gained the headers, a caller's metadata.headers was replaced, and a disable header loosened global redaction on failed calls. It now copies only the enable headers into a new litellm_params metadata dict, and only when the caller sent no metadata.headers, so no failure row is less redacted than on main

* test(responses): cover failure-row redaction headers and caller metadata on litellm_metadata routes

* fix(responses): keep earlier-turn instructions after compaction and count compaction items by their summary

A previous_response_id rebuild that starts at a compaction now keeps the system and developer
messages of every earlier turn. Token estimates for raw Responses input count a compaction item
as its summary text instead of the serialized item.

* fix(responses): drop the stored history a caller-sent compaction item replaces on previous_response_id

* fix(responses): count only message items with a string role as compaction instructions

* test(responses): flatten the last two-for comprehension in the compaction wire test

* fix(responses): replay from a compaction in stored Responses output and SDK compaction items

A previous_response_id whose stored row is a Responses output holding a compaction item now starts its replay at that compaction, and the handler dumps SDK compaction objects before checking for a summary. Adds tests for the uncovered compaction paths.

* fix(responses): keep the stored tool call a function_call_output answers when a compaction item replaces history

* fix(responses): keep a stored tool call on every turn after a compaction item and bound the stored summary

A previous_response_id rebuild that starts at a turn holding a compaction item now sends again each stored call a
replayed tool output answers, so a retry or a later turn no longer loses the turn-1 call. The start is picked only from
rows up to the referenced response. A spend row keeps a compaction summary up to the larger of 64K chars and
MAX_STRING_LENGTH_PROMPT_IN_DB instead of with no bound.

* fix(responses): replay a compacted session only through the referenced response

A later sibling turn that sent a compaction item put its summary between a stored tool call and its tool output, so Claude answered 400 on a turn that continued the first of two retried turns, and on a retry sent without the item

* fix(spend-logs): keep main's stored value for a prompt length limit under 2

At a MAX_STRING_LENGTH_PROMPT_IN_DB of 0 or 1 the shared truncation helper stored only the truncation marker, where
main stores the marker followed by the whole value

* fix(spend-logs): keep main's stored tail for a negative prompt length limit

At a negative MAX_STRING_LENGTH_PROMPT_IN_DB the shared truncation helper stored one more character of each value than main, since it dropped main's clamp of the end slice to the limit minus the start

* fix(responses): keep a compaction item when a guardrail rewrites its summary through structured_messages

* test(responses): run the compaction wire tests on the worker database under isolated workers

* fix(router): keep the encrypted content affinity pin out of provider requests

* fix(proxy): inspect compaction summaries in guardrails, strip a client-sent affinity pin, and price failures from the newest compaction

* fix(proxy): count a failed chat or /v1/messages call on its whole history, not from a compaction-typed message

* fix(proxy): trim a failed call's input at the newest compaction only when the client called /v1/responses

* fix(proxy): trim a failed call's input at the newest compaction when the proxy dispatched it as a Responses call

* test(proxy): run the dispatched-call-type failure test through the real request setup

* fix(proxy): count a failed Cursor stream's input from the newest compaction item

The Cursor route maps to no call type, so the stream generator's failure hook fell
back to the route and counted the whole history. Pass the processor's route_type.

* fix(proxy): count a broken Cursor stream's prompt from the newest compaction item

* fix(responses): keep a tool call a later output answers when trimming before a compaction item

* fix(responses): move a compaction-split tool call's turn to right before its output on both compaction routes

* perf(responses): look up moved turn items in a set so a long compaction-split turn trims in linear time

* test(responses): measure the compaction-split turn move as a CodSpeed benchmark instead of a clock read in unit tests

* fix(responses): move only calls still open at the compaction, and bill a disconnected stream from the newest compaction item

* fix(responses): restore only the newest stored call when a reused call_id is answered after a compaction item

* fix(spend logs): bound a Responses compaction item's summary like a chat block instead of cutting its encrypted_content

* test(proxy): price a failed dispatched call through a real ProxyLogging instead of mocked hooks

* fix(responses): finish a stream whose Claude compaction failed and group answered call ids by turn once

* fix(responses): count input_tokens after a compaction item the way inference sends it
2026-10-21 08:50:19 +00:00
Mateo Wang
820725661e
fix(router): log each dropped /v1/messages attempt through its own logging copy and open the retry without its usage (#45194)
* fix(proxy): keep a retried /v1/messages stream's spend log by logging the superseded attempt as a failure

A /v1/messages stream the router retries before any content shares one logging object across both attempts. The superseded attempt's end-of-stream logging ran as a success (a usage-only or placeholder result) and claimed the request's one-shot success log, so the recovered stream's spend row was either missing or written under the superseded attempt's message id. The passthrough logging path now logs an Anthropic stream whose frames carry an `event: error` as a failure with whatever usage the provider reported, the way a dropped stream already does, and the router closes the superseded stream before it opens the retry so that logging is queued first.

* fix(proxy): log a provider error frame through an attempt-scoped logging copy

The error-frame failure stashed the superseded attempt's usage on the logging
object the retry shares, so the retry's success log carried the failed
attempt's token counts. The failure now logs through a copy of the logging
object with its own model_call_details, and the helper moves into the
Anthropic passthrough logging handler so the proxy streaming handler only
routes by endpoint type.

* fix(proxy): scope the error-frame failure log to streams closed at the frame

A stream that ran to its end at the provider's error frame kept logging
through the success route before this PR, and the attempt-scoped failure
log is now taken only for a stream the router closed at that frame, with
the copy made at the close so the retry's deployment never reaches it

* fix(router): log each dropped /v1/messages attempt through its own logging copy and open the retry without its usage

A /v1/messages stream dropped after message_start and retried in its model group shares one Logging object
across attempts, so the retry's success log inherited the dropped attempt's stashed usage and exception,
and a second drop never reached the failure handlers because the first had already marked the object as
logged. The dropped attempt now dispatches its failure handlers through an attempt-scoped copy, and the
router discards the stashed usage right before it opens the next attempt, on same-group retries and on
every fallback hop, so a request that runs out of attempts still bills the last attempt's partial usage.

* fix(router): dispatch chat stream failures through the attempt-scoped logging copy

The router now clears a dropped attempt's stashed usage before every fallback hop, so the chat stream wrapper's queued failure log must read that usage from its own copy of the logging object, like the /v1/messages path already does. The stash test asserts through a real success log, the cooldown test reads a real Router's cooldown list, and the integration helpers carry full types.

* test(vertex): give the stream wrapper's logging stub an attempt_scoped_copy

* test: route the pass-through logging mock's attempt copy to itself and read the callback sink through a reader

The pass-through failure test stubbed dispatch_failure_handlers on a spec'd MagicMock, which the attempt-scoped copy no longer reached, so the logging worker never finished. The integration tests kept earlier sink batches in a caller-owned list the helper mutated; a small reader keeps them instead.

* test(router): use real Logging in fallback cooldown test

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

* fix(router): preserve superseded retry usage for billing

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

* fix(proxy): bill retry input after prior stream usage

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

* fix(proxy): track retry dispatch for terminal billing

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

* test(router): use deterministic retry timestamp

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

* test(router): avoid real clock in fallback tests

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

* fix(tests): remove duplicate streaming logger import

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

* fix(proxy): retain error-frame usage for exhausted retries

* test(proxy): fix retry billing expectations and helper annotations

* fix(proxy): preserve failure estimate without carried usage

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

* refactor(proxy): audit retry helpers and fixtures

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

* test(proxy): fix retry fixture timestamps

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

* test(proxy): observe retry stash at attempt boundary

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

* fix(logging): keep terminal failure attribution when dropped attempts log on copies

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

* fix(logging): publish a dropped attempt's payload only while it is still the current attempt

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

* test(proxy): cover spend-log encoding error propagation

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

* fix(anthropic): skip adapter failure dispatch when the wrapped stream already logged it

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

* fix(router): cool down the final fallback attempt when its failure callbacks were blocked

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

* test(anthropic): import Final in adapter regression

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

* test(proxy): drop spend-log encoding expectation superseded by main's poison isolation

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

* fix(anthropic): finish the wrapped stream's failure log before surfacing the error

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

* fix(anthropic): wait for the wrapped stream's failure log to finish like the inline dispatch did

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

* test(anthropic): wait for the executor-run sync failure callback separately from the async caller boundary

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

* test(proxy): pin pass-through stream fixture start times

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

* fix(logging): publish failed attempt payload before awaiting failure callbacks

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

* fix(logging): publish failed attempt payload from the failure handler after baseline invalidation

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

* refactor(logging): drop unused discard_partial_usage_for_failure

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-20 21:46:45 -07:00
Kazuki Hashimoto
c8ed5ccca8
fix(router): unregister strategy selectors from global callbacks on discard (#45795)
* fix(router): unregister strategy selectors from global callbacks on discard

Router.discard only removed its own bound methods from litellm's callback
lists, so the strategy selectors it registered (least-busy, usage-based,
latency-based, cost-based and routing group selectors) stayed behind and
kept receiving events after the router was gone. Reuse the identity based
unregister that update_settings already uses so a discarded router takes
only its own selectors with it

* fix(router): purge discarded selectors from every event callback list

function_setup copies litellm.callbacks into the success and failure
lists on every request, so a router that served traffic still leaked its
selectors there after discard. Route the cleanup through
remove_callback_from_all_lists, which now also covers input_callback, and
fold the selector gathering into discard so the router coverage check
sees no untested helper

* fix(router): keep live routers, in-flight hooks and user_config limits intact on discard

discard() now rebuilds every global callback list without the router's selectors, optional pre-call checks and own usage callbacks instead of removing them in place, so a callback loop already running never skips a hook. It then re-registers what the remaining live routers need, so a same-strategy survivor keeps its deduped handler. The proxy's per-request user_config router is discarded only after its call returns.

* fix(router): bind user_config handlers to the request and tighten discard

When a user_config request carries a logging object, its router is
discarded before the request runs and its handlers ride on that
logging object, so v1 rpm and v2 tpm limits count every request and
concurrent requests count once. Without one, the router is discarded
after the request as before. The router's pre-call loops also read
the request-scoped handlers.

discard() now refills only the handler keys it vacated, guards the
callback lists and the live-router set with a lock, covers routers
built without a model_list, and drops its Slack alerting, adaptive
router hook and budget limiter sync task

* fix(proxy): reuse one user_config router per config and bind its handlers to each request

Every user_config request now goes through a cached router built once per
config. Its strategy handlers leave the global callback lists and are bound
to the request's logging object instead, so limits count each request once
and nothing is left behind after the request.

* fix(router): keep the proxy budget limiter on user_config requests

A user_config router now swaps in only its strategy selectors, so the proxy's
RouterBudgetLimiting stays on the request and keeps counting its spend. Usage
logged after a strategy is retired, such as a request still in flight when its
user_config router is evicted, now schedules one more push to Redis.

* fix(proxy): init the retire fields on the key model budget limiter

The key model budget limiter builds its own state instead of calling the base
init, so with the proxy's Redis attached every spend increment raised on the
missing retire flag and the key's model budget never counted spend.

* fix(router): bind the deployment callbacks to user_config requests

A cached user_config router serves requests through each request's logging
object but bound only its selectors and policies there, so a failing
deployment was never cooled down and its usage counters missed requests.

* fix(router): retry an evicted router's budget writes and keep the adaptive header on user_config requests

A retired budget limiter now keeps retrying its queued Redis increments every
sync interval until Redis accepts them, instead of giving up after one failed
write.

The x-litellm-adaptive-router-model header now comes from one stateless hook in
litellm.callbacks that holds no router, so user_config adaptive routers, whose
per-router hooks are request-scoped, still answer with the header.

* fix(router): lock request callback setup against router cleanup and drain in-flight budget writes on retire

function_setup now edits the global callback lists under the same lock that
router cleanup uses, so a router dropping its callbacks on another thread can
no longer shift the indexes function_setup pops or put a dropped handler back.

A budget limiter retired while a Redis write is still in flight now keeps
retrying that batch if the write fails, instead of leaving the spend queued
with no task left to send it.

* fix(utils): set up loggers after releasing the callback lists lock

function_setup held the callback lists lock while set_callbacks ran its one-time
logger setup, which can block on a Slack package install, so other threads and
Router.discard waited on it. The lock now covers only the list reads and writes.

* test(router): annotate the retire test's locals as Final

* fix(logging): skip replaced global callbacks on the input path too

replace_global_callback already kept a same-key global logger out of
success and failure dispatch. pre_call and post_call still sent it the
input events, so a logger with an input hook could count a request start
it never saw finish. Both now share one filter with the success path

* test(logging): type the input-path regression test

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-21 02:39:36 +00:00
alberto
f768fb32ef
fix(streaming)!: send the include_usage chunk with an empty choices array (#47109)
* fix: Send the include_usage chunk with an empty choices array

Signed-off-by: Alberto Perdomo <aperdomo@redhat.com>

* test: Skip chunks with no choices when reading streams

Signed-off-by: Alberto Perdomo <aperdomo@redhat.com>

* fix: Strip usage from plain-iterator async stream chunks

Signed-off-by: Alberto Perdomo <aperdomo@redhat.com>

* test: Expect empty choices on the usage chunk in the gemma tracing test

Signed-off-by: Alberto Perdomo <aperdomo@redhat.com>

* fix(streaming): send the empty-choices usage chunk only to callers that asked for include_usage

The usage chunk now has choices=[] only when the caller set
stream_options.include_usage itself. When the OpenAI handler, the proxy's
always_include_stream_usage setting, or the harness added include_usage on the
caller's behalf, the chunk keeps its legacy single empty-delta choice, so
clients that read choices[0] on every chunk keep working.

The proxy and harness mark their injection with the private
_litellm_stream_usage_injected flag, and the proxy scrubs a client-sent copy.
Plain-iterator streams keep usage on their chunk when usage was not requested,
and the LLM Shield trailing flush ignores chunks with no choices.

* fix(proxy): mark the default stream usage injection so hooks keep the legacy usage chunk shape

The proxy's default strip-mode injection adds include_usage for clients that did not ask and drops the usage chunk before it reaches them, but streaming iterator hooks still saw it with choices=[]. Marking that injection with _litellm_stream_usage_injected keeps the old single empty choice for them.

* fix(router): treat deployment and router-default stream_options as injected, and drop the client flag on the queue route

* refactor(router): move the stream usage injection check into router_utils

* fix(router): read stream_options from pydantic deployment params when marking injected usage

Update the mantle, openinference, and text-completion integration tests to expect the include_usage chunk with empty choices.

* fix(proxy): keep the stream usage injection marker out of stored request bodies

* test(streaming): cover who asked for stream usage on chat, queue, deployment and spend log paths

* test: type the queue flag test, pin the logging start time, and build the deployment usage config without mutation

---------

Signed-off-by: Alberto Perdomo <aperdomo@redhat.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-21 02:26:05 +00:00
Kerry Lu
27efc387f7
fix(proxy): price earlier timed-out retry attempts at their own deployment's model rate (#47648)
* fix(proxy): bill every earlier timed-out retry attempt at its own deployment's model rate

* fix(router): read the retry record's base_model through the typed metadata helper

* fix(router): keep the four-record retry breadcrumb cap

* fix(router): carry a deployment's litellm_params base_model on its retry record

* chore(router): place the cast-ok reason on the cast line and list the base_model helper as covered through log_retry

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-20 22:26:09 +00:00
Kerry Lu
f0ae445cc4
fix(proxy): bill partial Responses API streams when the client disconnects (#47611)
* fix(proxy): bill partial Responses API streams when the client disconnects

Streamed /v1/responses requests and the default-route OpenAI and Azure /v1/messages requests behind them logged a 499 with zero spend when the client went away mid-stream, while /v1/chat/completions already billed the chunks it had sent. The Responses API iterator now keeps the response.created payload and can hand the disconnect cleanup a response.incomplete event with counted usage and the served service_tier, and the messages-route byte stream and router wrappers delegate to it.

* fix(proxy): satisfy lint gates for partial Responses API disconnect billing

Make the disconnect helper public so the proxy, router and messages adapter can import it without reportPrivateUsage, type the SSE byte stream as an AsyncGenerator so aclose is known, read litellm_call_id off the logging object, and keep the new tests on Final locals with the recorder injected through dynamic_async_success_callbacks instead of litellm.callbacks.

* fix(router): bill the primary stream's tokens after a mid-stream hop and chat-bridge Responses streams on disconnect

The router's fallback wrapper now folds the primary Responses stream's relayed tokens into the fallback's disconnect event the way _combine_responses_fallback_usage does for a natural end of stream, and LiteLLMCompletionStreamingIterator (chat-only providers served through /v1/responses) builds its disconnect event from the chat completion chunks it collected. stamp_responses_usage_cost is now public so the bridge module can price that event.

* Potential fix for pull request finding 'CodeQL / Log Injection'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-10-20 12:24:38 -07:00
Mateo Wang
dfac770e2b
feat(proxy): map baggage values to budget tags (#47358)
* feat(proxy): map baggage values to budget tags

* fix(proxy): preserve mapped tags in the parsed request cache

* fix(proxy): preserve mapper accounting and tag policy

* fix(proxy): preserve baggage tags across native transports

* fix(proxy): keep mapper rejection and passthrough logging safe

* fix(proxy): scope baggage tag mappings to billed inference and keep them out of tag routing

Mapped tags now apply only to billed LLM API routes, skipping token counting,
model listing, MCP and A2A. They classify spend and budgets but never select
deployments unless a key, team or client set the same tag. WebSocket inference
returns 403 only while a required rule exists. A tag created with /tag/new resets
its spend counter so its budget counts from creation. The General settings editor
keeps a new mapping unsaved until every required field is filled in.

* fix(proxy): keep client-tag rejection identical with baggage, map A2A calls, skip request previews

* fix(proxy): keep mapped tags out of requester metadata, answer 503 on tag lookup errors, map custom pass-through routes, check the key before the WebSocket 403

* fix(proxy): keep caller-sent tags routable, keep mapped tags out of policy and guardrail matching, honor allow_requests_on_db_unavailable, reject control characters in baggage values

* test(proxy): cover baggage tag mappings end to end

Adds integration cells for request_tag_mappings across chat, messages,
responses, A2A and custom pass-through spend, required and budgeted
rules, the WebSocket refusal, unbilled routes, reject_clientside_metadata_tags,
tag routing and policy isolation, a dropped tag read with and without
allow_requests_on_db_unavailable, and the /tag/new spend counter reset

* fix(proxy): keep base tag merges for requests that map nothing, accept JSON-string metadata tags, count litellm_metadata caller tags

Requests with no mapped baggage now reuse the cached body exactly as before, so header and key tag merges still normalize metadata for routing. The client body copy is made only when a tag maps, and the base merges are mirrored onto it. A request that maps nothing no longer gets a 400 for non-object metadata. caller_tags now counts tags sent in both metadata and litellm_metadata. Optional mappings no longer reject JSON-string metadata tags under reject_clientside_metadata_tags.

* fix(router): keep baggage-mapped tags from selecting tag-scoped pre-routing strategies

Auto, complexity, adaptive and quality router selection, the consumed-tags stamp and compression policy lookup now read request tags without baggage-mapped tags, unless the caller sent the tag or the key, team or project carries it, matching deployment tag filtering.

* feat(proxy): parse baggage headers into request tag mappings

* fix(proxy): match plus-sign baggage keys and keep deployment tags in guardrail modes

* fix(proxy): keep baggage-mapped tags behind key, team and deployment tags and serve non-object or invalid DB mapping settings like unset

Mapped tags are added after the key, team, deployment and caller tags, so a tag-mode guardrail picks the same mode with or without baggage. A non-object metadata field maps like empty metadata, and an invalid request_tag_mappings value loaded from the DB is ignored with an ERROR log instead of failing every request

* fix(router): merge deployment tags in linear time and keep malformed client tags as sent under mapped baggage

Merge serving deployment tags into the request tags in one pass, so a large client tag list no longer blocks the event loop. Leave a non-list metadata.tags untouched when baggage maps a tag, so the request gets the same answer as without baggage. Add the C17 integration cell for the team tag's guardrail mode under a mapped deployment tag.

* test(spend): cover string metadata.tags under mapped baggage

* fix(proxy): check mapped tag budgets when metadata.tags is a non-list or holds non-strings

Mapped baggage tags are applied again after the header and key tag merges, so
a header or key tag that replaces a string metadata.tags no longer hides the
mapped tag from the tag budget check. An empty or false metadata.tags is read
like a missing one, and a tags list with non-string items is kept as sent with
the mapped tags added after it, instead of failing auth with a 401
2026-10-20 09:09:42 +00:00
Mateo Wang
d698106ed7
fix(router)!: schedule default_priority requests once and reject a malformed priority (#47498)
* fix(router): schedule default_priority requests once and reject a non-integer priority

A router configured with default_priority failed every request that carried no
priority with a 500 naming a missing 'priority' argument, and an explicit
priority re-entered acompletion and hit the same error. acompletion now resolves
the priority once (the request's integer, else the default, with 0 kept as a
real priority) and schedules through _schedule_factory directly, while
schedule_acompletion delegates to acompletion so the two paths share one queue
pass. A non-integer priority answers 400 naming the param, or is dropped under
drop_params, instead of leaking to the provider or crashing the scheduler.

* fix(router): honor router-level drop_params and schedule prompt-management models under a priority

* fix(router): drop a non-integer priority when the model's deployments set drop_params

* fix(router): resolve the drop_params deployment scope for team routes and deployment ids

* fix(router): read the request and router drop_params flags without a loop-assigned Final

* refactor(router): type the prompt-management wrapper's return and mock the provider in its test

* perf(router): consult drop_params for the priority only when it is not an integer

* test(integration): assert the new priority contract for non-integer and zero priorities

* docs(router): describe default_priority as the priority every request without one takes

* fix(router): parse default_priority once and apply it to text completions and the queue route

A digit string default is taken and any other non-integer default is ignored with a warning instead of failing every request. atext_completion and /queue/chat/completions resolve the priority the way acompletion does: a request without one takes the default, an integer queues once, and a non-integer answers 400 or takes the default under drop_params. Integration cells cover every default-priority surface on an owned two-instance rig.

* fix(router): type the priority dispatch so the lint budgets hold

* fix(router): read priority like the scheduler did, require a priority on the queue route, and keep litellm's drop_params order

* fix(router): drop a priority named in additional_drop_params and report the queued priority on streamed queue responses

* fix(router): serve deployment-id requests without waiting behind the queue and ignore a boolean default_priority

A request whose model is a deployment id now counts that deployment as healthy in the
scheduler, since routing by id already ignores cooldown. Before, the scheduler found no
healthy deployment for an id, so the request waited behind every entry ahead of it.

A boolean default_priority (true, or YAML off) is ignored at startup with a warning,
like any other value that is not a whole number.

The scheduler integration tests now share worker state for the defaulted pair, cool a
group through /v1/responses so no stale in-memory queue is left behind, and use "high"
instead of "1" for the non-integer cases. The concurrent burst test no longer asserts
that the queue drains, matching the explicit-priority burst test, since the scheduler's
read-modify-write queue can race across workers.

* test(router): keep the queue drain check as a BUG-skipped integration test

* fix(router): queue default_priority chat requests once instead of failing them

* test(router): open with the default_priority regression tests alone

* fix(router): honor deployment drop_params for a malformed priority on a request routed by provider model name
2026-10-20 08:00:57 +00:00
Mateo Wang
8a2782e41c
feat(router): route batch creates to mode batch deployments and expose supported_endpoints on /model_group/info (#47376)
* feat(router): expose supported_endpoints on /model_group/info

* feat(router): route batch creates to mode batch deployments in mixed model groups

* fix(router): prefer batch deployments after eligibility filters and in group-name credential lookups

* fix(router): keep group-name credential lookups on the first deployment and prefer batch deployments before cooldown

Model-encoded ids carry only the group name, so preferring the batch deployment there moved ids created before the upgrade onto its credentials. Batch creates now pick batch-capable deployments before health checks and cooldown, routing and group info share one batch-capable check, the cost map's /v1/batch is dropped only for Azure, and /model/info applies the same per-deployment rule

* test(router): map _listed_endpoints to its /model/info tests in the router coverage check

* fix(router): skip only Azure chat deployments for batch creates and list batch support per caller team

Narrow the batch create filter to Azure and azure_ai deployments that are not mode batch, so OpenAI groups keep
their chat deployments during cooldown and under tag filters. Mark batch deployments by mode batch only, since
stored endpoint lists from older Admin UI saves copy the cost map. Leave /model/info unchanged so the Admin UI
never saves a computed endpoint list, and pass the caller's team into /model_group/info.

* test(proxy): let the model hub router fakes take the caller's team_id

* fix(router): gate batch-create routing on deployments the caller's team may use and list only /v1/batch for Azure batch deployments

* fix(router): skip a malformed model_info.supported_endpoints instead of failing group info and chat

* fix(router): apply the batch create filter after cooldown, managed files, and tags

The batch filter ran before health checks and cooldown, so a batch create on a mixed
Azure group answered with a router 429 or 401 when cooldown, tags, or a managed file's
targets left no batch deployment, where main sends the create on and returns Azure's
400. The filter now runs right before order filtering, after every eligibility filter,
so those cases answer as main does and creates still skip Azure chat deployments
whenever a batch deployment is left

* test(router): annotate the batch routing test assignments with Final
2026-10-20 07:01:12 +00:00
ekpog200
ba61ecdf57
fix(videos)!: keep model_id in proxy video ids so per-model keys resolve on status/content (#44614)
* fix(videos): keep model_id in proxy video ids so per-model keys resolve on status/content

Provider transforms encode returned video ids with an empty model_id, so
GET /v1/videos/{id} and /content cannot resolve the deployment (and its
per-model api_key) and fail with invalid_api_key.

Re-stamp the returned VideoObject id with the resolved model_id in
video_generation, video_status, video_remix, video_edit and
video_extension.

Fixes #33423

* fix(videos): return a copy instead of mutating the response, read hidden params via helper

* test(videos): fully type the new video id re-stamping tests

Annotate every parameter and return added by this PR (hidden_params,
harness, response, -> None), drop Any from the test helper, mark locals
Final, and narrow the endpoint/helper results with isinstance(VideoObject)
instead of reading .id off an untyped value. Route the mocked processor
response through a typed Harness.respond_with, read NotRequired
DecodedVideoId keys with .get, and name the rule and reason on the
_hidden_params suppressions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(videos): fill only an empty model in returned video ids

Keep an id that already names a model as is, so the asked-for name,
routing group and served group in it still drive access checks and
routing. Fill an empty model from the model the caller asked for
before the routed deployment id, and read the request model through a
typed view so the type gate does not grow

* fix(videos): route video calls to the deployment that created the video

Video ids now carry the id of the deployment that created them. Status,
content, remix, edit and extension calls go to that deployment when it
belongs to the requested group, so a group spread over two provider
accounts no longer polls the wrong one. A provider model shared by
several groups resolves to the creating deployment's group, and the
asked-for model is written into an id only when the router can route it.

* fix(videos): pin video calls only to deployments the caller may use

Pin a video id's creating deployment at deployment selection, and only when the caller may use that
deployment in the requested group, so aliases, routing groups and access groups still apply and the
spend label and fallbacks keep the requested group.

A status poll keeps the deployment its id names and takes the deployment that answered when the id
names none. An alias that routes to the creating deployment's group is kept on status and content.

* test(router): reset module fallback globals between unit tests

* fix(videos): check and route polls by the name the router serves

A poll on a video created through a provider wildcard is checked against the name the wildcard serves, a team model is checked by its public name, a poll stamps the deployment that answered, and the sync Router pins video calls the same way the async path does

* fix(videos): pin polls only among the deployments routing allows

Video calls route by group again and the creating deployment is picked only from the deployments left after access, team, health, cooldown, budget and tag filters, so a pinned poll no longer skips them. Wildcard groups now pin too. Adds tests for cooldown, wildcard pinning and character lookups through a provider wildcard, and return types on the auth tests

* fix(videos): scope the video pin to video calls, keep fallback-served videos reachable, skip cooldown on video 404s

* fix(videos): stamp the requested group only when one of its router fallbacks served the create

* fix(videos): follow chained router fallbacks for fallback-served videos, test with a real Router

* test(router): list the video pin helper with its sibling in the router coverage exemptions

* refactor(auth): walk chained router fallbacks without recursion

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-20 05:42:59 +00:00
Mrunmayee Daware
2196943195
fix(proxy): match partial wildcards against prefix-stripped model ids (#46390)
* fix(proxy): match partial wildcards against prefix-stripped model ids (issue #46384)

Refactor model checks to improve filtering logic and type hints.

* test(proxy): partial wildcard listing regression coverage (issue #46384)

* fix(proxy): drop unneeded annotation on suffix list (LIT001)

* fix(proxy): annotate suffix list with reasoned mutable-ok (basedpyright)

Change suffix_appended_wildcard_models to a typed list.

* Add lock timeout before creating telemetry report table

Set local lock timeout to 10 seconds before creating the telemetry report table.

* fix(proxy): list alias wildcards as routable ids, not raw catalog ids

An alias whose suffix collides with a catalog sub-prefix (e.g. public
"cloudflare/@cf/*" over deployment "cloudflare/*") took the partial-filter
branch and listed raw catalog ids. Routing substitutes the captured suffix
into the deployment pattern, so those listed ids routed to targets that do
not exist. Only use filter semantics when the deployment repeats the
wildcard suffix (or no deployment is available); aliases prepend their
prefix to provider-stripped catalog ids so every listed id routes back to
a real catalog model. Addresses Greptile review on #46390.

* fix(proxy): derive partial wildcard listings from the deployment's routing pattern

List an id for a partial wildcard only when routing it through the deployment's
pattern lands on a catalog model, so aliases, catch-alls, and key filters over a
wildcard deployment list ids that answer when called. /model/info expands a bare
* row the same way /v1/models does.

* fix(proxy): drop fragment ids for a catch-all over a partial wildcard and route bare aliases through the deployment pattern

A * catch-all over a partial deployment now lists no catalog ids, a bare alias like my-cf/* over cloudflare/@cf/* names ids by the deployment pattern, an alias whose wildcard count differs from its deployment lists nothing, and /model/info rows show the model each listed id routes to

* fix(proxy): keep listed ids on /model/info alias rows and clean up test-registered models

* fix(proxy): list catalog models through routing-prefix wildcard deployments

* fix(proxy): keep namespace and bare catch-all wildcards on routable listings

A leading deployment segment the model name repeats, or the provider name
itself, is no longer read as a routing segment, so same-name namespace
wildcards and a key filter over `*` -> `openai/*` stop listing ids that
route to no model. A bare `*` deployment under a prefixed name keeps the
legacy listing. `*` over a partial deployment now lists the names that
route through it, dropping empty ids.

* fix(proxy): list nothing for a catch-all over a mid-word wildcard deployment

* fix(proxy): list only wildcard ids that route back to their deployment and catalog model

A listed id now has to round-trip through the alias pattern to the catalog model it names, and the router's wildcard match for it has to pick this deployment, so an id a more specific sibling wildcard serves is no longer listed under the catch-all on /v1/models or /model/info

* fix(proxy): list wildcard aliases that route at a word boundary and ids a sibling serves with the same model

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-20 02:49:42 +00:00