Commit graph

85 commits

Author SHA1 Message Date
shrey
7143aeeb86
feat(passthrough): add Deepgram streaming text-to-speech WebSocket route (#49282) 2026-10-31 12:46:40 -07:00
Kerry Lu
948445c828
fix(watsonx): read pass-through credentials from use_in_pass_through deployments (#49462)
* fix(watsonx): read passthrough credentials from use_in_pass_through deployments

/watsonx/{endpoint} always called the provider config with api_key=None and api_base=None, so it only ever
worked from WATSONX_API_KEY / WATSONX_API_BASE. It now asks PassthroughEndpointRouter for the proxy-wide
watsonx deployment flagged use_in_pass_through, like the other passthrough routes, and keeps the env
fallback when none is flagged.

Co-authored-by: T K Chandra Hasan <t.k.chandra.hasan@ibm.com>

* fix(watsonx): keep env key precedence when no deployment is flagged

Resolve only use_in_pass_through deployments in the route. get_proxy_wide_credentials also falls back to
WATSONX_API_KEY, which put it ahead of WX_API_KEY in generate_iam_token

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: T K Chandra Hasan <t.k.chandra.hasan@ibm.com>
2026-10-31 11:53:25 -07:00
ryan-crabbe-berri
8ff495be96
fix(proxy): price OpenAI pass-through /v1/decisions calls like native Decisions (#49392)
The OpenAI pass-through dispatcher kept its own list of endpoints it could price,
separate from the handler, and /v1/decisions was on neither. Calls reached OpenAI
but logged $0 and no tokens.

The handler now owns the list the dispatcher reads, and prices a POST to
/v1/decisions through the same Decisions response type and cost path native
/v1/decisions uses, so the _decisions cost-map rates apply. Reads and sub-routes
under /v1/decisions stay unbilled.

Fixes #48543
2026-10-31 07:34:29 -07:00
Mateo Wang
a713ed6bab
fix(gemini): add thought_signature_in_tool_call_id to keep tool call ids short for clients that cap them (#47692)
* test(e2e): add gemini tool call id length repro for LIT-8912

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

* fix(gemini): add thought_signature_in_tool_call_id to keep tool call ids short for clients that cap them

* fix(gemini): pair a tool result with its call when the client re-adds the thought signature to the call id

Strands, which the AgentCore harness runs, puts provider_specific_fields.thought_signature back on the
assistant tool call id as <id>__thought__<signature> while its tool result keeps the bare id, so the Gemini
conversion found no matching call and answered 500. Ids now match with the signature suffix stripped.

Also adds thought_signature_in_tool_call_id to the litellm_params inventory test and gives the pass-through
tests' mocked logging objects the litellm_params a real Logging object always has.

* fix(gemini): keep the thought signature on streamed Messages and Responses tool calls and accept a string setting

* fix(gemini): read the thought signature back from Responses input and coerce the per-request setting

A Responses function_call input item now keeps its provider_specific_fields, so a
client that sends the item back gets its real thought signature replayed to Gemini.

A per-request thought_signature_in_tool_call_id of "false" or 0 now keeps ids short,
and a value that is not a boolean answers 400, or is dropped under drop_params,
instead of a 500.

The e2e test now uses the sdk fixture and @meta subjects

* fix(caching): key the response cache on thought_signature_in_tool_call_id

* fix(responses): send thought signature fields only with short ids and drop malformed routing kwargs whole

* fix(main): validate routing kwargs after the locals snapshot so fallbacks and model_list calls keep working

The e2e test now uses gemini-3.8-flash with enough max_tokens to finish thinking before the tool call.

* fix(gemini): keep non-chat routes tolerant of a malformed thought_signature_in_tool_call_id, match tool results by exact id first, and keep the deployment's value when drop_params drops a malformed override

* fix(router): validate strict litellm params through a helper so star imports cannot re-bind a public Final

* perf(router): skip strict validation when a request overrides no deployment param

* fix(responses): keep a dict tool call's thought signature on the streamed function_call done item

* refactor(main): return invalid supplemental params as a value and test the streaming tool call with a real response

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-31 03:20:42 -07:00
paveln83
febd340120
fix(guardrails)!: keep the upstream error when a /v1/messages stream ends in one (#49088)
* fix(guardrails): keep the upstream error when a /v1/messages stream ends in one

Raw-SSE guardrail stream hooks assembled the whole stream, and the Anthropic
chunk parser raises on an error event, so a stream the upstream ended with
event: error failed assembly. tool_permission then blocked it with a 400 that
hid the upstream error, bedrock and model_armor emitted a could-not-assemble
block, and presidio skipped masking.

Assembly now leaves the error events out, so the delivered content is checked
as usual and a clean stream passes through with its error frame. A rewritten
stream ends with the upstream error instead of a message_delta and
message_stop the upstream never sent. tool_permission also passes a stream of
only error frames, which carries no tool call to check.

* fix(guardrails): do not close a block the failed upstream left open when re-emitting a rewritten stream

* fix(guardrails): check only the finished blocks of a stream cut off by an upstream error

tool_permission and Presidio now assemble an error-ended stream through the
error with an opt-in keyword, leaving out a tool call or thinking block the
upstream cut off. Logging, OpenTelemetry, Bedrock, and Model Armor keep the
old assembly. A rewrite drops the text block stop only when the upstream left
its text block open.

* fix(guardrails): keep a cut-off tool call from the client and stop each rewritten block before the next starts

A failed stream holding a cut-off tool_use is now re-emitted through the rewrite, which leaves the call out and keeps the upstream error, so a denied call never reaches the client. The rewrite leaves its text block open only when it is the last block, and keeps a message_delta the upstream sent before failing. The new tool_permission tests no longer patch should_run_guardrail.

* fix(guardrails): pass a cut-off stream with nothing denied through as sent, minus the cut-off tool call

Re-emitting such a stream through the rewrite sent an empty text block and stopped it, and Claude Code
does not retry a failed stream that shows a finished block, so it showed a mid-response error instead
of retrying. Dropping only the cut-off call's events keeps the stream as the upstream sent it.

* fix(guardrails): strip a cut-off tool call only from a stream the upstream failed

Assembly leaves unfinished blocks out of the check only when the stream carries an upstream error
event, so a stream with an unfinished but allowed tool call and no error frame now reaches the client
as sent instead of losing a call the check already read

* test(guardrails): type the parameters of the new stream error tests

* fix(guardrails): skip SSE data lines that are not JSON objects when reading a stream

A data line holding a bare JSON value such as 5 made the stream helpers call .get on an int, so tool_permission and Presidio answered 500 where main answered 400 or passed the stream through

* fix(anthropic): skip SSE data lines that are not JSON objects when assembling a stream

* test(guardrails): annotate the upstream error stream tests

* fix(anthropic): read a stream's non-object lines and named error frames the same way everywhere

Run the skip for a data line that is not a JSON object through extract_sse_data and the assembler loop, so the usage fallback keeps a stream's tokens and the chat handler keeps its contract.

Presidio now holds a leading non-object frame until the first object frame, so the rest of the stream is still masked or blocked. An event: error frame now counts by its name, so a rewrite keeps it.

* fix(guardrails): refuse an unreadable stream in Presidio and cut every kind of tool call

A non-object JSON line with no Anthropic frame after it now gets a 400 from
Presidio before any data byte, since passing it would send text main refused.
A frame named error stays a normal event when its data is another Anthropic
event. server_tool_use and mcp_tool_use cut off by an upstream error are
dropped like tool_use, and a leading frame with empty data is relayed at once.

* fix(guardrails): refuse what Presidio cannot read past a non-object line and keep non-string error types out of the frozenset

Presidio now refuses a stream assembly cannot read when it holds a non-object
data line, instead of relaying it unmasked. Non-string event types no longer
crash a rewrite, and a CRLF stream that opens with a typed error is no longer
read as one error, so its tool calls are still checked.

* fix(guardrails): refuse a first frame whose type is a list or dict in Presidio

Presidio passed PII at 200 where main answered 500 when the first read was
a JSON object with a list or dict type, or a JSON array with a character cut
at the end of the read. has_unreadable_json_event now reads both as data no
reader of these streams can take, so the first-frame check refuses them.

* test(guardrails): cover Presidio's refusal when a word or null line comes before a Gemini stream

The first read then holds the unreadable line behind one Presidio can pass,
so the check has to look at every frame in it.

* fix(guardrails): treat a stream as error-only only when every data frame is an error

A tool call split over data: lines beside one error frame read as error-only,
so tool_permission passed it, and an SDK joins those lines and reads the call.
The check now joins a frame's data lines and refuses anything but an error.

* fix(guardrails): refuse a stream past an upstream error when a client could read a frame differently

Run 8 of the live risk pass showed each stream reader imitating a client and missing a shape: a
whitespace-led data line, an error key with no type, a split tool call after a plain message_start.
An error frame now has to be plain, and assembly past an error returns None for any frame a client
could read differently, so those streams keep main's refusal.

* fix(guardrails): on a failed upstream, deliver only what tool_permission read

A failed stream now reaches the client through the rewrite that already
serves a denial, followed by the upstream error, even when nothing is
denied. Raw frames could carry a tool call the check never read in shapes
such as a reused block index, mixed line ends, a tool in message_start,
interleaved deltas, or a form feed, and each refusal rule only moved the
search to the next shape.

The error frame goes out as its event name and first data line. An error
is found at CR, LF and CRLF line ends, and the rewrite adds no finished
block when it read no text, so Claude Code still retries the stream.

* fix(guardrails): read an error frame's name as clients do and send the error alone when a rewrite cannot be built

A data-less event: error frame with junk after the name is an unknown event to both SDKs, so a healthy stream
stays raw. A finished tool call whose input is not JSON cannot be rebuilt, so a failed stream sends its upstream
error alone, and a healthy stream still raises as before.

* fix(guardrails): read an error frame as a client dispatches it and keep its JSON whole

Only the last event line of a frame a blank line ended names an error, so a
healthy stream with a later event line or an unterminated error is no failed
stream. Any rebuild failure on a failed stream now sends the upstream error
alone with a warning, a param rule denies an input nested too deep, and a raw
line break inside a JSON error stays escaped so every reader gets it whole.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-31 02:56:20 -07:00
mika-lt
e5aa3d32cd
fix(proxy): forward a valid metadata.user_id to Anthropic Messages on pass-through routes (#49333)
* fix(proxy): forward the client's own metadata on Anthropic and custom pass-through routes

* fix(proxy): forward only a valid metadata.user_id to Anthropic Messages on pass-through routes

The whole-object forward sent keys Anthropic refuses and reached other
providers, so a request that used to pass could start failing with a 400.
Read the client's user_id after hooks, drop it unless it is a string with no
email or phone number, and send only that key when the upstream URL path ends
in /v1/messages. Share the validator with the translated route, whose email
check now matches any embedded address as Anthropic's does.

* fix(proxy): forward metadata.user_id only to Anthropic upstreams no guardrail scans

Limit the pass-through user_id to Messages create on api.anthropic.com or the
configured Anthropic base, mirror the checks Anthropic answers 400 for, and keep
the field off any request a pre-call guardrail inspects. The translated route
goes back to its own user_id filter.

* fix(proxy): judge user_id forwarding on the whole request and cap it for configured Anthropic bases

* refactor(proxy): pass the user_id check a plain mapping instead of an optional body

* fix(proxy): drop an unprintable metadata.user_id and warn when a refused id is dropped

Anthropic's whitespace is not Python's, so a control character before the at sign hid an email from the filter and turned a 200 into a 400. Fireworks and Moonshot also fail on newline ids. Requiring a printable id covers both, and a refused id now logs a warning that never carries the value.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-31 00:16:42 -07:00
simonp9
a392a39627
fix(vertex_ai): bill Claude web search per request on deployments priced in model_info (#49160)
Some checks failed
unit / auth-checks (push) Blocked by required conditions
unit / budgets (push) Blocked by required conditions
unit / custom-logging (push) Blocked by required conditions
unit / db-and-spend (push) Blocked by required conditions
unit / endpoints-and-responses (push) Blocked by required conditions
unit / enterprise-routing (push) Blocked by required conditions
unit / guardrails-hooks (push) Blocked by required conditions
unit / jwt-and-keys (push) Blocked by required conditions
unit / key-generation (push) Blocked by required conditions
unit / logging-misc (push) Blocked by required conditions
unit / mcp-oauth (push) Blocked by required conditions
unit / proxy-runtime (push) Blocked by required conditions
unit / proxy-server-core (push) Blocked by required conditions
unit / root (push) Blocked by required conditions
unit / proxy-utils (push) Blocked by required conditions
unit / ui-unit (push) Waiting to run
unit / docs (push) Waiting to run
unit / helm (push) Waiting to run
unit / OpenAI 2.20.0 compatibility (push) Blocked by required conditions
unit / OpenAI 3.0.0 compatibility (push) Blocked by required conditions
unit / OpenAI 3.25.0 compatibility (push) Blocked by required conditions
unit / redis-py 5.3.1 compatibility (push) Blocked by required conditions
unit / redis-py 6.4.0 compatibility (push) Blocked by required conditions
unit / redis-py 7.4.1 compatibility (push) Blocked by required conditions
unit / redis-py 8.0.1 compatibility (push) Blocked by required conditions
unit / coverage (push) Blocked by required conditions
unit / unit passed (push) Blocked by required conditions
GitHub Actions Security Analysis / zizmor (push) Waiting to run
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled
* fix(vertex_ai): bill Claude web search per request on deployments priced in model_info

A vertex_ai deployment priced in its model_info is registered under its hashed id, so the
web search cost check on the model key never saw "claude" and billed Claude searches with the
Gemini grounding calculator: one search per call, or Gemini's $0.035 default with no search
price. The check now also reads the model the response names.

* fix(vertex_ai): bill token-priced Claude deployments at the served model's search price

A Vertex Claude deployment priced in model_info without a search price now bills each web search at the served model's cost map price, read at cost time and never written into the deployment entry. An explicit search price, zero included, still wins.

* fix(vertex_ai): price web search on the Vertex Claude Sonnet 4.5 and Haiku 4.5 rows

Google bills Claude web search at $10 per 1,000 searches on these models, so token-priced deployments of them no longer bill searches at 0

* fix(cost): bill served-model search price on token-only Anthropic and Azure AI Claude deployments

Token-only Claude deployments on anthropic and azure_ai billed web search at 0 because the deployment entry has no search price. They now fall back to the served model's cost map search price, as vertex_ai already does. Vertex routing reads the served model first, so a Gemini deployment whose id contains claude keeps per-prompt grounding

* fix(proxy): price streamed usage frames under the deployment's model so usage.cost matches the spend row

* test(proxy): give the Vertex streamRawPredict logging doubles the request's model, as a real pass-through Logging has

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-30 23:14:51 -07:00
shrey
afec145edc
feat(passthrough): let a WebSocket relay bill a session from its client text (#49281) 2026-10-31 05:52:25 +00: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
tavren27
7e37c20748
fix(responses): run the guardrails a Responses WebSocket frame selects (#48725)
* fix(responses): run the guardrails a Responses WebSocket frame selects

* fix(responses): name the field a malformed frame guardrail selection got wrong

A frame whose guardrails or guardrail_config has the wrong shape now gets
an error event naming the field, instead of pydantic's raw validation dump

* fix(responses): accept every guardrail selection shape a Responses WebSocket frame sends, and run during_call guardrails

A frame can pick guardrails as a list of names or name-to-config dicts, inside metadata or litellm_metadata, with policies validated as a list of strings. during_call guardrails now run before the turn goes upstream, and selection fields sent next to a nested response are rejected instead of dropped

* fix(proxy): keep a key's configured guardrails when policies add guardrails

A policy's guardrails were merged with dict.fromkeys, so a configured guardrail entry ({name: {extra_body}}) in key metadata raised unhashable type: 'dict' and every request naming policies failed with a 500 on HTTP and on the Responses WebSocket. The merge now keeps existing entries and skips policy guardrails already selected by name. Also types the WebSocket test guardrail's hooks.

* fix(responses): run streaming-scoped guardrails on socket turns, keep guardrail fields on blocks, merge guardrail lists by name

* fix(responses): check frame guardrail picks against the team permission, honor picks beside a nested response, prefer a guardrail entry with settings

* style(responses): format the frame guard and give the pass-through guardrail test an enterprise license

* fix(proxy): keep pass-through guardrail settings, gate frame policy picks, skip moderation-only hooks on socket turns

* fix(responses): keep string metadata guardrail values as client metadata on socket turns

* fix(responses): ask no team permission for string metadata named like a guardrail pick on socket turns

* fix(proxy): socket guardrail picks read the key's team metadata when the proxy has no database

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-30 14:12:51 -07:00
tessellark
a084d13635
fix(passthrough): log the URL model and declared provider on Gemini and Vertex failures (#49075)
* fix(passthrough): log the URL model and declared provider on Gemini and Vertex failures

Pass-through failures on Gemini and Vertex routes carry the model only in the
URL, so the failure spend row and the Prometheus failure counter logged an
empty model. The failure payload now falls back to the model parsed from the
URL on those routes, and the Prometheus provider label honors the provider the
request declares before inferring one from the model name

* fix(passthrough): keep model and provider on mid-stream and Vertex failure logs

Build the failure payload from the URL at both streamed relay sites so a
200 stream that dies mid-body still logs the model, derive vertex_ai for
Vertex routes that never declare a provider, and have the Vertex logging
handler reuse get_vertex_model_id_from_url instead of its own regex

* fix(passthrough): bound the Prometheus provider label and name Vertex failures like the success handler

Declared providers only reach the api_provider label when they name a
registered provider, otherwise the label falls back to inference.
Vertex failure rows now use the success handler's model naming
(vertex_ai/ prefix on rawPredict and streamRawPredict, vertex_ai/search_api
on :search) and its host-based provider detection, and the detected-stream
failure sites are covered by tests.

* fix(passthrough): name Google failures from the URL on untracked methods too

A Gemini or Vertex pass-through failure on a method the success handler does
not track (countTokens, for one) still logged model "" and no provider,
although the model sits in the URL. The failure payload now falls back to the
Google AI host for the provider and reads /models/<name> from the URL, so
countTokens 400s name their model and provider like generateContent ones do.
Non-Google hosts keep the tracked-route rule and still log nothing extra.

* fix(passthrough): trust the declared Google provider and Vertex-shaped paths on custom bases

The failure path only named the Google provider from the URL when the
method was one the success handler tracks or the host was a Google API
host, so a Gemini countTokens failure behind a custom GEMINI_API_BASE
(or a Vertex one behind a custom base) still logged an empty model. A
route that declares gemini or vertex_ai now wins outright, and a URL
whose path carries /projects/<p>/locations/<l>/ counts as Vertex on any
host, while a tracked method on a non-Google host no longer passes as
Vertex.

* fix(pass_through): stop the URL model parser at the query string so a configured key never lands in a spend row's model

* test(pass_through): run the URL-model failure tests through the real ProxyLogging with a recording callback

* fix(passthrough): scope declared provider to pass-through failures and match Google hosts only

Prometheus trusts the resolved provider as before and reads the request-declared
provider only on pass-through failures. Vertex attribution matches Google hosts,
strips operation poll suffixes, prefixes vertex_ai/ only for rawPredict, reads the
URL path only, covers Vertex Live WebSocket failures, and labels a router alias
failure with the alias the client requested

* fix(passthrough): attribute failures from the URL only on built-in Google routes and keep other routes' metric labels

A user-defined pass-through route to a Google host keeps its empty model and provider, since its URL names an upstream model rather than a LiteLLM-managed one. Prometheus failure labels read the route's provider and model_group only for gemini and vertex_ai, so Azure, OpenRouter, Bedrock and other pass-through routes keep their earlier labels. The Gemini URL parser drops the query string, Google host matching needs a dot or dash boundary, and the Vertex Claude prefix follows alt=sse like the success handler.

* fix(passthrough): log the URL model on guardrail blocks and the named model on Vertex Live failures

A pre-call guardrail block on a pass-through route now builds its failure
payload the same way upstream failures do, so the model and provider read
from the URL reach spend logs and metrics. Vertex Live websocket failures
(connect errors, upstream rejections, and the credential failure) now carry
the model from the ?model= query parameter under the vertex_ai provider

* fix(passthrough): mark guardrail-block and Vertex Live credential failures as pass-through calls

Prometheus reads a failure's declared provider only on pass-through calls, so
without call_type it fell back to guessing from the model name. That labeled a
guardrail block on the Gemini route as vertex_ai and the Live credential
failure as None

* fix(passthrough): let the route name a failure's provider and give guardrail blocks their own spend row

* fix(pass-through): keep URL model attribution off routes that name another provider

A built-in route that names a non-Google provider, like /openai pointed at
Gemini's OpenAI-compatible host, no longer takes its failure model from the
Google URL. A guardrail block response now returns the x-litellm-call-id of
its spend row.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-30 09:40:58 -07:00
alvcol
d18efdfe13
fix(vllm): send the pass-through key as a bearer on both /vllm routes (#46861)
* fix(vllm): send the deployment api_key on /vllm passthrough calls

VLLMModelInfo.get_api_key always returned None, so /vllm/{endpoint} calls to a router
model reached the vLLM server with no auth header even when the deployment had an
api_key, and a server started with --api-key answered 401 (#23443).

Return the deployment key (or VLLM_API_KEY) and send it as Authorization: Bearer, the
header vllm serve --api-key checks. x-api-key was never actually sent before, since the
key was always None.

* fix(vllm): send the pass-through key as a bearer on both /vllm routes

vllm serve --api-key only reads Authorization: Bearer, so a keyed vLLM
answered 401 on both pass-through routes. Send the key as a bearer and
keep x-api-key for gateways that read it. Router models send their own
deployment key and never fall back to VLLM_API_KEY, which belongs to
the /vllm route's own server.

* fix(vllm): send only the bearer on router models and stop doubling /v1 on /vllm

Router models now send the same Authorization: Bearer header /v1/chat/completions sends for that deployment, without the x-api-key copy that route never sent. The VLLM_API_BASE route keeps x-api-key next to the bearer, as it sent it before. A router model whose api_base already ends in /v1 no longer gets /v1/v1 on /vllm/v1 paths.

* fix(vllm): send no placeholder key from keyless router models and stop doubling /v1 on the VLLM_API_BASE route

* fix(vllm): send VLLM_API_KEY when a router model with no api_base is forwarded to VLLM_API_BASE

* fix(vllm): keep a router model's own key on the VLLM_API_BASE server and resolve an oidc VLLM_API_KEY

* fix(vllm): treat an empty key as no key so pass-through sends no empty bearer

* fix(vllm): send no bearer for a Google oidc key and match VLLM_API_BASE across a trailing slash

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-30 08:57:48 -07:00
quietjules
df47ec0171
fix(spend): keep a NaN or infinite cost from wedging daily spend writes (#48829)
* fix(spend): keep a NaN or infinite cost from wedging daily spend writes

A non-finite provider-reported cost now counts as not reported, so the token price bills the request. Any other non-finite cost is logged and recorded as 0, and spend log JSON writes NaN and Infinity as null so Postgres accepts the row.

* fix(spend): price non-finite provider costs on every route and drain parked NaN batches

Perplexity, OpenRouter pass-through, and Responses streaming now treat a NaN or infinite reported cost as unreported, so the token price bills the request. A daily spend batch parked in the Redis buffer by an older version with a NaN spend now drains as 0 instead of failing every flush. The cost header never carries NaN or Infinity, and a set holding a non-finite float no longer breaks spend log JSON

* fix(spend): record non-finite savings as 0 and warn when a parked NaN batch drains

An overflowing price on a compressed request made compression savings infinite, which still wedged the daily spend batch. Every savings column now records a non-finite value as 0 with a warning. The Redis drain warns when it reads a NaN an older version parked, since the spend summed into it is lost from daily spend. The writer test now runs the real payload builder and spend log insert

* fix(spend): keep non-finite costs out of headers, responses, and admin JSON

Writing NaN as null is now opt-in for spend log columns, the Redis buffer, and
streamed frames, so admin writes and logging integrations keep their output.
The cost calculator records an overflowing total as 0 with a warning, cost
headers leave out non-finite values, a non-streaming NaN usage.cost is dropped
instead of failing the response, and a parked NaN spend log row reads back as 0.

* fix(spend): keep caller-set call ids from splitting the non-finite cost warning

The overflow warning strips newlines from litellm_call_id, and neither
non-finite cost warning prints the raw value, which said nothing beyond
"non-finite"

* fix(spend): drop non-finite Responses and OpenRouter costs and the breakdown of an unpriceable request

* fix(types): drop a usage cost breakdown that holds a non-finite number

* fix(streaming): keep a dropped non-finite final usage cost from falling back to an earlier chunk's cost

* fix(spend): keep the dropped-cost flag out of reach of provider usage keys

A provider usage key named dropped_non_finite_cost shadowed the Usage method on the instance, so the stream builder raised and dropped the request's spend. The builder now reads the flag through the class, and Usage sets the flag after copying provider keys, so a _dropped_non_finite_cost key cannot set it either.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-30 08:27:35 -07:00
Juan V.
60601ca995
feat(pass-through): add eu/jev decision API pass-through with spend tracking (#46875)
* feat(pass-through): add eu/jev decision API pass-through with spend tracking

Adds a /eu_jev/{endpoint:path} pass-through route for eu/jev, an EU-hosted
decision API that speaks the same POST /v1/systemone contract as TypeSafe.
It reads EU_JEV_API_KEY and EU_JEV_API_BASE (defaults to
https://jev.bevel.software), books spend through the shared decision logging
handler, and prices eu_jev/eu-jeff-1.0.0 and eu_jev/eu-jeff-latest from the
cost map

* fix(pass-through): default eu_jev to its canonical eu-jev.com origin

* fix(pass-through): reject custom_body on the eu_jev route

A key limited to other models could send an eu/jev request through
custom_body, because auth checks the outer body while the shared
pass-through forwards custom_body upstream. The route now answers 400
before anything is forwarded, the same way validate_oss_request guards
the laya and bespoke routes

* fix(pass-through): give the eu_jev route request and 200 response schemas

The OpenAPI docs gate flagged POST /eu_jev/{endpoint} for OA001 and
OA003. The route now documents the eu/jev System One request body and
its 200 answer, usage and meta shape, taken from the eu/jev OpenAPI
document. The lazy OpenAPI snapshot and the dashboard API types are
regenerated with python -m litellm.proxy._lazy_openapi_snapshot and
npm run gen:api

* fix(pass-through): reject streaming on the eu_jev route

A body with stream=true switched the route to streaming, which skips the
decision spend tracking. Reject it with a 400, as validate_oss_request does
for laya and bespoke.

* fix(pass-through): type the eu_jev request body like the other decision routes

* fix(pass-through): check eu_jev decisions against the model eu/jev runs

Serve only POST /eu_jev/v1/systemone, eu/jev's one endpoint, resolve an omitted
model and the legacy names to eu-jeff-latest for key model checks and pricing,
check scopes under eu_jev/<model> like Laya and Bespoke, and answer 503 when
EU_JEV_API_KEY is unset

* fix(pass-through): apply eu_jev key limits, register eu_jev, price unlisted releases

Pre-call hooks and guardrails now see the body model as eu_jev/<release>, so
model_rpm_limit and model_tpm_limit set on eu_jev/eu-jeff-latest apply, and
upstream still gets the client's exact body. eu_jev is a registered decisions
provider, so access groups, config deployments, /v1/systemone and /v1/decisions
accept it, and the Admin UI can add it. A release without its own cost map row
is priced as eu-jeff-latest instead of $0

* fix(eu_jev): use the pass-through deployment's api_base and accept provider-prefixed models

* fix(eu_jev): declare /v1/systemone on the eu/jev cost map entries so the generated endpoint table keeps it

* fix(eu_jev): take pass-through credentials only from proxy-wide deployments

The eu/jev route now ignores team-scoped deployments when it picks the
upstream key and base, so one team's deployment can no longer redirect
another team's calls, and a base ending in /v1 is not doubled

* fix(eu_jev): let a configured pass-through at /eu_jev/v1/systemone keep serving it

Scope the mapped pass-through and gateway allowlist entries to the decision
route instead of the whole /eu_jev prefix, so custom routes under /eu_jev keep
their auth, metering, and gateway behavior from before this PR

* fix(eu_jev): treat /eu_jev/v1/systemone as a provider route only while the built-in route serves it

Drop the decision path from the mapped pass-through list and classify it with one
check that asks the pass-through registry, so an operator's own pass-through at that
path keeps its auth, metering, LLM-route status and litellm_user_api_key handling
from before this PR. The built-in route now yields on a per-request root path and
on any configured method, and the gateway keeps only the exact decision path

* fix(eu_jev): find a configured route at the decision path with or without a trailing slash, and stop a deleted one forwarding

* test(eu_jev): annotate new test variables with Final

* fix(proxy): keep eu/jev pass-through routing stable during updates and method mismatches

Swap an updated pass-through's routes with no await between removal and
re-registration, so the built-in eu/jev route never sees the path as
unconfigured mid-update. The built-in eu/jev handler no longer takes a
configured route's target, and the built-in route also yields on a
method-only match, so GET and PUT on the no-slash form of a slash route
redirect again.

* test(pass-through): annotate the route-swap test's locals as Final

* fix(pass-through): keep an updated auth route's LLM API route checks on the updating worker

* fix(eu_jev): leave a null model out of the upstream decision request

A JSON model of null is limited and counted as eu-jeff-latest, so send the
request upstream without a model field, the way an omitted model goes, since
eu/jev answers a null model with 400 Unknown model

* fix(pass-through): keep route checks on a path another auth endpoint still serves after an update

Forward a non-string eu/jev model unchanged so eu/jev rejects it, and drop only a null or missing one

* test(eu_jev): keep the non-string model test in the eu/jev route test file so a CI shard runs it

* refactor(eu_jev): build the scoped pass-through body as a new dict instead of updating it in place

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-29 17:52:43 -07:00
devin-ai-integration[bot]
a0a8ca97d9
fix(cost): finalize synchronous response cost once for header, spend log and rollups (#48993)
* fix(cost): finalize synchronous response cost before accounting callbacks

* fix(cost): keep serving and billing when cost finalization fails

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

* fix(cost): read the finalized cost only off real logging objects

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

* test(guardrails): follow the renamed success dispatch and deferred sync callbacks

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

* fix(cost): keep cache hits free and their headers finalized when logging is deferred

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

* fix(cost): preserve completed synchronous charges after hook failures

* fix(cost): retain completed usage across cancellation and log suppression

* fix(cost): reconcile completed failures and retain cached usage metadata

* fix(cost): retain finalized request tags in failure budget updates

* fix(cost): honor disabled spend tracking for completed failures

* fix(cost): preserve pass-through accounting across cancellation

---------

Co-authored-by: Joshua Valluru <326636767+joshua-berri@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-29 11:46:49 -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
Mateo Wang
961fabbb76
feat(secret_managers): resolve oidc/google api_key references per request (#47703)
* test(secret_managers): cover oidc/google api_key references resolved per request

Unit tests send a metadata server ID token for an admin oidc/google/<audience> api_key, reuse it within its
lifetime, refresh it before it expires, fetch it off the event loop, fail closed with a 401 that names only the
kind, and honor GCE_METADATA_HOST. The integration file scripts a metadata server and checks the bearer the
upstream receives for config, stored credential and DB credential sources, plus a metadata failure

* feat(secret_managers): resolve oidc/google api_key references per request

An admin-set oidc/google/<audience> api_key now resolves on each call to the GCE metadata server's ID token for that audience, cached until a minute before it expires. Async callers fetch on a cache miss in a worker thread, so the event loop never waits on the metadata server. A failed fetch answers 401 naming only the kind, and GCE_METADATA_HOST points the fetch at another metadata server.

* fix(oidc): bound the Google metadata fetch and answer 503 when it fails

A Google OIDC api_key fetch now waits at most GCE_METADATA_TIMEOUT seconds
(default 3), runs on its own thread pool instead of the loop's default
executor, and sends the token read from the cache at call start even if it
expires mid-call. A metadata timeout, network error, or 5xx answers a
retryable 503, while a 4xx or empty token stays a 401. The expiry and
empty-token warnings log the secret name on one line.

* test(oidc): expect 503 when the metadata server answers 5xx in the integration test

* fix(oidc): answer 503 on token-count routes during a metadata outage and fall back on a bad GCE_METADATA_TIMEOUT

* fix(secret_managers): keep get_secret's metadata wait, share concurrent google token fetches, refuse oidc/google on pass-through

A model's api_key fetch keeps the 3 second GCE_METADATA_TIMEOUT wait while get_secret callers keep the handler default. Any metadata transport error answers a retryable 503. Async cache misses for one reference share one fetch, and a call the reference may not go to is refused before any fetch. Pass-through routes answer 401 for an oidc/google credential instead of sending the reference text. The metadata client is closed after each fetch, and the unused sync resolver is removed.

* fix(utils): refuse an oidc/google api_key on pass-through calls before any fetch

A router-model or credential-name pass-through call with an oidc/google
reference now fails before the metadata fetch and sends nothing upstream,
since the pass-through config never sends the resolved key. The
single-flight test also waits on the fetch-start signal with a bounded
window instead of a fixed sleep.
2026-10-29 08:44:46 -07:00
mika-lt
4212f1af27
fix(passthrough): forward OpenAI-Beta and default agents=v1 on the OpenAI passthrough (#49009)
* fix(passthrough): forward OpenAI-Beta and default agents=v1 on the OpenAI passthrough

The OpenAI-shaped passthrough now forwards the client's own OpenAI-Beta header (agents, chatkit, realtime and any future beta), keeps an operator's extra header over it, and injects agents=v1 on /v1/agents and /v1/vaults paths when the client sent none. Unbilled Agents API calls now log the agent's model on the spend row instead of unknown.

* fix(passthrough): keep OpenAI-Beta forwarding on OpenAI only and name the model on streamed and saved agent rows

Azure and Azure AI keep main's assistants=v2 rule. ChatKit paths get no route default. Streamed agents rows read session.agent.model from the session events instead of defaulting to gpt-4o, and saved agents name their top-level model.

* fix(passthrough): forward the client's OpenAI-Beta only on agents, vaults, and ChatKit paths

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-29 05:52:34 -07:00
tavren27
3e3362d461
fix(cohere): price native Cohere embed calls on Foundry relays and the Cohere passthrough (#48978)
* fix(azure_ai): price Foundry relays of the native Cohere v2 embed route

A proxy relay of POST /azure_ai/<group>/providers/cohere/v2/embed logged spend 0 because the
Foundry relay shapes had no entry for it, so the body fell through to the passthrough object
with no usage. The Cohere v2 embed body-to-EmbeddingResponse transform is now one shared
function that the direct Cohere path and the relay both call, priced from meta.billed_units
and falling back to the echoed texts' token count when billed_units is absent. A body that is
not a Cohere embed shape raises ValidationError, so the relay fails open as before.

* fix(cohere): price native Cohere embeds on the v1 Foundry relay and the v2 passthrough, billing image tokens per caller

The Foundry relay now also prices /providers/cohere/v1/embed, and the shared Cohere embed parser
accepts the v1 plain list of float vectors next to the by-type object. Direct Cohere bills
billed_units.image_tokens (or v1 images) apart from input_tokens, while Foundry already folds the
image into input_tokens, so the parser takes an explicit CohereImageTokenBilling and each caller
picks its own. The /cohere passthrough now routes and prices /v2/embed through the same parser
as the cohere/ SDK path. The relay prices any body that carries billed units whatever texts holds,
and the no-billed-units fallback counts special-token text as plain text instead of raising

* fix(cohere): keep embed type labels, per-image v3 pricing, and the plain row for unpriceable passthrough embeds

Passthrough and Foundry relay log rows keep each vector's embedding type,
text-only usage keeps image_tokens null, a Cohere v3 billed image count is
priced per image, and an embed call the cost map cannot price falls back to
the plain passthrough row instead of raising inside the logger.

* fix(cohere): warn when a passthrough embed cannot be priced and pin test timestamps

* test(cohere): cover embed-jobs passthrough calls writing the plain passthrough row

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-29 05:17:11 -07:00
tavren27
e7d3b24d7c
fix(logging): mark mirrored silent_model calls with is_silent_experiment in spend logs (#48888)
* fix(logging): mark mirrored silent_model calls with is_silent_experiment in spend logs and logging payloads

* fix(router): keep the mirror flag when litellm_metadata is set and strip a client-sent is_silent_experiment

Spend rows read router metadata from litellm_metadata when a call carries it, so the router now sets the flag there too. The proxy drops a client-sent is_silent_experiment with the other router-owned keys, so real traffic cannot be recorded as mirrored.

* fix(logging): mark mirrored calls with a router-only key so clients cannot set is_silent_experiment

Spend rows and the standard logging payload now derive is_silent_experiment from _litellm_silent_experiment, which only the router writes and the proxy and pass-through routes strip from client metadata. A caller's own is_silent_experiment key stays untouched, so it keeps its current effect on mirroring and on provider metadata, and unmirrored calls log false in both places.

* test(langfuse): expect is_silent_experiment false on every logged generation

* fix(logging): mark mirrored calls with a router-only enum value so no client route can set is_silent_experiment

The router stamps SilentExperimentMarker.MIRRORED, which client JSON can never produce, so batches, Responses WebSocket frames and pass-through need no strip. Langfuse layers the flag after its requester-metadata dedupe.

* fix(logging): set is_silent_experiment only on mirrored calls so real calls keep their payloads

Real calls no longer gain an is_silent_experiment key in spend rows or the standard logging payload, so OTEL, S3, GCS and every other logger receive what they did before. The Langfuse regression test now runs the real prompt helper.

* fix(logging): keep the mirror marker out of callback metadata and read the spend-row flag from the logging payload

Logging now strips the router's mirror marker from the request metadata buckets before any callback or vendor sees it, without touching the router's own dicts, so retries keep it. Spend rows read is_silent_experiment from the standard logging payload, which survives the collector's JSON round trip, so collector-mode rows keep the flag too.

* fix(logging): mark mirrored calls through a context variable so router usage counts them once

* fix(router): leave callback-started calls unmarked and stop client flags from turning mirroring off

Logging handlers now run outside the mirrored-call context, so an LLM call a success or failure callback starts while logging a mirrored call is not flagged as mirrored. The guard against mirroring a mirror reads the same context variable instead of request metadata, so a client-sent is_silent_experiment no longer skips the mirrors. The unused module-level get_standard_logging_metadata stops copying resolved keys from raw metadata.

* fix(router): keep main's client flag mirroring skip and the other logging metadata keys

A client's is_silent_experiment: true skips mirroring again as on main, the module-level get_standard_logging_metadata drops only the flag, and the silent experiment tests wait on a logged-call signal instead of polling a deadline

* fix(logging): take the mirror flag in Prometheus labels and Datadog cost tags only from the router

* test: type the new parametrized tests and keep the recorder from changing its argument

* fix(datadog): keep client request tags from setting the silent experiment cost tag

A request tag is_silent_experiment:true set the Datadog cost tag on a direct call.
Only the router flag sets it now. Also covers the sync failure handler's
mirrored-call reset with a failure function callback test.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-29 05:12:50 -07:00
rj-cairn
d06b889d41
fix(streaming): keep merged think tags balanced and split SSE lines only on CR/LF (#48755)
* fix(vertex_ai): split Gemini SSE lines on CR/LF only so U+2028 in text keeps the stream

The Gemini and Vertex AI streaming path read the SSE body with httpx
iter_lines()/aiter_lines(). Those follow str.splitlines(), which also breaks
on U+0085, U+2028, U+2029, \x0b, \x0c and \x1c-\x1e. JSON leaves U+0085,
U+2028 and U+2029 unescaped inside strings, so a data: line whose text holds
one of them was cut in two. The tail is not a data: line and the head never
parses, and the accumulate-and-retry fallback then carries the unparsed
prefix forward, so every later event is appended to it and the stream ends
with no more content and no usage.

SSE lines end only at CRLF, LF or CR. The new iter_sse_lines/aiter_sse_lines
split decoded text on exactly those, treating a CR at the end of one chunk and
an LF at the start of the next as one break. The first request and every
long-decode hop now read through them.

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

* fix(streaming): keep merged think tags balanced and split shared SSE lines only on CR/LF

Close an open <think> block on the first content or tool-call delta and at stream end, reopen it when reasoning returns, and route the shared chat completions stream through the SSE line splitter so U+2028 and U+2029 inside JSON no longer drop chunks.

* fix(streaming): exact-match the SSE [DONE] marker in BaseModelResponseIterator

* fix(streaming): split every SSE reader on CR/LF only and keep the logged think block closed

Switch the remaining httpx stream readers (Anthropic chat, Google GenAI,
Interactions, Codestral, openai_like, Predibase, LangGraph, Vertex Agent
Engine, A2A providers, Azure AI agents, sandboxes, the A2A endpoint, and
others) from iter_lines/aiter_lines to iter_sse_lines/aiter_sse_lines, so a
raw U+2028 or U+2029 inside a JSON string no longer splits an event.

Keep the chunk that closes a think block in the logged and cached response
when a stream ends without a finish_reason, close the block before a legacy
function_call, and make sync Codestral FIM streaming send stream=true and use
the text-completion-codestral stream parser.

Test fakes that only mocked iter_lines now stream real httpx bytes and
assert the content they feed in.

* fix(streaming): split buffered SSE parsers only on CR and LF, shard the codestral tests

Buffered SSE and JSONL parsers used str.splitlines(), which also breaks on
U+2028, U+2029 and U+0085 that JSON allows raw inside strings. They now use
split_sse_lines from sse_lines.py, which also gains a str.split fast path.
Adds tests/unit/llms/codestral to the unit workflow shard.

* fix(streaming): keep U+2028 in restamped Anthropic frames and fold CRLF before splitting SSE lines

* test: type the new stream tests, drop a comment, and benchmark the SSE line splitter

* test: type the iterator class parameters in the line-separator stream tests

* fix(responses): stream tool call arguments opened in a text chunk

* fix(responses): hold a tool call that shares a chunk with text until the text ends

* test: type the a2a SSE handler helper and the sandbox fake responses

---------

Co-authored-by: Tim Vink <vinktim@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: michelligabriele <gabriele.michelli@icloud.com>
2026-10-28 17:00:29 -07:00
Mateo Wang
ea6dbd2109
feat(proxy): log one line per token count call (#47657)
* feat(proxy): log one line per token count call

Each count call on /v1/messages/count_tokens, /utils/token_counter, /v1/responses/input_tokens and the Gemini countTokens routes now logs one INFO line with the litellm call id, the requested model, the model the count ran against, the count, the count source and the status code, so it joins the inference call that sends the same x-litellm-call-id

* test(google_endpoints): give the count test's fake request a headers list

* fix(proxy): keep token count log lines one per call and trusted in JSON logs, and log Bedrock native counts

Quote log values that carry whitespace, quotes, equals, or control characters so a model name can't split or forge a line. Let JsonFormatter record extras win over a dict parsed from the message. Log the Bedrock native count-tokens route through the same line.

* fix(proxy): escape braces in token count log values, log the Bedrock model AWS counts, and skip the line when there is nothing to count

* fix(proxy): log the deployment id the count routed to on every token count line

* fix(proxy): log count requests rejected before counting with the status they answer

A count route that rejects a request before the shared count function runs now logs the same token count line with its 400. An unsupported content block on the Messages count route becomes its 400 inside the logged wrapper, so its line says 400 instead of 500. A count that ends in a BaseException other than cancellation logs a 500.

* fix(proxy): compute the Bedrock counted model id only after a successful count

* revert(logging): keep the JSON log formatter's field order as on main

The count line already escapes braces in its values, so the formatter never parses a dict from it. Changing the order for every logger is a separate decision.
2026-10-28 11:23:52 -07:00
yucheng-berri
74392efbd4
fix(otel): label configured pass-through routes litellm.passthrough and read the operation from the upstream (#48675) 2026-10-28 10:48:34 -07:00
mrinal-berri
b5fb898d03
feat(passthrough): let upstreams report the model name via x-litellm-upstream-model (#48275)
* feat(passthrough): let upstreams report the model name via x-litellm-upstream-model

A pass-through upstream that prices its own requests already reports cost and
tokens in x-litellm-response-cost / x-litellm-total-tokens. Product endpoints
that take no `model` in the body still landed under `unknown`, so their spend
could not be broken down.

When the upstream sends x-litellm-upstream-model, record its value as the
request's model on success and failure rows, overriding any body `model`. The
name is a label only: it never marks the request as upstream-priced and is not
used to look up a price. A blank or missing header changes nothing.

Refs LIT-9910

* fix(passthrough): keep the upstream-reported model name bounded

The upstream model name becomes a spend row, a daily rollup key and a metric
label. Use only the first element of a repeated header (RFC 9110 field
combining, which is how httpx joins it) and cut the name to
MAX_SPEND_LOG_MODEL_NAME_LENGTH, the limit the spend log already applies to
model names.

Refs LIT-9910

* test(passthrough): cover x-litellm-upstream-model through spend rows and rollups

Drives a config-defined pass-through against a scripted upstream and reads
back the spend row, the daily team rollup and /team/daily/activity. Covers the
header winning over a body model on buffered and streamed responses, failure
rows, a model-only header keeping cost_per_request, the no-header and blank
header cases, and repeated or long header values.

Refs LIT-9910

* test(passthrough): annotate new test locals with Final

Refs LIT-9910
2026-10-27 18:28:54 -07:00
yucheng-berri
788868a52f
fix(security)!: build proxy-only failure logs from an allowlist and drop caller guardrail info (#48599)
* fix(proxy): build proxy-only failure logs from an allowlist and ignore request-time GCS credentials

* fix(proxy): drop caller metadata from proxy-only failure logs

metadata and litellm_metadata leave the proxy-only failure-log allowlist, so a rejected request can no longer point its failure log at its own Langsmith or Langfuse host through nested metadata.

* fix(proxy): keep allowlisted metadata in proxy-only failure logs

* test(proxy): isolate proxy-only failure log metadata tests

* fix(logging): ignore non-list request tags when building the logging payload

A guardrail-rejected request with metadata tags set to a non-list value crashed
the standard logging payload, so its spend row lost model attribution.

* fix(proxy): keep router attribution and drop unauthenticated tags in proxy-only failure logs

A request rejected before routing now logs the configured deployment and model group even when its metadata or litellm_metadata carries its own values. Requests rejected at auth no longer write caller tags into failure logs and tag spend.

* fix(proxy): always strip client guardrail info and mirror failure attribution into litellm_metadata

allow_client_pricing_override no longer keeps a caller-supplied standard_logging_guardrail_information on the chat or pass-through paths, since its guardrail_cost is billed. Proxy-only failure logs now also carry key and team attribution in litellm_metadata.

* refactor(proxy): keep the proxy-only failure-log fixes inside the type gates

Types the picked helpers so basedpyright and the type-discipline gate stay at main's counts. No runtime change.

* fix(proxy): keep the WS frame guardrail-info strip and string interaction tags as sent

The guardrail-info key left the pricing strip list, so the Responses WS frame guard now drops it by name. get_request_tags no longer raises on string tags, so background interaction polling checks for them before keeping them as sent.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-27 12:40:59 -07:00
Kerry Lu
0b66a27528
feat(bedrock): price native guardrailConfig trace usage into spend and x-litellm-response-cost (#48278)
* feat(bedrock): price native guardrailConfig trace usage into spend and x-litellm-response-cost

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

* test(integration): bill native Bedrock guardrail trace usage on chat, responses and pass-through

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

* fix(streaming): record the Bedrock guardrail trace of a streamed metadata chunk on the logging object

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

* feat(bedrock): apply guardrailConfig on /v1/messages and price its trace, record the stream trace on both builder paths, redact the native guardrail_response

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

* refactor(bedrock): validate the guardrail trace with one pydantic model

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

* fix(bedrock): record the native guardrail entry when litellm_params metadata is None

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

* test(bedrock): import the renamed messages invoke transformation module

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

* fix(bedrock): skip native guardrail billing on cache hits and redact the trace with message logging off

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

* fix(bedrock): walk the guardrail trace iteratively when redacting it

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

* refactor(bedrock): record the native guardrail trace where the Bedrock response is parsed

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

* test(bedrock): type the native guardrail trace fixtures and locals

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 12:10:45 -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
tin-berri
bc0f598db5
fix(proxy): validate JSON object bodies before request handling (#48438) 2026-10-26 16:11: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
Kerry Lu
9b7f2c8dea
fix(opencode): bill OpenCode Go requests from the reported list_cost (#48254)
* fix(opencode): bill OpenCode Go requests from the reported list_cost

OpenCode returns usage.opencode.list_cost on every response. Read it into the provider-reported cost the cost calculator checks before the price map, for chat and Responses, streamed and not. Keep the static rows as the fallback, and stop the generic usage.cost path from billing Go requests at 0 (usage.cost is what hit the prepaid balance, 0 inside the plan).

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

* fix(core): resolve reported cost breakdowns without recursion

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

* test(opencode): cover reported list_cost spend through the proxy

Replaces the recursive _reported_cost_value with a flat _cost_number helper
so the code-quality gate passes, and adds an integration test that runs the
proxy against a scripted OpenCode upstream for a non-stream and a stream
chat completion, asserting the response cost header and the spend log row

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

* fix(opencode): bill native /v1/messages from the reported list_cost

The native Anthropic Messages route priced OpenCode Go bodies from the price
rows twice over: the non-stream body is a TypedDict the response-cost header
path priced as-is, and the stream assembled its ModelResponse with the rows
cost already stamped. Both now read usage.opencode.list_cost, the non-stream
body through billable_result and the stream from the final message_delta.

The integration test now covers chat, responses and messages, stream and
non-stream, through the proxy against a scripted OpenCode upstream

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

* fix(opencode): keep the stream cost stamp inside the lint gate

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

* fix(opencode): read list_cost off dict chunks and trust it only from OpenCode on /v1/messages

Stream builders receive chunks both as mappings and as ModelResponseStream
objects, so the list_cost scan now reads usage through chunk_usage. The
native Messages route only trusts a reported cost from opencode_go, every
other upstream keeps the price map, which also keeps a zero usage.cost from
being billed as free. Served bodies without stop_sequence still price, and
the new code stays inside the basedpyright gate

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-24 16:44:29 -07: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
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
Mateo Wang
4b1928387f
test(bedrock): detect Vault reads on the event-loop thread (#47597) 2026-10-20 10:45:04 -07:00
Mateo Wang
f3513bca9e
fix(bedrock): resolve AWS region and secret lookups off the event loop (#47331)
* fix(bedrock): resolve AWS region and secret lookups off the event loop

Bedrock request paths that resolve their region, endpoint or bearer token
inside the async handler now do it in the AWS signing executor, so a slow
secret manager read no longer stalls every other request on the worker

* fix(bedrock): type the STS client kwargs and claude platform environment override

Keeps the basedpyright counts at or below the merge base after the off-loop wraps, by typing the STS client kwargs as a TypedDict, the claude platform environment override with concrete dicts, and the realtime region call through the typed getter

* test(bedrock): detect event loop stalls without wall-clock timing

The Vault fake now answers a read only after the test loop runs a callback, so a read made on the loop thread is caught deterministically instead of by a sleep and a heartbeat threshold
2026-10-20 17:04:41 +00: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
6c4c134f23
fix(secret_managers)!: resolve oidc/file and oidc/env refs in admin-configured api_key per request (#47341)
* fix(secret_managers): resolve oidc/file and oidc/env refs in admin-configured api_key per request

An api_key set to oidc/file/<path> or oidc/env/<VAR> in a credential_list entry or a deployment's
litellm_params was sent upstream as literal text, so every call answered 401. The SDK client wrapper
now reads the referenced file or env var on each call and never writes the token back, so a rotated
token is used on the next call. The existing server-owned federation gates treat an oidc/ api_key as
server-owned by value, so a request body, a non-admin credential or model write, user_config, fallback
targets and routing tier params can never carry or name one. Other oidc/ kinds answer 400

* fix(secret_managers): strip only the leading oidc/ from a secret reference

A token file under a directory named oidc resolved to the wrong path, because every oidc/ in the reference was removed

* fix(auth): refuse oidc/ api_key refs in advisor tools and scope the route gates to them

The advisor tool's own api_key now answers a 400 when it holds an oidc/
reference, since its sub-call runs through the client wrapper that resolves
one. Stored fallback targets, auto-router tiers and user_config refuse only an
oidc/ api_key, inline or through the credential they name, so their workload
identity federation handling stays as it was. Provider model discovery for a
wildcard deployment resolves the reference before calling the provider.

* fix(auth): allow admin oidc api_key on deployment writes, resolve refs on discovery, batch probe and realtime health, refuse client api_base on oidc deployments

* fix(utils): resolve oidc api_key refs only from admin-configured sources

Mark an oidc/ api_key read from a proxy-level deployment or a stored
credential, and resolve only that marked value. Team deployments, prompt
templates, nested configs and a direct SDK api_key send the text as
written. An unsupported reference kind raises a 401 so the router fails
over to the group's next deployment.

* fix(utils): send a credential's oidc token only to its own api_base host

A stored credential's oidc/ api_key now resolves only for the scheme, host and port of the
credential's own api_base or base_url, so a team model or vector store that names the
credential with another api_base gets a 401 and that host gets nothing. Saving a team model
with an inline oidc/ api_key answers 400, since team rows never resolve one.

* fix(proxy): send an admin oidc api_key only to the hosts configured beside it

Every admin oidc/ reference now carries the origins of the host params set beside it, checked for api_base, base_url and the Bedrock, S3, SageMaker and deployment_url params. Token counting reads the reference only when it calls the provider, answering 401 when it cannot, and pass-through 401s redact internal paths.

* fix(proxy): redact the token path from token count auth errors

* test(security): cover token count, Bedrock endpoint, api_base update and pass-through path redaction for oidc api_key refs

* fix(proxy): redact the token path from secret reference errors where they are raised

* fix(proxy): keep the secret reference's path out of the error a caller sees

The 401 for an unreadable oidc api_key or pass-through credential now names only
the reference kind and a fixed reason, so a token path under /run/secrets or a
custom credential dir never reaches the caller, while the server log keeps the
full reference for the operator

* fix(secret_managers): strip line breaks from the logged secret reference

A reference holding a newline can no longer forge a server log line

* fix(secret_managers): label an unknown oidc reference kind instead of echoing it

An api_key such as oidc/env_MY_VAR (a typo for oidc/env/MY_VAR) came back in
full in the caller's 401 and in the spend log, since the kind was whatever
text followed oidc/. A kind get_secret does not know now reads
oidc/<unknown kind>. The relative-path hint uses oidc/file//<absolute path>,
which the client-message redaction on chat routes leaves whole.

* test(health): run the real model auth check in the oidc test connection test

* test(secret_managers): read an oidc/file token stored under a directory named oidc

* fix(secret_managers): count a URL in the model name as an upstream host for oidc api_key references

A huggingface/* wildcard call could name its own endpoint URL as the model and
receive the resolved token. The origin check now treats a URL in the model as
a host the reference must allow. The integration contract also reads the
proxy's own database instead of the pytest worker's.

* fix(secret_managers): treat a default port as explicit when matching an oidc api_key origin

A credential on https://api.mistral.ai refused a call to https://api.mistral.ai:443,
though both reach the same server. The origin check now compares scheme, host
and effective port, as assert_same_origin does, and an unparseable api_base is
refused instead of raising. The advisor test sets the real proxy flag instead of
replacing our own helpers.

* test: type the oidc request-body helpers and drive the tier test through mock_response

* fix(secret_managers): refuse an oidc api_key reference whose call drops the configured host
2026-10-20 01:43:59 +00:00
Mateo Wang
cf9ebeda07
fix(proxy): stop billing every OpenAI pass-through /responses sub-route as a Responses call (#47171)
* test(proxy): cover OpenAI pass-through billing for each /responses sub-route

* fix(proxy): bill only the OpenAI pass-through Responses calls OpenAI bills

* fix(proxy): fail closed on unknown /responses sub-routes and label unbilled spend rows

Bill every OpenAI pass-through /responses call except input_tokens, input_items,
delete, and a non-background retrieve or cancel, so an encoded or new sub-route
is never free. Unbilled calls keep the response's model on their spend row, and
a resumed background stream takes its model from the stream's final event.

* fix(proxy): give billed background Responses reads their own spend log row

A billed retrieve or cancel of a finished background response reused the
create's resp_ id, so the spend log insert dropped the row that carried the
cost. It now takes the native path's _background_cost suffix. Adds Azure
path coverage and types the test recorder.

* fix(proxy): bill a background Responses read only once it carries usage

An in-progress poll took the _background_cost spend log id with $0, so the
finished read's row that carried the cost was dropped again. Polls with no
usage are now unbilled like any other read.

* fix(proxy): keep OpenAI's response id for callbacks on a billed background read

* fix(openai): give a streamed background create the settlement spend row id

* fix(proxy): keep a request body from giving its spend row the background settlement id

* test(proxy): give the forged settlement marker test a fixed time

* fix(passthrough): move the background settlement marker into the pass-through types module

A test that reloads spend_tracking_utils gave it a new enum class, so the
handler's marker no longer matched by identity and the settlement row lost
its _background_cost id. The types module is never reloaded.
2026-10-19 22:04:25 +00:00
Kerry Lu
62ce515d15
feat(router): let a deployment set a timeout per service_tier (#47406)
* feat(router): let a deployment set a timeout per service_tier

Adds timeout_by_service_tier and stream_timeout_by_service_tier to a deployment's
litellm_params. A request whose service_tier matches a key uses that value in place
of the deployment's timeout or stream_timeout. A timeout the client sends still wins
and requests without a matching tier are unchanged

Co-authored-by: adc77 <amandogra2016@gmail.com>

* style(router): sort the request_timeout_resolver import

* fix(router): read the service tier maps without the untyped params get

* fix(router): look the service tier timeout up through a typed get

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: adc77 <amandogra2016@gmail.com>
2026-10-19 14:27:29 -07:00
alexcastgonz
6762d65eb8
fix(vertex_ai): price streamed pass-through calls with the regional uplift (#44795)
* fix(vertex_ai): price streamed pass-through calls with the regional uplift

The streamed Vertex pass-through handler builds the response with
stream_chunk_builder, which stamps a cost into _hidden_params without the
vertex_location. The handler then prices the response again with the
location from the URL, but only into kwargs, and the logger prefers the
stamped cost. Streamed calls on a regional endpoint were billed at the
global rate.

Stamp the location-aware cost on the response in the shared
generateContent payload builder so the logger records it.

Fixes #44755

* style(vertex_ai): keep the response_cost assignment within the 120-char limit

* refactor(vertex_ai): set the passthrough response cost with set_hidden_param

Writing _hidden_params directly mutated a parameter and needed a pyright ignore, which the type discipline gate counts as LIT004 and LIT011. set_hidden_param is the shared helper for this.

* test(integration): check Vertex pass-through spend rows keep the regional uplift when streamed

* fix(proxy): price streamed Vertex pass-through spend like the non-streamed call

Drop the location-blind cost stream_chunk_builder stores instead of stamping a cost, so the logger prices streamed generateContent and rawPredict calls with the URL's location and the deployment's custom pricing, the same way it prices non-streamed calls

* fix(proxy): price custom-priced Vertex pass-through deployments at their own rates

The stamped response_cost from the generateContent logging helper is returned before the
logger checks custom pricing, so a deployment with custom input/output rates was billed at
the cost-map price. The helper now passes custom_pricing and the deployment id into
completion_cost, so the stamp holds the deployment rate for streamed and non-streamed calls,
and deployments without custom pricing keep the regional uplift.

* fix(proxy): bill Vertex /v1beta and interrupted pass-through streams by the deployment location and price

* fix(proxy): give the new pyright ignore a reason for the LIT004 gate

* test(integration): bill a custom-priced Vertex pass-through stream at the deployment price, cut-off streams included

* fix(proxy): let the logger price non-streamed /gemini pass-through calls so both modes bill the same

* fix(proxy): keep a client base_model out of pass-through pricing and price gateway Gemini streams as Gemini

A top-level base_model in a pass-through body now reaches the logging
params only for a key or team with allow_client_pricing_override, so a
plain key cannot pick a cheaper model's price once the logger prices
these calls. The test that pinned every litellm-owned body name into the
logging params now leaves base_model to its own key-gated test.

The Vertex handler now classifies a non-Google upstream host as Vertex
only for Vertex resource paths, so a /gemini stream through a
GEMINI_API_BASE gateway is priced as Gemini without the regional uplift.

Adds the audit's integration spend test for Claude on Vertex, /v1beta,
the named custom-priced /gemini deployment, and a cut OpenAI stream.

* test(integration): price the gateway /gemini stream case as Gemini and cover client base_model and gateway host pricing

The cost harness sends /gemini through GEMINI_API_BASE, which the pass-through now logs and bills as gemini, so the streamed case moves from the Vertex cases to the Gemini cases under the Gemini price. New spend tests cover a body base_model repricing only for keys allowed client pricing, a GEMINI_API_BASE gateway billing as Gemini in both modes, and a configured non-Google gateway picking the provider from the path.

* fix(proxy): price pass-through Gemini by the route and ignore a non-string base_model

The Vertex pass-through handler picked Gemini pricing from the URL path,
so a configured pass-through to a Vertex gateway with a short path (no
/projects/ segment) was billed as Gemini, and its Claude streams at $0.
The provider now comes from the route: a call through /gemini or to
generativelanguage.googleapis.com prices as Gemini, and everything else
as Vertex, the same rule main used. Matching the GEMINI_API_BASE host
was rejected because one gateway can serve both APIs, and the path rule
was rejected because short gateway paths look like Gemini paths.

A non-string base_model (a number, list or object) made the cost
calculation fail and bill $0 once the stamped stream costs were gone.
get_base_model_from_metadata now ignores it and falls back to the
deployment's base_model, so the call bills the list price. A 400 was the
other option, but the logger runs after the upstream answered.

* test(integration): bill a non-string body base_model at the list price on every pass-through route

* fix(proxy): bill a non-string model_info base_model at the list price and drop a non-object client model_info

A key allowed client pricing can send metadata.model_info in a pass-through
body. A non-string model_info.base_model made the cost lookup fail, and
once the stamped stream and Gemini costs were gone those calls billed $0
on /gemini and on streamed /vertex_ai and /v1beta. The lookup now ignores
a base_model that is not a string, the same rule the root base_model got,
so the call bills the list price.

A model_info that is not an object (a string or a list) broke several
logging steps that read it as a mapping, so a non-streamed /gemini call
fired no success callback and no pass-through call wrote a spend row. The
pass-through route now leaves a non-object model_info out of the logging
metadata, so the call bills the list price. A 400 was the other option,
but it would turn calls that answer 200 today into errors, and guarding
every reader of model_info would spread the same check over many files.

* fix: reject a non-object model_info before the provider call again

The previous commit made the base_model lookup return None for a
model_info that is not an object. That lookup also runs during call
setup on every router route and SDK entrypoint, where such a value used
to fail before any provider request. With the early return the call went
upstream, the provider billed it, and the request then failed in a later
reader that still expects an object. On /v1/messages and /v1/responses
the key was also charged for a response the client never got.

The lookup again reads base_model from any truthy model_info, so a
non-object value fails during setup the way it did before this PR. A
non-string base_model inside an object still bills the list price, and
the pass-through route still drops a non-object client model_info at
intake, so pass-through calls keep billing the list price.

* fix: match the Gemini API host exactly when picking the pass-through cost provider

The provider check accepted any host ending in
generativelanguage.googleapis.com, so a Vertex host such as
vertex-generativelanguage.googleapis.com was priced as Gemini and lost
the regional uplift. It now accepts that host or a subdomain of it.

* test: pin that a subdomain of the Gemini API host keeps the Gemini price

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-19 08:10:52 +00:00
Mateo Wang
5012083b72
feat(vertex_ai): let operators drop chosen anthropic-beta values on the Vertex AI pass-through routes (#47185)
* feat(vertex_ai): let operators drop chosen anthropic-beta values on the Vertex AI pass-through routes

* fix(vertex_ai): drop configured betas sent through x-pass-anthropic-beta and only fail beta requests on a malformed setting

The drop list now runs on the final upstream headers, after x-pass- expansion, through an
upstream_headers_filter on pass_through_request, so x-pass-anthropic-beta cannot carry a
dropped value to Vertex. A malformed setting now answers 500 only for requests that carry
anthropic-beta or x-pass-anthropic-beta, so Gemini and discovery calls keep working.

* test(vertex_ai): accept the new upstream_headers_filter argument in the existing Vertex route tests

* fix(vertex_ai): refuse a malformed drop-betas list at boot and match list entries written with spaces

* fix(vertex_ai): split drop-betas list entries on commas like the header
2026-10-18 12:51:27 +00:00
Mateo Wang
82ca10cc2d
feat(bedrock): apply bedrock_request_metadata_fields on the /bedrock pass-through routes (#47040)
* feat(bedrock): apply bedrock_request_metadata_fields on the /bedrock pass-through routes

* fix(bedrock): move Invoke body requestMetadata into the signed header and match Bedrock's ASCII whitespace rule

* fix(bedrock): keep Invoke body requestMetadata in the body and answer bad metadata as AWS does

Invoke requests whose body already carries requestMetadata keep the resolved pairs in the body
instead of the 8,500-character signed header. Malformed caller metadata now answers 400 with the
ValidationException error type the AWS SDK expects, and drop_params drops bad pairs on both the
pass-through routes and /v1/chat/completions. The caller-pair dedupe is linear, and the chat
validator treats \s as ASCII whitespace, as Bedrock does

* fix(bedrock): match Bedrock's 400s and header limit on pass-through request metadata

Invoke metadata too long for the 8,500-character header now goes in the body,
Invoke requestMetadata null answers 400 like Bedrock, Converse type errors carry
SerializationException, violation messages name the failing key, and the reserved
user_api_key_ prefix is matched case-insensitively after trimming whitespace

* fix(bedrock): trim the Invoke metadata header to Bedrock's limit and drop bad requestMetadata under drop_params without the setting

* fix(bedrock): leave /bedrock pass-through bodies as sent when request metadata forwarding is off

Global drop_params no longer rewrites requestMetadata on the pass-through routes unless
bedrock_request_metadata_fields is set. Chat keeps dropping bad pairs under drop_params.
Adds a tests/integration wire test for the four pass-through actions and removes an unused
parameter from the Invoke header helper.

* test(bedrock): drive the pass-through metadata wire test with boto3's bearer key env var

* fix(bedrock): trim the pass-through invoke metadata header and stop Nova invoke from checking metadata it drops

* test(bedrock): cover pass-through metadata header trimming, malformed caller metadata, and chat drop_params on the wire

* fix(bedrock): match requestMetadata keys and values against the whole string so a trailing newline cannot pass the 256-character limit
2026-10-17 14:01:50 +00:00
devin-ai-integration[bot]
8bb8ac7f18
fix(proxy): keep a retried /v1/messages stream's spend log by logging the superseded attempt as a failure (#45136)
* 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

* chore: drop duplicate litellm import and mark the stream_ended rebind

* fix(proxy): log only router-superseded attempts as error-frame failures

A client that hangs up at a forwarded error frame closed the stream the same way the router closes a superseded attempt, so a non-retried stream lost its spend. The router now closes a superseded attempt through aclose_superseded_attempt, and only that close routes the attempt to the failure log

* test: type the _settle helper's recorded events

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-16 21:19:58 +00:00
yucheng-berri
b5870df967
fix(bedrock-passthrough)!: return AWS-shaped errors so AWS SDKs read the code and message (#46522)
* fix(bedrock-passthrough): return AWS-shaped errors so AWS SDKs read the code and message

A guardrail block on a /bedrock route now answers with x-amzn-errortype ValidationException and a top-level message, and an error Bedrock returned (such as a 429 ThrottlingException) is passed through with its status, x-amzn-errortype and body

* fix(bedrock-passthrough): keep 5xx guardrail failures and failure-hook replacements unchanged

* test(bedrock-passthrough): guardrail vendor failures and unknown models keep the litellm error

* fix(bedrock-passthrough): only answer guardrail exceptions that blocked content as ValidationException

* fix(bedrock-passthrough): pass Bedrock's content-type through with its error body

* refactor(bedrock-passthrough): wrap the guardrail ProxyException instead of copying its fields

Unit tests drive the error response with the real request processor and ProxyLogging

* test(bedrock-passthrough): cover the direct model route, invoke, and auth, budget and rate-limit errors

* fix(bedrock-passthrough): pass through only errors a Bedrock model operation returned

A guardrail service's HTTP error that carries x-amzn-errortype keeps the litellm error

* refactor(bedrock-passthrough): mark Bedrock's errors where the passthrough raises them instead of matching the URL

A guardrail service's HTTP error never goes through the passthrough call, so it keeps the litellm error whatever its URL

---------

Co-authored-by: yucheng-berri <293926326+yucheng-berri@users.noreply.github.com>
2026-10-15 22:05:46 -07:00
Mateo Wang
3fec5075fa
feat(typesafe): support Jev 1.14 images, box questions, and long-context headers (#46596)
* feat(typesafe): support Jev 1.14 images, box questions, and long-context headers

* fix(cost): drop the typesafe/jev-1.14.0 entry main already prices

* fix(typesafe): send images only with the images header and box questions only to models that answer them

TypeSafe takes assets and input_image parts only when the request carries
TypeSafe-Danger-Zone-Images: true, so without it they follow drop_params like
any other provider that cannot take images. Box support now comes from a
supports_box_questions cost-map flag set on jev-1.14.0 and jev-preview, so
jev-1.13.0 and jev-latest refuse or drop a box question before the call. A
malformed provider_specific_header answers 400 instead of 500.

* fix(typesafe): allow null box confidence and validate asset references and base64

* fix(typesafe): gate box questions on the images header, drop unreadable assets under drop_params, forward opt-in headers on the pass-through

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-14 22:29:48 -07:00
devin-ai-integration[bot]
47e4ed88c2
fix(anthropic): keep refusal stop_reason and stop_details on the logged response (#46104)
* fix(anthropic): keep refusal stop_reason and stop_details on the logged response

* fix(anthropic): keep the refusal stop-fields boundary tolerant of a non-dict upstream body

* test(anthropic): use Anthropic's documented refusal example as the unit-test fixture

* fix(streaming): keep provider fields on async stream cache hits and received_model_id on synthesized finish chunks

* fix(caching): replay cached completion_tokens_details on stream cache hits and skip non-object cached provider fields

* test(integration): cover Anthropic-family refusal stop_details on the wire, the cache and under chaos

* fix(caching): skip a cached completion_tokens_details that fails validation on stream replay

* fix(anthropic): keep refusal stop fields on a JSON-mode non-stream response

Also adds the round 3 audit cells for JSON-mode refusals, the proxy response serialization, and wrong-typed cached usage details on both stream replays

* test(anthropic): expect content_filter on streamed JSON tool refusals after main's stream change

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-14 14:42:49 -07:00
devin-ai-integration[bot]
2d4472efc1
fix(vllm): write spend log rows for /vllm passthrough calls to router models (#45733)
* fix(vllm): write spend log rows for /vllm passthrough calls to router models

The vLLM passthrough config never built a logged response for relayed calls, so stream and non-stream calls through /vllm with a router model wrote no spend row and never charged the key. vLLM now shares the OpenAI-shaped relay logging that Azure already had, moved into an openai_like helper both configs call.

* ci: run the vllm passthrough unit tests in the llms-providers shard

* fix(proxy): log /vllm text completions and ask router models for stream usage

A chat stream relayed to a router model without stream_options now gets
include_usage added on the way out, and the usage chunk vLLM appends is
removed before the client sees it, so the spend row carries vLLM's own
token counts instead of a local estimate. /vllm/completions calls now
write a row too, stream and non-stream.

* fix(vllm): keep usage, fingerprints, and disconnect rows correct on /vllm streams

Inject and strip stream usage on /vllm/completions too, hold every finish
frame for the fingerprint when n > 1, pass a forced-usage server's stream
through untouched, stop turning continuous_usage_stats alone into per-chunk
usage, close the upstream when the client hangs up so the row is written,
and assemble text choices with one sort instead of a scan per choice

* fix(vllm): keep n>1 stream order when holding finish chunks for the fingerprint

A held finish chunk now goes out as soon as any other chunk arrives, so a
choice that finishes while another still streams keeps its place. Only the
finish chunks right before the usage chunk get vLLM's system_fingerprint.

* fix(vllm): keep caller stream_options and log both success handlers

Forward a stream_options the proxy cannot read, or a non-boolean
include_usage, to vLLM as the caller sent it. Only a false
always_include_stream_usage changes the /vllm relay, so clients get the
same stream bytes under every setting.

Record the passthrough origin when logging starts so the sync and async
success handlers both parse the relayed body, hand callbacks a parsed
text completion, and read relayed messages and prompt separately so a
stray field no longer drops the local count.

Add the audit's integration tests for the relay, the settings, and the
Azure spend row.

* test(vllm): expect the caller's stream under always_include_stream_usage true

* fix(vllm): relay long SSE frames in linear time

* test(vllm): time the relay's usage and finish_reason prefilter on one large frame

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-14 12:40:48 -07:00
Mateo Wang
0ebec123fd
chore(typing): clear 670 basedpyright Any errors by validating provider payloads at the boundary (#45388)
* chore(typing): validate provider payloads at the boundary to clear 464 basedpyright Any errors

Replaces untyped reads of decoded JSON in 48 provider, guardrail and proxy
modules with one pydantic validation at the point the payload enters the
module, so every value read afterwards carries a real type.

reportAny 6111 -> 5663 and reportExplicitAny 1416 -> 1400. No basedpyright,
ruff-strict or type-discipline rule is higher repo-wide or in any single
file, and no cast, ignore or suppression marker was added.

* chore(typing): replace 47 Any annotations that every caller already satisfies with object

Each annotation here is a parameter or return type whose value is only
stored, compared, logged or forwarded, so object describes it exactly and
no function body changes. A site was kept only if no basedpyright,
ruff-strict or type-discipline rule rose in any file that references it.

reportExplicitAny 1400 -> 1353 and reportAny 5663 -> 5655.

* chore(typing): type upstream responses and loose annotations across providers, proxy and integrations

Second pass of the Any reduction. Decoded upstream JSON is validated once at
the provider boundary and read through typed views afterwards, annotations
that every caller already satisfies move from Any to a concrete type, and the
nine search transformations validate each result item on its own so error
text no longer carries the item index.

No cast, no type: ignore, no suppression marker and no budget file edit

* ci(unit): run the brave, google_pse, nlp_cloud and opensandbox tests in the providers shard
2026-10-13 15:35:23 -07:00
devin-ai-integration[bot]
7dc7f101d9
feat(decisions): bill decisions and systemone from _decisions cost map fields (#45904)
* feat(decisions): bill decisions and systemone from <provider>/decisions/<model> cost map rows

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

* feat(decisions): mark decisions pricing rows with mode decisions

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

* fix(ui): sync generated API types for decisions mode

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

* fix(decisions): update pricing expectations in proxy tests

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

* test(decisions): price classifier calls from decision rows

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

* fix(health): drop redundant evaluation handler after main merge

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

* test(decisions): inject pricing row for deployed classifier

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

* refactor(decisions): ship the decisions pricing lookup without cost map rows

Restore both cost maps to main. Tests inject their own
<provider>/decisions/<model> rows. The Add Model dropdown hides those
pricing keys and offers mode decisions instead of evaluation.

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

* test(ui): expect the Decisions mode option in the Add Model form

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

* feat(decisions): bill decisions and systemone from _decisions cost map fields

Replace the provider/decisions/model pricing rows with <field>_decisions
fields on the model's own row. cost_per_token overlays them for the
decisions, adecisions, systemone and asystemone call types and prices
through generic_cost_per_token; chat, responses and batch keep the plain
fields. gpt-6-luna gains its Decisions rates (input 1e-07, output 0,
cached and cache-write input 1e-07, 2x above 272k tokens).

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

* fix(decisions): satisfy type gate, schema sync and test_utils schema for _decisions fields

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

* test(decisions): import cost_calculator module to keep basedpyright unknown-variable count flat

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

* fix(decisions): drop stray fireworks import from __init__ and annotate test cost rows for the type gate

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

* test(integration): cover gpt-6-luna decisions and systemone above the 272k tier

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

* refactor(decisions): keep the decisions pricing block clear of main's stale azure comment

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

* chore(cost_calculator): drop stale azure models doc comment

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

---------

Co-authored-by: kerry <kerry@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-12 14:31:12 -07:00
devin-ai-integration[bot]
21cfa0eacd
ci: run every CircleCI check from the integration workflow (#45938)
* ci: run every CircleCI check from the integration workflow

* test(ci): keep the startup test CircleCI-owned

* test(ui): find reset log rows by request id so concurrent workers cannot page them out

* test(ui): retry the key budget window spec until the draft refresh fix lands

* ci(ui): set Playwright workers and retries on the CircleCI command only

The shared playwright.config.ts also drives the Buildkite UI suite, where 3 workers
ran out of memory on one of two runs. Buildkite keeps main's 1 worker and 2 retries

---------

Co-authored-by: yuneng <yuneng@berri.ai>
2026-10-11 07:39:16 -07:00
devin-ai-integration[bot]
a7a3a6b375
test: migrate legacy unit dirs and unrun folders into tests/unit, tests/integration and tests/e2e (#45940)
* test: migrate legacy unit dirs and unrun folders into tests/unit, tests/integration and tests/e2e

* test: fix disk cache collection, ungate saturation check, drop RSS memory tests

* test: assert one delivered log per completion in Datadog and LangSmith checks

* test: drop SDK-direct e2e copies and align cost-map path filter cases

---------

Co-authored-by: yuneng <yuneng@berri.ai>
2026-10-11 06:24:57 -07:00