Commit graph

1165 commits

Author SHA1 Message Date
Kerry Lu
ddd162439f
fix(proxy): stop /tag/update 500 when the body carries the tag's budget_id (#50324)
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-06 08:55:44 -08:00
Alexander Chernov
d1cde1002e
fix(chatgpt): forward text and max_output_tokens to the Codex backend (#38086)
* fix(chatgpt): keep text and accept string input for the Codex backend

Two request-side defects stop chatgpt/* serving structured output or a string
input.

The allowlist at the end of transform_responses_api_request omits "text". The
chat-to-responses bridge translates response_format into text.format, so the
allowlist discards strict schemas silently and the backend answers with prose
instead of JSON. The backend does honour text.format when it receives it.

Separately the backend rejects a bare string input with
{"detail": "Input must be a list"}, while the Responses API itself accepts
either shape, so the string needs wrapping before it is sent.

Both are request-shaping only, so neither depends on the streaming work in
#31332 or #34095, though those two block the same call and have to be in place
to observe this one end to end.

The "text" case was reported in #24356 and closed by the stale bot without a
fix.

Signed-off-by: Alexander Chernov <alexander@chernov.it>

* fix(chatgpt): annotate the input coercion for the type-discipline gate

The LIT002 budget counts mutable-collection construction, and wrapping the
string input builds a list and a dict, which put the total two over its
ceiling. Both constructions are the wire payload this backend requires, so the
list cannot be replaced with an immutable equivalent; annotate the line with a
reason instead, matching the convention used elsewhere in the tree.

LIT005 freezes reasonless suppressions at zero, so the reason is required
rather than decorative.

Signed-off-by: Alexander Chernov <alexander@chernov.it>

* refactor(chatgpt): bind the coerced input instead of rebinding the parameter

Greptile flagged the parameter rebinding and the explanatory comments against
the repo conventions in CLAUDE.md, which bans rebinding a function parameter
(LIT011) and keeps comments to suppressions, TODOs and genuinely complex logic

Bind a Final local for the coerced input rather than reassigning `input`, and
drop both prose comments; the rationale lives in the commit and the PR body
rather than duplicated at the call site. The mutable-ok suppression stays,
since it is the allowed kind and the list is still constructed

Behaviour is unchanged: 24 tests pass, and the same 4 fail with only the
provider file reverted to base

Signed-off-by: Alexander Chernov <alexander@chernov.it>

* fix(chatgpt): forward max_output_tokens to the Codex backend and return incomplete non-stream responses

The Codex backend accepts and enforces max_output_tokens, so the allowlist now keeps it
instead of dropping it. A structured-output request could otherwise stream until the
backend gave up, ignoring the caller's max_tokens.

A capped request ends with response.incomplete, so the non-stream SSE aggregator now
treats that event as terminal and returns the partial response instead of an error.

---------

Signed-off-by: Alexander Chernov <alexander@chernov.it>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-06 06:14:32 -08:00
JeongCheolLee
febbe38326
fix(anthropic)!: do not surface server_tool_use as client tool_calls (#48385)
* fix(anthropic): do not surface server_tool_use as client tool_calls

Anthropic server-executed tools (web search, tool search, code execution)
were returned in chat completions tool_calls with srvtoolu_ ids, so
OpenAI-protocol agent clients treated them as pending work and re-invoked
the model until their recursion limit.

Move them to provider_specific_fields.server_tool_calls on both the
non-streaming and streaming paths. History replay, the /v1/responses
bridge and the /v1/messages streaming guardrail read that field, so
multi-turn reconstruction, web_search_call output items and the
guardrail's fail-closed check on server tool blocks keep working.

* fix(responses): return Claude web_fetch calls as web_search_call open_page items

* fix(responses): keep Claude server web calls without a result as hosted items

A non-streamed Claude turn that calls web_search or web_fetch next to a
client tool can return the server call before its result. Build an
in_progress web_search_call for it, so the Responses bridge no longer
returns it as a client function_call

* fix(anthropic): keep pending server calls native and hide server calls from client tool paths

Replaying a server call that has no result yet now sends it back as
server_tool_use, so Anthropic accepts the next turn. The tool permission
guardrail checks server calls again and rewrite mode removes a denied one.
The /v1/messages adapter keeps Claude's server blocks, and the Responses
bridge never returns a server call as a client function_call.

* fix(anthropic): let guardrails, tool usage, tracing, and Responses replay see Claude's server calls

Claude's server tool calls moved out of tool_calls into provider_specific_fields.server_tool_calls,
so every reader of tool_calls stopped seeing them. The chat guardrail handler now passes them to
output guardrails and writes rewrites back to server_tool_calls (stream and non-stream), tool usage
and registry discovery count them, otel, Datadog and the Logs Tools panel list them, and a pending
Claude web search or fetch item replayed to /v1/responses goes back to Anthropic as its own
server_tool_use instead of running again

* fix(anthropic): let guardrails and loggers read Claude's server tool calls again

Guardrails, tool policies, tracing loggers and the Responses guardrail
translation read only client tool calls after server calls moved to
provider_specific_fields. They now read server calls first, then client
calls, through one shared helper, and the Responses guardrail treats
Claude's web_search_call items as web_search or web_fetch calls.
Non-streaming splits server calls by block type, as streaming does.

* fix(anthropic): keep Claude server calls visible to guardrails, redaction and tracing

A guardrail rewrite of a Claude server call now also updates its hosted web_search_calls copy,
streamed or not. Redaction with message logging off hides server call arguments and hosted web
call actions on chat and Responses. Pillar and Pangea scan server calls, transform-mode streaming
guardrails keep server call chunks, and OTEL v2 Responses spans list Claude's web calls.

* fix(anthropic): keep the legacy hook view of streamed /v1/messages web search after the main merge

Main's legacy post-call hooks see Claude's server call as tool_use with its search results first. The streamed /v1/messages response now keeps that view while native scans keep server_tool_use.

* fix(guardrails): let Cisco AI Defense scan Claude's web calls on Responses

Claude's web search and fetch now come back from /v1/responses as web_search_call items, so Cisco AI Defense reads and clears their query or URL the way it did for the old function_call arguments. The helpers that read and rewrite those items move to the shared guardrail utils so the Responses guardrail handler and Cisco use one copy.

* test(guardrails): inject Pillar's HTTP client and type the Cisco web call actions

* test(guardrails): type the fake Pillar client like its Protocol

* fix(anthropic): keep a pending server call and its thinking when the turn also waits on a client tool

Only a turn with no client tool_use is a paused turn. A tool_use turn that also started web_fetch or
web_search still needs that call and its thinking replayed, or Claude loses the fetch result

* fix(guardrails): scan replayed Claude server calls and hosted web calls

Request guardrails now see and rewrite server_tool_calls replayed in chat
messages. Cato and Purview scan Claude web_search and web_fetch calls on
Responses output, and a function tool call with an extra custom key is no
longer dropped from Cisco, Purview, Repello and PANW scans.

* fix(guardrails): scan replayed Claude server tool calls in Lasso, Lakera and Qualifire request checks

* fix(anthropic): apply streamed guardrail tool call rewrites to their own block when a server tool call follows

* test: annotate the new server tool call test locals with Final and type their parameters

* test: type the new guardrail test helpers with litellm's tool call types instead of Any

* test: narrow the remaining new test helper parameters from bare dict and object

* fix(anthropic): keep server calls in Responses streams, guardrail masks, OTEL and Logs

Streamed /v1/responses now emits Claude's tool search and paused web search calls, text guardrail masks rewrite replayed server calls, OTEL gen_ai messages and the Logs pretty view list server calls next to client calls, and integration tests read server calls from server_tool_calls

* fix(guardrails): keep hosted web call copies in sync on request masks and denied server calls, and accept null tool_calls in Lakera

* fix(anthropic): serialize masked hosted web call copies through their adapter so a replay before the OpenAI model is built does not 500

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-06 03:54:34 -08:00
Siddharth
2a4f8b1d19
fix(bedrock): drop another model's signed thinking when replaying to a non-Claude Converse target (#50073)
* fix(bedrock/converse): omit reasoning signature for non-Anthropic models (#49969)

* test(bedrock): cover signed reasoning replay in mapped tests

* fix(bedrock): drop another model's signed thinking when replaying to a non-Claude Converse target

A signed thinking block from Claude replayed to a Converse model known not to be Claude, such as GPT-6 Luna or Nova 2, now leaves the request instead of going out as reasoningContent. Bedrock rejects that field on those models, so the whole request answered 400

A target that may still be Claude, such as an application inference profile ARN with no base_model, keeps the signed block, since Claude needs it on a tool-use continuation. Unsigned blocks keep the existing text path

The thinking-block error matcher added earlier on this branch is removed, because Converse and non-Claude Bedrock requests never reach that retry

* fix(bedrock): keep signed thinking for Claude behind a region prefix litellm does not list

ca. and in. inference profiles name Claude after a region prefix get_bedrock_cross_region_inference_regions does not have yet, so they counted as non-Claude and lost their signed thinking. The thinking-only turn test now pins the exact merged and continue-message shapes.

* fix(bedrock): limit the region-prefix Claude match to signed thinking and drop a dropped block's cache point

* fix(bedrock): drop Claude's bridged reasoning items before a Bedrock Responses API replay

* fix(bedrock_mantle): drop litellm-minted reasoning items before a Mantle Responses call

* test(integration): expect Bedrock and Bedrock Mantle Responses to drop minted reasoning

Bedrock and Bedrock Mantle answer 400 on a reasoning item litellm minted, so the
wire test now expects both to drop it, like OpenAI and Azure

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-06 03:15:49 -08:00
devin-ai-integration[bot]
fc8f614a03
test(bedrock_mantle): expect the generic unsupported-param 400 for temperature on fixed-sampling Claude (#50344)
Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-05 21:53:06 -08:00
jesus-berri
18b02380e5
feat(spend-logs): opt-in latest_turn mode that stores only the current turn of stateless requests (#49544)
* feat(spend-logs): opt-in latest_turn mode that stores only the current turn of stateless requests

* fix(spend-logs): keep bridged Responses rows and classifier audit in latest_turn mode, reject unknown values at boot

* fix(spend-logs): drop the recursive call in the latest_turn trim and type the stateless test cases

* test(spend-logs): format the stateless latest_turn cases

* test(spend-logs): wrap the classifier audit fixture

* fix(spend-logs): type the latest_turn trim inputs

* fix(spend-logs): leave the raw request body unvalidated before the latest_turn trim

* fix(spend-logs): trim classifier rows and ignore continuation keys off /v1/responses in latest_turn mode

* fix(spend-logs): take the classifier's originating request from the logging payload and redact a copy of vector store results

* fix(spend-logs): redact the body selected for storage, after the latest_turn trim
2026-11-05 19:41:15 -08:00
ryan-crabbe-berri
9a473fbbb0
feat(proxy): give Codex accurate model entries for every model in /v1/models (#49825)
* feat(proxy): give Codex accurate model entries for every model in /v1/models

* fix(router): read a deployment's upstream model without a not-required key access

* fix(proxy): keep Codex tiers for unpriced deployments and match stock entries by declared provider

A deployment with no token price no longer votes a Codex service tier off,
an undescribed model keeps Codex's default reasoning summary parameter,
a Bedrock deployment naming its provider in custom_llm_provider gets Codex's
stock entry, and Bedrock provider namespaces follow the AWS model id format.

* fix(proxy): import cost tracking and provider helpers lazily in the Codex catalog

lite codex imports the catalog before the SDK is initialized, so importing
cost tracking, the Bedrock helpers or provider resolution at module level hit
a circular import and broke every lite command.

* fix(proxy): drop Codex's fast speed tier wherever its priority tier is dropped

Codex shows /fast for a leftover fast speed tier even when service_tiers
no longer offers priority, so an alias, an empty configured tier list or
a configured list without priority showed a /fast toggle that sends nothing.
One helper now sets service tiers, speed tiers and the default together.

* fix(proxy): give an older Codex a translated entry for models past its version

Codex's own entries name the oldest release that can read them in minimal_client_version. A release older than that can fail to decode the entry, such as a reasoning effort it has no name for, which fails the whole catalog, so it now gets the translated entry, whose levels are trimmed to that release
2026-11-05 19:31:32 -08:00
Kerry Lu
8ac9da84f7
fix(router): drop supports_web_search false deployments for Anthropic web_search_* and Gemini googleSearch tools (#50275)
* fix(router): drop supports_web_search false deployments for Anthropic web_search_* and Gemini googleSearch tools

* test(integration): build the web search filter proxy config without mutating the shared config

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-06 01:58:17 +00:00
devin-ai-integration[bot]
d96da7267b
test(copilot): route direct-token fixtures through authenticated peers (#50307)
Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-05 17:42:46 -08:00
tin-berri
a7f9f892be
feat(skills): add sortable Skills Hub catalog and publication audit (#50195)
* feat(skills): add sortable Skills Hub catalog and publication audit

* refactor(skills): remove redundant publisher table column

* style(skills): apply dashboard formatting

* fix(skills): preserve approval audit and complete hub sorting
2026-11-06 00:57:33 +00:00
Kerry Lu
d295f76c4a
fix(types): define the responses compact call types for during_call guardrails and deployment hooks (#50274)
* fix(types): define the responses compact call types so during_call guardrails and deployment hooks see them

* test: regenerate dashboard api types and skip compact in the azure sdk client sweep

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-05 16:09:49 -08:00
Nate Armstrong
58896e2009
test(router): expect a primary stream's stall to cool its deployment (#50261)
The primary stream's mid-stream stall reaches failure_exits as a mapped litellm.Timeout (408), which cools the deployment on the Rust core. Keep that behavior and update the integration test that expected the Python core's exemption. Fallback hops stay exempt through is_stream_stall.
2026-11-06 00:06:59 +00:00
Kerry Lu
3f13c1ca34
fix(router): write the prompt caching pin when turn_off_message_logging is on (#50276)
* fix(router): write the prompt caching pin when turn_off_message_logging is on

The success event PromptCachingDeploymentCheck reads had its messages redacted in place by
turn_off_message_logging before any callback ran, so the pin was keyed off redacted-by-litellm,
never written, and every follow-up bounced across the group. Callbacks that route on the request
and never log it now opt out of that redaction and get the request messages back, while every
other callback and the spend log keep the redacted payload.

* fix(logging): type the request-messages payload helper as dict[str, object]

The bare dict return grew the LIT001 count past the merge base and left the returned fields unchecked

* test(integration): type the pin rig's Redis client as Redis[bytes]

The untyped generic grew the basedpyright reportUnknownMemberType count past the merge base

* test(integration): quote the Redis[bytes] annotation so the rig collects at runtime

redis.Redis is not generic at runtime, so the unquoted subscript raised TypeError on import

* fix(logging): read the request messages once as object in the exempt payload helper

Two partially unknown reads of self.messages grew the basedpyright reportUnknownMemberType count past the merge base

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-05 15:59:57 -08:00
Mateo Wang
faf4ce3985
fix(responses): preserve native stream spend on guardrail rejection (#50286)
* fix(responses): preserve native stream spend on guardrail rejection

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

* refactor(responses): type deferred logging dispatch once

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

* test(responses): expect guarded rounds on one failure row

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

* style(tests): format guarded billing regression

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

* style(tests): preserve existing MCP unit formatting

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-11-05 15:47:24 -08:00
Kerry Lu
3b20b48473
feat(interactions): forward continuation_token and type requires_continuation so callers can resume a long decode (#50151)
* feat(interactions): continue requires_continuation long decodes with continuation_token hops

A foreground Interactions request stops at Google's per-hop output cap with
status requires_continuation and a continuation_token. litellm now re-posts the
request with previous_interaction_id and continuation_token until the status
moves on, merging the hops into one interaction (last id, every hop's steps,
summed usage with initial_input_tokens from hop 1), for non-streaming and
streaming calls. continuation_token is accepted as a request param, and
requires_continuation is a known status for billing.

* refactor(gemini interactions): validate streaming chunks with model_validate

* fix(interactions): keep the held completion across stream hops, stop on a repeated continuation_token, follow the spec redirect

* fix(interactions): add grounding_tool_count per tool type, stop on any seen continuation_token, keep streaming past a [DONE] frame when a hop is pending

* ci(interactions): point the spec canary at aistudio.google.com, where Google moved the Interactions OpenAPI spec

* test(interactions): scripted Google ends each long decode hop with a [DONE] frame

* refactor(interactions): pass requires_continuation and continuation_token through instead of looping hops in the proxy

The caller drives the continuation, the same as against Google directly. /interactions accepts continuation_token in place of input so a resume request needs no input.

* test(proxy): assert the accepted /interactions bodies pass the required-param check

* fix(proxy): keep the router default fallback for input on /interactions, continuation_token stands in for it

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-05 15:38:44 -08:00
Mateo Wang
4679b9f642
test(copilot): align login guards with direct access tokens (#50285)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-05 15:38:33 -08:00
Nate Armstrong
300db9f01d
test(router): expect no fallback errors header for non-bool include_fallback_errors (#50259)
The Rust core reports fallback errors only when include_fallback_errors is
literally true. The routing and SDK wire tests still expected the header for
truthy non-bool values (1 and a 5 KB string), which the deleted Python core
produced by accident. Every non-bool value now expects no errors, and the
flag still must not reach the provider.
2026-11-05 15:34:08 -08:00
devin-ai-integration[bot]
4cdfaaa334
test(usage): assert per-user entity spend and metadata (#50251)
Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-05 20:16:40 +00:00
devin-ai-integration[bot]
6f9c602650
test(skills): assert patch publication and serialize preference writes (#50252)
Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-05 20:16:15 +00:00
tessellark
f357872c98
test(integration): stop the wire peer from dropping requests whose bytes arrive late (#49889)
* test(integration): stop the wire peer from dropping requests whose bytes arrive late

The scripted wire peer set a fixed 5 s socket timeout, so a request whose
bytes reached it late on a loaded rig was dropped with no reply and the
proxy answered 500 Server disconnected. Remove the timeout and, at
teardown, stop reading on every accepted connection so idle handlers exit
at once. A request still arriving when teardown starts never reaches the
test's respond callback.

* fix(tests): bound wire peer teardown on unread replies and answer complete requests

* fix(tests): poll wire peer reads so teardown keeps queued requests and only cuts stuck calls

* fix(tests): write wire peer replies in pieces so teardown never cuts a client still reading

* fix(tests): give a wire peer reply the whole teardown grace when its write blocked before teardown

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-05 09:24:05 -08:00
joshua-berri
a12a6cd154
feat(accounting)!: make the SpendLogs ledger the default accounting path (#49872)
* fix(accounting): keep one undecodable delivery from blocking the ledger

A request whose metadata carried a 401-digit tokens_saved stored that number in
its delivery payload. The query engine could not decode it when the worker
claimed the batch, so every later delivery on the proxy stayed pending

The worker now claims the payload as text and decodes each row in Python. A row
that still cannot be decoded is parked alone while the rest of the batch
applies. The payload builder also clamps integers to the JSON-safe range, so
caller-supplied counts can no longer reach the engine at that size

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

* fix(accounting): take spend locks in one order so the ledger cannot deadlock

A ledger delivery updates many spend tables in one transaction, so it can
deadlock with any writer that locks the same rows in another order. It took the
team advisory lock after the user and team rows, while team roster writers take
it first

The delivery now takes every team advisory lock before any row, then writes
tables in one canonical order with ids sorted. It also runs under the rollup
lock_timeout, applies 500 deliveries per batch by default, and spreads retries
with jitter

The writers that could still collide with it now follow the same order. The
budget reset cascade, model allowlist rename, global spend reset, bulk user
update, org delete and the delete-by-list paths lock their rows in key order
first, and access group endpoints update teams and keys in one sorted pass

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

* test(accounting): prove a ledger delivery waits instead of deadlocking

Real Postgres tests hold the first lock of a team roster change, an access group
update and a budget reset cascade, start a delivery, and let the other writer
finish. The delivery must wait and then charge once. Before the lock order fix
each case ends in a Postgres deadlock

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

* feat(accounting)!: make the SpendLogs ledger the default accounting path

use_spend_logs_ledger now defaults to true, so every request is charged through
LiteLLM_SpendLogDelivery. Setting it to false rolls back to the in-memory and
Redis spend queues, which stay in place. Tests that cover those legacy writers
pin the setting off, and the rest check the queued charge the ledger stores

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

* test(proxy): let the mocked client open a transaction for key and team deletes

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

* fix(proxy): take team locks first when a model rename rewrites allowlists

A rename locked team rows before user rows while a roster change holds the team
advisory lock and a user row, so the two could deadlock. The rename now takes the
advisory lock for every team that lists the model, sorted, before any row lock.

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

* fix(accounting): drain a backlog in one worker run, one batch per transaction

One run applied a single fresh batch, so at 500 per batch charges fell behind any
load above that rate. The worker now keeps claiming fresh batches while they come
back full, for up to five seconds, and each batch still commits on its own.

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

* fix(accounting): queue only the charge when a request's spend log is suppressed

With logging off for a request, the ledger still queued the full row, so a failed
flush or a shutdown could leave its prompt and response in Redis. The queue now
holds only the fields the charge needs.

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

* style(proxy): format the rename team listing query

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

* fix(accounting): remove expired delivery receipts as fast as the worker applies them

The worker now applies several batches per run but still removed one cleanup batch
of applied receipts, so the table could outgrow its retention window. Cleanup now
repeats while each pass removes a full batch, for up to five seconds.

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

* fix(proxy): retry a model rename when a team starts listing the model mid-lock

The team advisory locks come from a snapshot, so a team that gained the old name
before its row lock was locked without its advisory lock. That attempt now commits
without writing and the next one locks the fresh set of teams.

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

---------

Co-authored-by: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 09:14:10 -08:00
tin-berri
aa7917a3d2
feat(skills): show download counts and downloader history (#50165) 2026-11-05 05:25:08 -08:00
tin-berri
79e63d3e06
feat(skills): add optional manual security scans (#50194) 2026-11-05 01:57:39 -08:00
ishaan-j-berri
639625d679
fix(usage): restore per-user spend breakdown on the user aggregated route (#49464)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-05 01:57:23 -08:00
tin-berri
7e4ad6eb33
feat(skills)!: add portable submissions and admin review (#50160) 2026-11-05 00:56:51 -08:00
tin-berri
0ad5d750d4
feat(skills)!: version published Skills Hub updates (#50152) 2026-11-05 00:19:38 -08:00
devin-ai-integration[bot]
b4a8bc715f
test(translation): expect thinking usage fields on Bedrock responses (#50181)
Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-05 06:41:05 +00:00
devin-ai-integration[bot]
3577aa2d94
test(translation): expect thinking usage fields on Bedrock messages (#50179)
Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-05 06:30:43 +00:00
devin-ai-integration[bot]
74493f39fb
test(translation): expect thinking usage fields on Bedrock chat (#50175)
Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-05 05:59:57 +00:00
devin-ai-integration[bot]
796f751603
test(providers): expect 503 server_is_overloaded for an upstream overload on the responses bridge (#50166)
Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-05 04:40:22 +00:00
joshua-berri
49f56e8b23
refactor(proxy)!: drop the unused accounting event tables (#49923)
LiteLLM_AccountingEvent and LiteLLM_AccountingDelivery have had no reader since
the SpendLogs projection replaced them, so the migration drops them with their
enums. AccountingDeliveryState stays because LiteLLM_SpendLogDelivery uses it

The schema now spells the delivery_id default the way Postgres stores it, which
ends the drift Prisma reported against the ledger migration. The schema check
compares base types only, so an attribute-only change is no longer reported as
a type change

Co-authored-by: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 20:23:30 -08:00
quietjules
5a392b9e5f
fix(bedrock)!: keep Claude's thinking when response_format json_schema is set (#49696)
* fix(anthropic): stop forcing the response_format json tool when Claude will think

A forced tool call skips Claude's thinking. The json_schema emulation forced
json_tool_call unless thinking.type was enabled, so adaptive thinking and the
models that think by default (Opus 5, Sonnet 5, Haiku 5.5, and the always-on
families) lost their thinking on every structured-output request.

AnthropicModelInfo.response_format_tool_can_be_forced is now the one check
the Anthropic, Bedrock Converse (including the native output fallback) and
Databricks paths use. It leaves the tool unforced when thinking is requested
in any form or the model thinks by default, and keeps forcing it when the
request turns thinking off (thinking disabled or reasoning_effort none) on a
model that accepts forced tools.

The new thinking_on_by_default cost map key marks Opus 5, Sonnet 5 and
Haiku 5.5 on the providers that reach these paths, and a miss-only fallback
rule covers unmapped Claude 5 and later ids.

* fix(bedrock): route native output and thinking token counts by whether Claude will think

Bedrock's native output opt-in now uses the same will-think check as the json tool, so Claude 5 models that think by default with no thinking param go native. Converse also asks for /usage/output_tokens_details on those models, so reasoning tokens are reported for them.

* fix(anthropic): tell Claude to answer through the unforced response_format tool

Moves the integration tests that check the forced JSON tool from Claude 5 models to Opus 4.8, since Claude 5 models now keep their default thinking and take the unforced tool or native output

* fix(anthropic): keep open-object schemas on the json tool when Claude will think

Native output closes every object, so an open object came back empty. Also declare thinking_on_by_default in the Rust model catalog and expect the Converse thinking-token field in test_main

* fix(anthropic): hold streamed prose until the JSON tool decides the answer

In json mode with the unforced JSON tool, Claude can write its working before it calls the tool. The stream now holds that text, drops it when the JSON tool answers, and sends it when Claude calls a caller tool or skips the JSON tool, matching the non-streamed reply.

* test(anthropic): build the stream test events without nested comprehensions

* fix(anthropic): keep always-on thinking models unforced, open objects native when thinking, and the tool instruction Claude-only

* test(anthropic): keep main's open-object integration cases and cover the unforced tool on a default-thinking model

* fix(anthropic): treat thinking False as thinking off so the JSON tool stays forced

Cover thinking False on the Anthropic and Converse forcing tests, and pin that a Converse
json_schema stream with the unforced JSON tool never sends Claude's working text as content

* fix(anthropic): drop the thinking False forcing case that completion never reaches

litellm.completion turns thinking False into no thinking param before any route sees it,
so the case was dead on the request path and False keeps meaning the model's default

* test(bedrock): collect the Converse json_schema stream chunks in one assignment

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-05 04:07:11 +00:00
devin-ai-integration[bot]
5c60824024
test(providers): expect 503 server_is_overloaded for an upstream overload (#50154)
Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-05 03:28:30 +00:00
devin-ai-integration[bot]
71fc204495
test(routing): expect 503 server_is_overloaded for an upstream overload (#50155)
Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-05 03:26:45 +00:00
ryan-crabbe-berri
42b4cfc5bf
fix(nvidia_riva): send the api key exactly once by calling riva over grpc directly (#50140)
* fix(nvidia_riva): send the api key once per call by talking to Riva over grpc directly

Drops nvidia-riva-client from the stt-nvidia-riva extra. Depending on its version it sent
the bearer token zero times on plaintext or twice on TLS, so token-checking Riva deployments
rejected every request (#50059). The provider now declares the few Riva messages it uses,
opens its own grpc channel and puts authorization and function-id on each call exactly once

* test(nvidia_riva): run the provider against a real grpc server built from nvidia's own definitions

* fix(nvidia_riva): validate endpointing settings with pydantic before building the riva message

* test(nvidia_riva): check the key is sent once over tls, and drop a comment that restated the code
2026-11-04 18:07:31 -08:00
joshua-berri
906fa7ed28
feat(accounting): add opt-in SpendLogs ledger (#49856)
* feat(accounting): add opt-in SpendLogs ledger delivery

* fix(accounting): preserve missing organization daily spend parity

* fix(accounting): preserve ledger receipts when replaying migration

* fix(accounting): preserve callback windows and tune ledger batches

* fix(accounting): preserve window charges and bound ledger statements

* test(accounting): verify budget callback registration order

* test(accounting): verify captured charges after repeated startup

* feat(accounting): store durable payloads independently of spend logs

* fix(accounting): preserve first delivery and trace ledger writes

* fix(accounting): keep SDK imports independent of proxy tracing

* fix(accounting): honor log retention during batch takeover

* test(accounting): cover retained batch alerts and invalid settings

* test(accounting): verify downgrade command against pending work

* fix(accounting): drain retries without starving fresh deliveries

* test(accounting): cover delivery projection, metrics and window key reads

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

* test(accounting): read delivery metrics without the shared registry

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

* fix(accounting): charge a leftover $0 batch row when its claim fails

A failed batch claim buffers the retrieve's row and reports it charged. At flush the
row conflicted with the pre-upgrade $0 SpendLogs row, so its delivery was rolled back
and the charge never reached the spend tables. The flush now takes that row over in the
same transaction and keeps the delivery.

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

* fix(accounting): trace the batch takeover at flush as a SpendLogs update

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

* fix(accounting): keep local spend logs when SPEND_LOGS_URL has no export client

The export client is never created, so main always writes SpendLogs to the proxy's own
database even with SPEND_LOGS_URL set. The ledger skipped local rows whenever the URL was
set, which dropped those request logs. It now treats a flush as an export only when the
client exists, and batch claims write local rows as before.

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

* fix(accounting): keep every request's charge when its log row is shared, evicted or rejected

- Key non-batch deliveries on a per-request delivery_id so a reused client call id is still
  charged once per request, while buffer replays stay deduplicated
- Persist the redacted api_key instead of an opaque bearer token
- Keep the accounting payload when the spend-log queue evicts a row or Postgres rejects it
- Strip NUL from payloads and skip non-text tags so projection never parks a paid request

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

* fix(accounting): isolate a raw-SQL spend log row Postgres rejects instead of requeueing the batch

A rejected row on the ledger insert path raises RawQueryError, which the exact DataError check missed, so the whole batch was requeued every flush. Data and integrity SQLSTATE classes now bisect like before, while missing tables and deadlocks keep retrying

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

* fix(accounting): keep charging a request whose tag is too long for the daily tag index

A tag past Postgres's btree entry limit failed the DailyTagSpend upsert, and on the ledger path that rolled back the request's key, team and user charges with it. Such tags are now left out of the daily tag rollup only

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

* fix(accounting): store only guardrail billing fields in the accounting payload

The ledger payload copied whole guardrail records, including responses and match details, into LiteLLM_SpendLogDelivery even when spend logs were disabled. It now keeps the id, name, usage and cost fields the projection reads. NUL stripping reuses safe_json_structure instead of a recursive walk

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

* refactor(proxy): move spend payload stamping out of update_database

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

* test(proxy): restore the modules the import-layering guard evicts

The guard popped litellm.proxy.utils from sys.modules for good, so later tests on the same xdist worker patched a fresh module while calling functions bound to the old one, and spend queue tests failed depending on scheduling

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

* refactor(proxy): keep update_database's payload stamping on its own local

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

* refactor(accounting): type guardrail entries and use a public key redaction helper

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

* fix(accounting): keep charging requests whose end-user id is too long for an index

An oversized end-user id failed the SpendLogs insert with SQLSTATE 54000, which was requeued forever, and failed the end-user upsert in the delivery, which parked the key and team charge with it. Class 54 rows are now isolated like other rejected rows, and such ids are left out of the end-user rollups only

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

* refactor(proxy): type the spend log queue snapshot

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

* fix(accounting): leave out any projected row Postgres cannot index or store

Caller-supplied ids and counters, such as a long tag or end-user id or an out-of-range compression count, failed the delivery transaction and rolled back the request's key, team and user charges. One check in project() now drops only the row that cannot be stored, replacing the per-field cutoffs

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

* fix(accounting): keep the guardrail fields compression savings are counted from

Slimming the guardrail records dropped the provider and the tokens_saved figure that Headroom compression savings are read from, and dropped a bare-dict record entirely. The payload now keeps both while still leaving out guardrail content

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

* fix(accounting): cap projected counters so daily totals cannot overflow

Counters such as a caller-supplied compression count fit one by one but overflowed the bigint daily total once summed across requests, rolling back the key, team and user charges in the same transaction. Each counter is now capped at 2^40, far below any total that could overflow

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

* fix(accounting): let a buffered batch charge take over its zero-cost receipt

A batch charge queued after a failed lookup hit the $0 receipt an earlier poll left behind, and ON CONFLICT dropped it. The buffered insert now runs the same receipt takeover as a live retrieve

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

* fix(accounting): read a rescued batch row's spend from its accounting payload

Rows rescued from an overflowing queue keep only the accounting payload, so the buffered takeover never saw their positive spend and left the $0 receipt uncharged

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

* fix(accounting): read budget windows through the delivery transaction

Window seed and existing-row reads used a separate connection while the delivery transaction held one, so with a one-connection pool they waited on their own transaction until it timed out. Inside an enclosing transaction they now share its connection

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

* fix(accounting): retry a transient failure while keeping a dropped row's charge

The rescue swallowed every error, so a database blip right after a row was rejected lost the unpaid charge. Only a rejected row is still logged and skipped, anything else propagates so the flush retries

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

* test(accounting): give delivery projection payloads their charged entities

project() now reads entities only from accounting_entities, so the hand-built
delivery payloads carry them like real stored payloads do.

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

* refactor(accounting): own ledger rows by their payload and key batches on the batch id

Drop the accounting_path stamp and its SpendLogs column. A buffered row that
carries an accounting payload is ledger owned, one without it is legacy, and
spend counters get a plain windows_from_ledger flag. Window seeding tells
applied ledger rows apart by their delivery row.

A ledger batch is plain transaction A keyed by the batch id, so a repeated
completed retrieve conflicts and charges nothing. A $0 retrieve records no
delivery, a batch a legacy retrieve already charged is not charged again, and
the legacy batch path is unchanged from main apart from refusing a batch the
ledger already owns. Batch takeover and its delta bookkeeping are removed.

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

* test(accounting): drop a projection test duplicated while rebasing

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

* refactor(accounting): drop previous_payload now that deliveries are never replaced

Batch takeover was the only writer of previous_payload, so the worker applies
each delivery's payload as stored and the column leaves the migration.

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

* refactor(accounting): drop the ledger downgrade checklist script

The use_spend_logs_ledger flag is the rollback path, so a separate downgrade
gate is no longer needed.

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

* fix(accounting): name the window seed reads so their Prisma span resolves

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

* refactor(accounting): remove the unused accounting event types and protocols

types.py, protocols.py and measurements.py were left from the old event
scaffolding. Nothing outside the package imports them, so they go with their
tests.

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

* fix(accounting): park only deliveries whose own data fails

A delivery that keeps failing on a database outage, lock timeout or deadlock
stays pending and retries with capped backoff, so a recovered database applies
it. Only a data error or an invalid payload parks the isolated row.

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

* fix(accounting): isolate a spend log row the insert statement rejects

The raw SpendLogs insert validates columns and JSON before Postgres sees the
row, so a row that fails there is now bisected out and dropped alone while its
charge is kept, instead of failing and replaying the whole flush.

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

* fix(accounting): carry a job's model window on every completed poll in ledger mode

The ledger keeps the poll whose delivery claims the job, which need not be the
poll that won the limiter's in-memory charge. Every completed poll now carries
the job's window, so the delivery that lands always has it, while the live
model budget is still charged once.

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

* refactor(accounting): leave model-budget windows on the existing window writer

Model-budget windows are written by the model budget limiter rather than
derived from a request's spend, so they stay on main's window queue like live
budget counters do. This drops window capture from the accounting payload,
the deferred limiter registration and the job-window capture, and the ledger
keeps owning key and team budget windows.

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

* fix(accounting): keep a delivery pending when the database is unreachable

Prisma reports a can't reach database server outage as a DataError, so the
worker now checks for an unavailable database before treating a data error
as the delivery's own fault.

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

* fix(accounting): isolate only rows the raw insert cannot encode

Catching every ValueError also swallowed unexpected insert errors that main
re-raises. The raw insert now raises RejectedSpendLogRow for an unknown column
or non-JSON value, and only that is isolated as a rejected row.

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

* fix(accounting): store zero savings when caller data makes them uncomputable

A huge caller-supplied tokens_saved overflowed the savings math inside
build_accounting_payload, which dropped the request's charge in ledger mode.
Savings that cannot be computed are now recorded as zero and the charge lands.

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

---------

Co-authored-by: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:49:15 -08:00
Mateo Wang
7a641e5172
fix(proxy): answer an upstream overload with 503 server_is_overloaded on OpenAI routes (#49993)
* fix(anthropic): answer a 503 as 529 overloaded_error on /v1/messages

Anthropic's error contract has no 503, so a client built against it (Claude Code) treats a 503 from /v1/messages as a plain 5xx and retries it ten times instead of taking its overload path. A Bedrock 503 now leaves /v1/messages and /v1/messages/count_tokens as 529 overloaded_error, for both a JSON answer and a stream that fails before its first byte.

An Anthropic 529 overloaded_error now maps to litellm.ServiceUnavailableError (503) instead of InternalServerError (500), so OpenAI-shaped routes answer it as 503, the status OpenAI documents for an overloaded engine, and router retry and allowed-fails policies key it as ServiceUnavailableError.

* test(proxy): expect overloaded_error on a 503 stream error after a keepalive ping

* fix(proxy): record the 529 the Anthropic routes answer and type the chat stream error frame

The messages route stamps the answered status on the exception before post_call_failure_hook runs, so the spend log row, the OTel server span, the failed-request metric, and alerting record the 529 the client saw instead of the deployment's 503, while the deployment failure metric and cooldown keep the deployment's own status. A chat completions stream error frame names the type its status stands for instead of the null .type ServiceUnavailableError carries. An Anthropic-shaped 503 envelope passes through as 529 overloaded_error through a validated TypedDict, and count_tokens answers every ProxyException as the Anthropic envelope. Five integration tests that pinned 503 on the messages route now expect 529

* test(proxy): expect the top-level Anthropic envelope from count_tokens errors

count_tokens now answers a ProxyException as a JSONResponse carrying the
Anthropic error envelope instead of raising HTTPException, which FastAPI
wrapped in {"detail": ...}. The three legacy error-format tests now read
the response body and check that no detail wrap is left.

* test(proxy): cover the 529 overloaded_error answer across routes, cooldown, and retry budgets

* fix(anthropic): keep Anthropic overloads as InternalServerError and answer 529 from the provider status

An upstream Anthropic 529 stays litellm.InternalServerError (status 500) so
InternalServerErrorRetries, InternalServerErrorAllowedFails, SDK handlers,
and the OpenAI-shaped routes behave as on main. The error now carries
provider_status_code, and the /v1/messages routes answer 529
overloaded_error from it.

* refactor(proxy): unwrap a mid-stream fallback error once instead of recursing for the provider status

* fix(anthropic): keep count_tokens' detail body, end keepalive streams with an Anthropic error event, and log any 529 as an overload

* test(integration): cover the audit round 3 cells and expect 529 on the count_tokens 503 cases

* fix(proxy): re-raise an unchanged stream HTTPException as itself and walk nested mid-stream fallbacks for the provider status

* fix(proxy): walk nested mid-stream fallbacks without recursion

* fix(anthropic): preserve 503s and answer upstream 529s on messages routes

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

* fix(anthropic): answer an upstream Anthropic 529 as 529 on count_tokens

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

* test(anthropic): use integration coverage for keepalive errors

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

* fix(proxy): answer an upstream overload with 503 server_is_overloaded on OpenAI routes

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

* fix(proxy): pass answered overload status to failure hooks

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

* fix(proxy): preserve Anthropic route exception handling

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

* refactor(proxy): bind the transformed failure without rebinding the parameter

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

* test(proxy): observe the overload failure hook through a real ProxyLogging

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-11-04 16:37:55 -08:00
Oliver Yu
733b776403
fix(least-busy): reserve the picked deployment so a burst of requests spreads out (#47484)
* fix(least-busy): reserve the picked deployment so a burst of requests spreads out

The in-flight count only went up in log_pre_api_call, which runs after
selection and the pre-call hooks. Requests selected in that window all saw
the same counts and landed on the same deployment (16 simultaneous
requests over 4 deployments went as lopsided as 12/4/0/0 in the proxy).

When the router selects for a request it is about to send, least-busy now
records a reservation for the chosen deployment in the same synchronous
step that reads the reservations, and log_pre_api_call turns it into the
normal in-flight count. Selections without request kwargs (token counting,
probes) do not reserve. A reservation whose request never reaches the API
call expires after 30 s. The shared and per-worker counts and their
success/failure decrements are unchanged.

* fix(least-busy): prune expired and empty selection reservations

The reservation map kept a key per model group forever, so a wildcard
route with caller-chosen model names grew it without bound. Drop expired
and empty entries on every write.

Signed-off-by: Oliver Yu <tc3oliver@gmail.com>

* fix(least-busy): keep pick reservations in the shared router cache

Selectors that share a router cache dedupe to one registered callback, so
only that instance saw log_pre_api_call. Reservations held on the other
instances were never released and skewed picks for the full TTL. Storing
them in the router cache's in-memory layer lets any selector release them,
and the per-key TTL replaces the full-map rebuild on every write.

Tests now hold the burst with a barrier instead of a sleep and use a manual
clock object.

* fix(least-busy): restart the reservation TTL on every write

InMemoryCache keeps the TTL a live key was first given, so a reservation
key that kept being rewritten still expired 30 s after its first pick and
took the newer reservations with it. Drop the key before each write so the
TTL counts from the latest one, and leave it deleted once it is empty.

Signed-off-by: Oliver Yu <tc3oliver@gmail.com>

* fix(router): count least-busy decisions calls and release reservations on cache hits

Least-busy now reads the model group from litellm_metadata before metadata, so /v1/decisions calls that carry caller metadata are counted in flight and release their reservation at pre-call. A response cache hit releases its reservation instead of decrementing another request's in-flight count, and Responses WebSocket turn admission selects without reserving. The reservation store is reached through a typed protocol.

* fix(decisions): count proxy /v1/decisions calls in least-busy's in-flight totals

* fix(least-busy): keep reservations out of the shared router cache and count requests that start during a Redis read

Least-busy now reads a call's deployment from the routing metadata the router sets, so /v1/decisions logging params stay as on main and failed decisions calls no longer start cooldowns.

* style(router): wrap the least-busy reservation store line

* fix(least-busy): lock the reservation read-and-update so threaded sync calls keep each other's picks

Router.completion from a thread pool and sync success callbacks can read and rewrite a deployment's reservations at the same time, so one thread's pick or release could be lost.

* fix(least-busy): release a pick whose call ends before reaching the provider

A call that fails or returns before the provider handoff (a 400 for an unsupported param, token counting, a proxy-gate rejection without a start log, batch create) now drops its reservation instead of decrementing a count it never added. Router.select picks and batch cancel no longer reserve, since neither makes a call that logs.

* test(least-busy): give callback fixtures a fixed call-start time

The routing-group tests fed least-busy a finished call without the start time litellm stamps before counting it, so the count stayed up after the fix. Fixtures now use a fixed timestamp instead of the clock.

* fix(least-busy): free tier-group picks, stop lookups reserving, and count only started attempts

Reservations are keyed by deployment id, so a complexity, auto, adaptive or
quality router frees its tier pick when the call starts. Each started attempt
is recorded under its call id, and only a recorded start lowers the count when
the attempt ends, so a fallback hop rejected before the provider frees its pick.
Whether a pick reserves now comes from a context variable set by the routed
call sites, so get_available_deployment and the deprecated
async_get_available_deployment no longer hold a deployment, and custom routing
strategies and Router subclasses keep their method signatures.

* fix(least-busy): free a request's unstarted picks when it ends

Each pick now carries its call id, so a start frees only its own call's pick, and the router frees every pick a request never started once that request ends. A fallback hop rejected before the provider no longer leaves its pick reserved for 30 seconds.

* fix(router): free a least-busy pick when a vector store create ends

* fix(router): free least-busy picks of calls made without a call id

* fix(router): free the least-busy pick of a sync text completion

* test(router): use fixed start times in least-busy fixtures

* fix(router): free the least-busy pick of a sync generic call that fails before its provider

* fix(router): read in-flight counts and start attempts under one lock so a busy deployment never looks free

* fix(router): keep pass-through lookups from reserving least-busy picks

* test(least-busy): wait for the helper thread to start before the timed join

* fix(least-busy): keep realtime sessions out of in-flight counts

---------

Signed-off-by: Oliver Yu <tc3oliver@gmail.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-05 00:30:12 +00:00
devin-ai-integration[bot]
81e8d652b0
test(observability): reserve sink port throughout outage (#50100)
Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-04 21:07:21 +00:00
mrinal-berri
6d60bc4929
test(integration): poll for the cached JWT rejection sentinel instead of a single Redis read (#50014)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-04 11:46:38 -08:00
Kerry Lu
b662f3f7a4
feat(decisions): add EmpirioLabs AI as a decisions provider (#49977)
* feat(decisions): add EmpirioLabs AI as a decisions provider

empiriolabs/aplomb-1 now works with litellm.decisions and litellm.systemone, the proxy /v1/decisions and /v1/systemone routes, the decision model guardrail and the Admin UI. Requests go to POST /v1/decisions on the EmpirioLabs AI API with the System One body, keyed by EMPIRIOLABS_API_KEY with an optional EMPIRIOLABS_API_BASE

Chat models keep routing through the existing OpenAI-compatible JSON provider, and a test pins that

* ci: run the EmpirioLabs AI decisions tests in the llms unit shard

* fix: align EmpirioLabs registration with CI gates

---------

Co-authored-by: Adam Dalloul <47503782+Adam-Dalloul@users.noreply.github.com>
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-04 10:41:16 -08:00
joshua-berri
02b8da2f16
refactor: replace accounting event scaffolding with SpendLogs projection (#49843)
* refactor(accounting): replace event scaffolding with SpendLogs projection

* test: preserve spend timestamps without removed capture callback

* fix: guard accounting event removal across schema update paths

* refactor(accounting): retain schema and prepare durable payloads

* fix(accounting): preserve dates in serialized payloads

* fix(accounting): retain schema push compatibility

* fix(accounting): retain span registration for the legacy schema

* test(accounting): remove obsolete capture acknowledgement test

* fix(accounting): date every daily projection row the way the model rollup does

A space-separated startTime kept the whole timestamp as the daily key, while the model usage rollup took the calendar date. Every daily row now uses the same date helper

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

* chore(ui): regenerate dashboard API types after rebasing on main

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

* refactor(accounting): project only from the stored accounting payload

project() now reads entities only from the payload's accounting_entities, so
the raw SpendLogs fallback branches are gone. build_accounting_payload() builds
the metadata once instead of dumping it to JSON and parsing it back. Guardrail
usage dates use a tolerant UTC parse, so a malformed startTime drops only the
guardrail rows instead of failing the whole projection.

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

---------

Co-authored-by: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:37:07 -08:00
yuneng-jiang
b18188dee2
test(e2e): retire the quiet_stack lock and batch the claude_code model registration (#49984)
* test(e2e): retire the quiet_stack lock

quiet_stack held an exclusive host-wide lock for six tests, draining every
other xdist worker while they ran. On the full e2e lane that left 35-38% of
worker time idle. Each of those tests now lives where it can run without
pausing the suite

- the two memory tests move to perf/ behind the perf marker, so the perf lane
  runs them on its own fresh stack. Their refusing deployments use a literal
  key because the perf stack carries no provider keys
- the stored-request size check from the memory test becomes an integration
  test on an owned proxy that stores prompts
- the discount and margin spend-row tests are dropped from e2e. Their
  breakdown assertions join the existing integration pricing test, which
  already owns the discount and margin repricing flow
- the Bedrock guardrail cost test stays in e2e without the pricing reset
  fixture, since nothing else on the stack writes pricing config now
- the MCP semantic tool filter test turns on a proxy-wide setting, so it gets
  a new proxy_wide_config opt-in marker and runs after the main run

* test(e2e): register the claude_code deployments as one batch

The claude_code session fixture registered its 24 deployments one at a time on every xdist worker, and each one waited out the 15 s propagation window on its own, about 6 minutes per worker. ProxyClient.register_models posts the whole batch, polls each model until every replica serves it, then waits out propagation once from the last write. A model that fails to post or never becomes servable comes back as ModelNotRegistered, and an unservable one is deleted, so the fixture keeps reporting per-deployment failures the way it did
2026-11-04 09:55:53 -08:00
Mateo Wang
80b18d165b
chore(proxy): use neutral example names for baggage tag mappings (#50011) 2026-11-04 14:49:55 +00:00
devin-ai-integration[bot]
527d882508
test(responses): isolate cancel wire checks from background polling (#50002)
Some checks are pending
unit / router (push) Blocked by required conditions
unit / rust-bridge-harness (push) Blocked by required conditions
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
Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-04 05:12:01 +00:00
devin-ai-integration[bot]
0150896a50
test(spend): run explained reads within real read transactions (#49996)
Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-04 05:11:23 +00:00
tavren27
b9ad61d4f6
fix(agentic)!: surface a failed agentic follow-up call instead of the pre-loop tool call (#49227)
* fix(agentic): surface a failed agentic follow-up call instead of the pre-loop tool call

* fix(agentic): surface failed follow-ups from the OpenAI chat dispatcher and update pinned integration tests

* fix(agentic): end a capped agentic turn on chat and responses instead of a retried 500

* fix(openai): run agentic hooks after the first call's error handling so a follow-up error keeps its own type and message

* fix(agentic loop): end capped turns on every chat path and bill the first call when a follow-up fails

A turn refused at max_agentic_loops now drops the internal tool calls of every hook that claims calls in it, while a call the client declared survives. The hosted_vllm and openai chat paths now carry the loop depth, fingerprints and max_agentic_loops into the follow-up, so they stop at the cap instead of looping forever.

A failed follow-up call now records the first call's usage and cost on the request's failure log before the error reaches the client. A code interpreter sandbox that is missing or fails becomes the tool call's output for the model to handle, so the turn completes without a retry.

On native streaming /v1/messages a hook or follow-up failure now reaches the client as an Anthropic error event with the mapped type, and compresr declares its retrieve tool so the stream holds it back. Comments and docstrings the branch added against the repo rule are removed

* fix(agentic loop): bill retried attempts and clean up what failed or capped turns return

The router's retry loop now carries each failed attempt's paid first call into the request's billing, so a request whose follow-up keeps failing bills every first call on its failure row, and one that recovers bills the failed attempt's first call on its success row. A failure row no longer adds unpriced estimated tokens on top of attempts that were billed, so its tokens match its spend

A sandbox failure now answers the model with a fixed sentence and marks the code_interpreter_call as failed, without the sandbox's error text. Code interpreter on /v1/responses with Claude now runs its loop, because the bridged inner call no longer strips the hook's own markers

Dropped internal tool calls leave the kept calls numbered from 0, also on a replayed stream. A capped native /v1/messages stream now says the loop hit its limit and that max_agentic_loops raises it, instead of asking the client to retry

The agentic refusal and hook error logs no longer include the model or the exception text, and the agentic loop helpers take typed messages, params, and read-only fingerprints

* fix(agentic loop): keep a recovered retry's cost breakdown on the response it returns

A retried agentic request that recovers no longer adds the failed attempt's first call to its success row. The proxy keeps the follow-up's own row for that request id, so the added spend never showed up in the stored row, and that duplicate row is tracked separately. When every attempt fails, the failure row still bills each attempt's first call

Pricing a first call for the failure row no longer replaces the request's cost breakdown. Before, a recovered retry answered with x-litellm-response-cost-original, -input, and -output from the failed attempt's first call. Now its headers and logged breakdown match a request that succeeds on its first try

* fix(agentic): stop retrying a request whose agentic follow-up failed and bill every round it served

An error raised by an agentic follow-up after the first call was served now carries a typed mark, so the
router's retry loops, the mid-stream retry, and litellm's num_retries wrapper raise it as is instead of
rerunning the whole request. A failure of the first call itself still retries, and fallbacks still run.

The failure row bills every round the loop served before the failure, at any depth, and a later attempt
that succeeds bills what the failed attempts paid on its success row, with the cost breakdown and the
response cost headers adding up. The error response's x-litellm-response-cost reports the same amount
its failure row bills. Failure rows outside agentic hooks keep the prompt-token estimate they had before.

A refused tool call now matches by id among the refused ids, or by name among refused calls that carry
no id, so a name-only refusal next to an id refusal is no longer kept.

* fix(code_interpreter): delete a session sandbox whose code run failed so the next call starts a fresh one

* fix(code_interpreter): delete only the session sandbox that failed, never its replacement

* test(code_interpreter): type the session sandbox test helpers

* fix(proxy): report what a failed agentic follow-up's failure row bills on /v1/messages errors

* fix(agentic): keep the no-retry mark through error mapping and bill only failed follow-ups on success

An agentic loop error that is not a provider error, such as the streaming chat cap or a crash in a hook's
plan build, now keeps its no-retry mark when litellm maps it to an APIConnectionError, so the router no
longer reruns every paid round. The mark is stored on the exception, so a built-in error can carry it too.

A crash in a hook's plan build on /v1/messages and /v1/responses bills the first call on its failure row.

A recovered attempt adds a failed attempt's usage to its success row only when that attempt was a failed
agentic follow-up. Other failed attempts, such as a dropped stream, log their own row as on main.

A code interpreter sandbox that fails while running the code now says the code may have run but its
result is lost, instead of saying the code did not run.

* test(responses): bill a retried MCP request each attempt on its own row again

Round 4 stopped adding a non-agentic failed attempt to the success row, so this test goes back to main's assertions: the success row bills only its own rounds and the failed attempt's first round stays on its own failure row

* fix(spend): name a failed follow-up attempt the success record already bills

With the usage meter on, a failed agentic follow-up attempt gets its own charge-eligible record, and the request's success record also carries that attempt's usage and cost once the success row bills it. The success record now lists the attempt's record ids in included_attempt_ids, so a reader can tell the success record already covers it. Nothing changes when the meter is off

* fix(spend): count a billed failed follow-up's tokens on the success row

* fix(spend): bill a failed follow-up on a fallback served from cache

* fix(spend): show a billed failed follow-up in a cached fallback's cost breakdown

* fix(spend): keep a failed follow-up's billing state off the request-merged call details

* fix(spend): record the cost breakdown of a successful agentic follow-up

The chat agentic loop returned the follow-up response with the cost its own internal call had already stamped, so the request's logging reused that cost and never recorded cost_breakdown on the spend row. The loop now drops that stamped cost unless the follow-up was served from the cache, and the request prices the response itself

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-04 02:00:33 +00:00
Mateo Wang
d5ca88900c
fix(anthropic): answer an upstream Anthropic 529 as 529 overloaded_error on /v1/messages (#46063)
* fix(anthropic): answer a 503 as 529 overloaded_error on /v1/messages

Anthropic's error contract has no 503, so a client built against it (Claude Code) treats a 503 from /v1/messages as a plain 5xx and retries it ten times instead of taking its overload path. A Bedrock 503 now leaves /v1/messages and /v1/messages/count_tokens as 529 overloaded_error, for both a JSON answer and a stream that fails before its first byte.

An Anthropic 529 overloaded_error now maps to litellm.ServiceUnavailableError (503) instead of InternalServerError (500), so OpenAI-shaped routes answer it as 503, the status OpenAI documents for an overloaded engine, and router retry and allowed-fails policies key it as ServiceUnavailableError.

* test(proxy): expect overloaded_error on a 503 stream error after a keepalive ping

* fix(proxy): record the 529 the Anthropic routes answer and type the chat stream error frame

The messages route stamps the answered status on the exception before post_call_failure_hook runs, so the spend log row, the OTel server span, the failed-request metric, and alerting record the 529 the client saw instead of the deployment's 503, while the deployment failure metric and cooldown keep the deployment's own status. A chat completions stream error frame names the type its status stands for instead of the null .type ServiceUnavailableError carries. An Anthropic-shaped 503 envelope passes through as 529 overloaded_error through a validated TypedDict, and count_tokens answers every ProxyException as the Anthropic envelope. Five integration tests that pinned 503 on the messages route now expect 529

* test(proxy): expect the top-level Anthropic envelope from count_tokens errors

count_tokens now answers a ProxyException as a JSONResponse carrying the
Anthropic error envelope instead of raising HTTPException, which FastAPI
wrapped in {"detail": ...}. The three legacy error-format tests now read
the response body and check that no detail wrap is left.

* test(proxy): cover the 529 overloaded_error answer across routes, cooldown, and retry budgets

* fix(anthropic): keep Anthropic overloads as InternalServerError and answer 529 from the provider status

An upstream Anthropic 529 stays litellm.InternalServerError (status 500) so
InternalServerErrorRetries, InternalServerErrorAllowedFails, SDK handlers,
and the OpenAI-shaped routes behave as on main. The error now carries
provider_status_code, and the /v1/messages routes answer 529
overloaded_error from it.

* refactor(proxy): unwrap a mid-stream fallback error once instead of recursing for the provider status

* fix(anthropic): keep count_tokens' detail body, end keepalive streams with an Anthropic error event, and log any 529 as an overload

* test(integration): cover the audit round 3 cells and expect 529 on the count_tokens 503 cases

* fix(proxy): re-raise an unchanged stream HTTPException as itself and walk nested mid-stream fallbacks for the provider status

* fix(proxy): walk nested mid-stream fallbacks without recursion

* fix(anthropic): preserve 503s and answer upstream 529s on messages routes

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

* fix(anthropic): answer an upstream Anthropic 529 as 529 on count_tokens

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

* test(anthropic): use integration coverage for keepalive errors

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-11-03 17:37:09 -08:00
mrinal-berri
7fa438df06
feat(auth): add link-only JWT mapping and remove route eligibility filtering (#49956)
* feat(auth): allow skipping JWT key route eligibility check

* feat(auth): support link-only JWT key mapping

* fix(auth): model JWT mapping rejection as a failure value

* feat(auth): select existing JWT keys without route filtering
2026-11-03 16:24:51 -08:00
Kerry Lu
ce73cf8b9d
chore(cost-map): remove the 52 rows whose provider retired them before 2026 (#49974)
* chore(cost-map): remove the 52 rows whose provider retired them before 2026

* test(utils): pin the proxy function-calling parity case to azure/gpt-4.1

* test(batches): price the Azure batch-logging regression case against gpt-4.1

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-03 16:23:06 -08:00