Commit graph

966 commits

Author SHA1 Message Date
Kerry Lu
e21de0e418
fix(logging): bill each SDK call its own native Bedrock guardrail trace when calls share one metadata dict (LIT-10239) (#48923)
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-28 15:11:26 -07:00
yucheng-berri
c6ca22ebc5
fix(guardrails)!: make hide-secrets honor skip_tool_message_in_guardrail (#48689)
* fix(guardrails): make hide-secrets honor skip_tool_message_in_guardrail

With the flag on, per guardrail or in litellm_settings, hide-secrets leaves
role: tool messages and Anthropic tool_result blocks as sent, as the
unified guardrails do. Tool call arguments are still masked.

* test(guardrails): type the skip-tool walker test and assert the whole request
2026-10-28 14:49:48 -07:00
devin-ai-integration[bot]
4516a251c9
feat(accounting): add accounting contracts, PricingSnapshot and ledger schema (LIT-10193) (#48862)
* feat(accounting): add typed contracts and spend log adapters

* refactor(accounting): reuse spend identity constants and register tests

* refactor(accounting): remove unused spend log conversion API

* feat(accounting): add PricingSnapshot, delivery enums and projection dimensions

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

* feat(db): add LiteLLM_AccountingEvent and LiteLLM_AccountingDelivery ledger tables

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

* feat(accounting): export JSON schema and semantic fixtures for the Rust cost crate

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

* refactor(accounting): drop the schema and fixture export, the Python package is the contract

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

* test(accounting): make tests/unit/accounting a package

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

* fix(accounting): validate pricing snapshots and ledger rows

* fix(otel): register the accounting ledger tables as span tables

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

* fix(accounting): address review findings on dimensions, token subsets, multipliers and agent target

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

---------

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-28 14:39:25 -07:00
moe-berri
cf7c35a117
fix(anthropic): map model_context_window_exceeded stop reason to length (#48874)
* test(anthropic): cover model_context_window_exceeded finish reason on chat and responses

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

* fix(anthropic): map model_context_window_exceeded stop reason to length

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

* fix(rust): mirror model_context_window_exceeded finish reason mapping

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-28 13:48:37 -07:00
Mateo Wang
9007e0fa70
fix(files)!: scope provider and model file lists to the caller's managed files under require_managed_files (#47682)
* test(files): cover owner-scoped file listing under require_managed_files

* fix(files): list only the caller's managed files on every list route under require_managed_files

* fix(files): narrow owner-scoped managed file lists to the named provider or model and page with after_id and before_id

* fix(files): narrow scoped lists the way uploads route, and refuse lists that cannot be scoped

A model name now matches every deployment that serves it, wildcards and
aliases included, and a provider matches deployments by the provider
uploads resolve, so anthropic/* and unprefixed Claude models count. The
OpenAI and Azure lists also return the caller's pass-through uploads.
An unknown provider and different after and after_id cursors answer 400,
and a list with no matching deployment skips the database read.

* fix(files): match stored files by model name and keep flag-off lists as on main

Under require_managed_files, a narrowed list now matches a file's model
mappings by deployment id or model name, so files LiteLLM stores in its own
database show on their model and provider lists. The Anthropic list defaults
to 20 rows when no limit is sent, after_id, before_id and the cursor 400s
apply only under the flag, and an admin keeps the provider list when the
managed files hook is not loaded. List errors are returned as values and
mapped to HTTP errors in one place.

* fix(files): keep admin provider lists and index the owner-scoped walk

Under require_managed_files, an admin keeps the provider list as on main, and
only a non-admin list reads the caller's own and team managed files. A stored
file now also shows on the provider its model name routes to through a
wildcard, with exact model names first, and a list routed to a model takes the
default page size of the provider serving it. A new index on created_by and
created_at keeps each chunk of the walk an index read, and the model check
runs before the file object is parsed.

* fix(files): filter provider and model lists in the query

A non-admin list scoped to a provider or model now asks Postgres only for the
caller's rows that map to one of that scope's deployments or models, or that
live in file storage, instead of reading every owned row and filtering them in
Python. A caller with many files on other providers no longer waits for the
whole set to be read. Team members' files are matched in the same query.

* test(files): type the fake raw query in the files list tests

* refactor(proxy): list managed files through one typed database handle

* fix(files): list alias and routing-group storage uploads on their provider
2026-10-28 13:42:22 -07:00
zoveki
41fcce62ff
fix(gemini)!: accept Google's current Managed Agents fields on create and interactions (#48750)
* fix(gemini): send Google's current Managed Agents fields on create and forward agent_config on interactions

* fix(gemini): accept id on managed agent get, delete, and list_versions

* fix(gemini): drop unsupported agent fields under drop_params and keep agent_config keyword-only

A model interaction drops agent_config under drop_params, agent create drops wrongly typed optional fields under drop_params and otherwise answers 400 naming the field the caller sent, and agent_config on interactions create is keyword-only so positional response settings stay in place

* fix(gemini): drop a non-object agent_config under drop_params and name the id alias the caller sent

* fix(interactions): reject agent_config on bridged providers and drop a typeless one under drop_params

Bridged providers answer 400 for agent_config without drop_params and drop it with drop_params. A Gemini agent run drops an agent_config without a string type under drop_params. The Gemini agents wire test expects Google's current upstream field names.

* fix(interactions): honor additional_drop_params for agent_config on bridged providers

* fix(agents): drop a clashing instructions alias under drop_params

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 12:37:59 -07:00
moe-berri
365d3d3729
feat(decisions): support Ollama decision models (#48871)
* test: cover local Ollama Decisions wire contracts

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

* fix(decisions): support ollama providers

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

* test(decisions): account for Ollama model-info requests

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-28 12:35:26 -07:00
tessellark
c9a5777739
fix(azure): share one Entra token request per worker and audit #47348 (#48769)
* test(azure): audit #47348

Add proxy integration tests for #47348's cached Entra token providers and
off-loop token refresh, and fix the token herd the audit found: once the
fetch moved to a worker thread, concurrent cold requests each fetched their
own token. Every cached bearer token provider now serializes its calls so
the first fetch fills the cache and the rest reuse it

* fix(azure): build each cached Entra token provider once under concurrent first calls

lru_cache does not hold back concurrent misses, so worker threads that miss together each built their own credential and token provider, and each fetched its own token. A lock around each provider cache makes the first caller build it and the rest reuse it

* fix(azure): share Entra token fetches only while no valid token is held

Concurrent callers now share one result (or one error) from the cold
fetch, while callers inside the refresh window keep the still-valid
token. The oidc/azure provider build and the client-assertion exchange
are single-flighted per audience, and the shared helpers no longer call
subscripted generic aliases, which crashed on Python 3.10.

* fix(azure): refresh Entra tokens once while other requests keep the valid token

MSAL treats a token with less than five minutes left as expired, so every
request in that window fetched a new one. Now one request refreshes while
the others keep using the still-valid token.

Requests that waited on a fetch that failed retry once through a fresh
fetch instead of all failing with the first error.

Credentials that only implement get_token keep working, and OIDC token
exchanges are cached per scope.

* fix(azure): bound Entra token waits during an outage

Requests waiting on a failed token fetch now retry only when the fetch
before it succeeded, so during an Entra outage later bursts fail after one
fetch instead of two.

Requests during a refresh only get the held token while it has more than
60 seconds left, and a failed refresh falls back to that token instead of
failing its request.

* fix(azure): share one retry per failed Entra token fetch and raise on a failed refresh

Waiters of a failed fetch now retry the flight they joined, so a fast-failing Entra makes one retry instead of one per waiter. A failed refresh raises again instead of returning the held token, which Postgres token auth read as a fresh one. The DefaultAzureCredential fallback reuses one provider per scope.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 12:24:20 -07:00
tessellark
a0c9a5993b
fix(batches): stop unbillable batches from blocking file deletes and new-batch polling (#48752)
* test(batches): cover the batch cost poller gaps left after #47190

* fix(batches): keep unbillable batches from blocking file deletes and new-batch polling

The file delete check now ignores batches past MANAGED_OBJECT_STALENESS_CUTOFF_DAYS, a provider 404 for the output object retires the batch, each poll page takes the arrival lane first, a batch expires only after the poller visits it past the cutoff, and the compare-and-swap matches batch_processed instead of updated_at

* test(batches): cover the full arrival page and a failed expiry write in the batch cost poller

Old batches still get polled every other cycle while new batches fill the page, old batches that fill a page never push a new batch out after boot, and a failed stale_expired write no longer stops the rest of the page from being polled

* fix(batches): share the poll page between old and new batches and hold output files until billed

Each lane now gets at least half of the poll page, a lane with nothing left gives its half to the other, and a lane read that resumed mid-lane wraps to the start, so neither old nor new batches can starve the other.

The delete guard applies the 7 day cutoff to input files only. Output and error files stay locked until the batch is billed or retired, and stale_expired rows no longer lock anything.

Only a lasting provider failure expires a batch past the cutoff, so a timeout or a 5xx no longer gives up on a batch that may still bill. Past the cutoff a lasting failure, a lasting cost-tracking failure, or an output file id that never appeared retires a finished row instead of polling it forever.

* fix(batches): retry refused finished batches past 7 days, expire unfinished ones on a failed retrieve

A finished batch the provider refuses after 7 days (rotated key, bucket 403, a 404 that does not name the batch) now keeps retrying so its spend is written once the config is fixed, instead of retiring unbilled. A batch the provider still reports running keeps polling. A batch that is not finished in the database becomes stale_expired once a retrieve fails after 7 days, so an unreachable provider is not polled forever. An S3 NoSuchKey for the output object now counts as gone.

* fix(batches): expire an unfinished old batch only after 3 failed retrieves in a row

* fix(batches): keep retrying an unfinished old batch through timeouts, 429 and 5xx

* fix(batches): wait 3 polls before expiring an old batch whose output id lags

* fix(batches): match a resource id in a 404 message followed by punctuation

* fix(batches): keep refusal and pending-output expiry streaks apart, and void a pending-output retirement on a client write

* fix(batches): give up on unsettled batches past twice the staleness cutoff

Replace the per-reason refusal and pending-output streaks with one give-up age, and inject a clock into the poller and the delete check so tests pin exact age boundaries

* fix(batches): keep polling finished batches past the give-up age so their spend stays recoverable

* test(batches): expect a restarted poller's first page to split between old and new batches

* test(batches): pin the delete-guard tests to a fixed clock and type a new journal list

* test(batches): type the provider helper and the storage 404 cases

---------

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

Resolves LIT-10233

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-28 12:13:56 -07:00
tessellark
43ce3e22aa
fix(responses)!: answer 400 before counting a lone Responses built-in tool on Claude (#48798)
* test(responses): audit #47363

Adds integration cells for the input token count paths #47363 changed: the provider id of a managed file and the 403 for another team's file on OpenAI, file_search and additional_tools parity plus cache-mark stripping and untranslatable tools on Claude, and the Bedrock runtime count leaving out the options it refuses.

* fix(count-tokens): reject a lone tool with no chat equivalent before the Claude count call

A Claude input token count whose only tool was mcp or computer_use_preview skipped the pre-call check and went to the provider, so the caller got the provider error instead of the 400 naming the tool. The check now runs whenever a tool would get a placeholder name.

* fix(responses): count a lone Anthropic toolset and name the uncountable tool

The lone-tool 400 now fires only for OpenAI Responses built-in tool types, so a nameless Anthropic toolset still counts. The error says LiteLLM cannot count that tool type for the model. Adds a Bedrock unit cell for the lone-tool 400 and pins the runtime wire test's tool shape per request type.

* fix(responses): take the uncountable tool types from the OpenAI SDK and keep the region hint off LiteLLM's 400

The Responses built-in tool types now come from openai's ToolParam union, so a type the SDK adds is refused before the count call too. The AWS region hint no longer lands on an error LiteLLM raised before calling the provider. Tests cover two more lone types and the hint.

* fix(responses): keep LiteLLM's own list of uncountable tool types and pass a non-string tool type through

* fix(responses): count tool_search and namespace tools the way Claude chat inference sends them

The count refused a lone tool_search with LiteLLM's 400 and sent a lone
namespace raw or squashed it into an empty tool, while Claude chat
inference maps tool_search to Anthropic's tool search tool and unpacks
a namespace into its function tools. Both now go through the
deployment's own map_openai_params, so the count sends what inference
sends, and tool_search leaves the list of uncountable types.

* fix(bedrock): count a Claude request with its deployment's own chat translation

Bedrock invoke and Mantle inference turn a chat tool_search tool into Bedrock's
tool search tool, but the Bedrock counter always translated with the Anthropic
config, so the count answered 400 for a request inference serves.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 12:12:45 -07:00
rj-cairn
ff5bf4826d
fix(count_tokens)!: audit #47441 and keep the framed estimate for Bedrock count models from the cost map (#48805)
* fix(count_tokens): keep the framed estimate when a region-less token_count_model cannot count (audit #47441)

* fix(count_tokens): let framing stand in for a shipped token_count_model, keep a configured one's error

The framed local estimate now replaces a 400/404 from any count model the cost map ships, Mantle-pinned rows included, and an admin-set token_count_model keeps its error as the docs say.

* fix(count_tokens): warn with the region when framing stands in for a pinned count model

Test a Mantle row #47441 pinned in its own region, and keep the region hint check on an admin-set count model.

* fix(count_tokens): name the deployment in the framing stand-in warning

* fix(count_tokens): let framing stand in when a cost map count model's region denies the account

A count model the cost map pins to a region can answer 401 or 403 when the
account has not enabled that region or lacks Mantle count access there. With
framing set, the estimate now stands in, as it does for 400 and 404. An admin
set count model and a count model with no pinned region keep the error.

The warning now names the model group, the count model and its region, and
says the proxy falls back to the estimate.

* test(count_tokens): cover framed 401 and 403 from a shipped count model region on the wire

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 19:11:35 +00: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
moe-berri
208ef99b88
fix(images): honor max_retries and prevent Router retry amplification (#48705)
* test(images): cover SDK retry budgets and router ownership

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

* fix(images): forward max_retries to the OpenAI image generation client

Per-call, deployment and default_litellm_params max_retries were filtered
out before reaching the OpenAI-compatible image handler, which then always
used 2 SDK retries. Forward the caller's value, and pin it to 0 for routed
calls so the Router stays the only retry owner, matching completion()

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

* fix(images): read image max_retries through a typed kwargs view

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

* test(images): assert the public invalid retry error mapping

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

* test(images): preserve the invalid retry status code

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

* test(images): group the proxy test imports

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

* test(images): discard proxy retry test routers

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>
Co-authored-by: Ericz <yonglzh@gmail.com>
2026-10-28 10:46:47 -07:00
shrey
0f49db5c3b
fix(proxy): store boolean settings as JSON booleans on /config/field/update (#48595) 2026-10-28 17:39:01 +00:00
moe-berri
3d7b94b34f
fix(ollama): forward embedding credentials, headers and timeout (#48707)
* test(integration): cover ollama embedding key, header and timeout forwarding

* fix(ollama): forward api_key, headers and timeout on native embeddings

Co-authored-by: Mrun25 <165297479+Mrun25@users.noreply.github.com>

* test(ollama): disable fixture key for keyless proxy guard

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Mrun25 <165297479+Mrun25@users.noreply.github.com>
2026-10-28 09:32:47 -07:00
moe-berri
4ce2602548
fix(huggingface): stop forcing truncate=false on TEI rerank and pass truncate params through (#48706)
* test(huggingface): cover TEI rerank truncate passthrough and server default

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

* fix(huggingface): stop forcing truncate=false on TEI rerank and pass truncate params through

Co-authored-by: Luke Schaefer <225965040+luke-e-schaefer@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(huggingface): keep TEI truncate options provider-specific

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

* test(huggingface): use MockTransport for rerank truncation

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>
Co-authored-by: Luke Schaefer <225965040+luke-e-schaefer@users.noreply.github.com>
2026-10-28 09:27:26 -07:00
ryan-crabbe-berri
db5b940143
fix(vertex_ai): send Vertex Claude labels as X-Vertex-AI-Labels header instead of the request body (#48753)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-28 15:41:50 +00:00
zoveki
38b4d31447
test(scim): audit #47544 (#48791)
* test(scim): audit #47544

Add integration cells for the SCIM group delete that now runs through the team delete's bulk path: an admin group delete revokes its members' admin key, a delete sent with the IdP's SCIM token revokes member keys and audits them under that token, a delete that times out on a locked member row changes nothing so a retry works, and two racing team deletes write one tombstone and one team audit row

* test(scim): read the racing team delete's audit rows after the proxy drains them

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 06:08:30 -07:00
tavren27
356069d2ed
test(mcp): audit #47422 (#48821)
Adds a real-proxy case where the MCP follow-up call fails on every router retry, checking that the one failure row bills the first call of every attempt on chat completions and /v1/responses

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 06:02:40 -07:00
norkelbyte
3507781735
test(proxy): audit #47352 (#48804)
* test(proxy): audit #47352

* test(proxy): audit #47352 lazy load during a cold docs build

* test(proxy): route the overlapping chat cell to the rig upstream

* test(proxy): keep the root path cell from rewriting the packaged UI

* test(proxy): prove the lazy and chat cells overlap the cold docs build

* test(proxy): warm the chat path before the overlapping chat cell

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 05:54:40 -07:00
quietjules
ca24b396da
test(nvidia_riva): audit #41021 (#48801)
* test(nvidia_riva): audit #41021

Adds an owned Riva ASR gRPC server and provider integration cells for the riva_offline request-body ban, the offline and streaming paths, the per-deployment caller opt-in, and the 413 and 408 error mappings

* test(nvidia_riva): check word timing requests and audio samples in the Riva double

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 05:24:08 -07:00
tavren27
826859588c
test(router): audit #41368 (#48794)
* test(router): cover silent_model shadow fan-out and streaming end to end

Proxy cells check that each silent_model target gets one request in the
caller's stream mode, that the caller sees only the primary, and that every
shadow lands its own spend row under its own model group. SDK cells check the
sync Router.completion thread path streams each shadow and fires its success
callback.

* test(router): flatten the spend row lookup with itertools.chain

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 05:14:59 -07:00
norkelbyte
93056ef939
test(proxy): audit #47358 (#48784)
* test(proxy): audit #47358

Add integration cells for pass-through bodies and settings-endpoint mappings across workers

* test(proxy): wait for every worker to hold the saved mapping before probing

* test(proxy): compare passthrough bodies to the client body and probe each worker on its own connection

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 04:55:44 -07:00
norkelbyte
64d545ba7c
test(router): audit #47376 (#48792)
* test(router): audit #47376 batch create routing and supported_endpoints listing

* test(router): make the batch mode cells deterministic and assert concurrent creates

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 04:48:30 -07:00
tavren27
889d8aec15
test(decisions): audit #47508 (#48777)
* test(decisions): audit #47508

* test(decisions): price the Databricks decision cell from its cost-map row

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 04:38:21 -07:00
mika-lt
9546f9a69f
test(router): audit #47498 (#48799)
Pin the queue route contracts #47498 changed: the priority header on streamed answers, the 400 for a request with no priority to queue at, a provider bad request keeping its own status, and a deployment-id request served ahead of the queue

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 04:36:50 -07:00
paveln83
7f44cff274
test(anthropic): audit #47478 (#48803)
Covers the json_schema envelope edge cases #47478 fixed on the Anthropic json tool path: a key listed only in
`required`, a tool-named wrapper on a stream, non-ASCII and lone-surrogate answers, a stream cut before the tool
block closes (proxy and sync SDK, Anthropic and Bedrock Invoke), and a string `output_format` with and without
`drop_params` on Anthropic and Vertex chat and /v1/messages. The scripted Anthropic peer now refuses a non-object
`output_format` the way Anthropic does

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 04:34:21 -07:00
simonp9
e838957b1d
test(token_counter): audit #47586 (#48800)
* test(token_counter): audit #47586

* test(token_counter): check streamed usage equals the local token count

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 04:30:19 -07:00
zoveki
53c6939ad4
test(anthropic, bedrock): audit #47489 (#48782)
* test(anthropic, bedrock): audit #47489 thinking replay across Claude routes and Bedrock Converse

* test(bedrock): compare the whole replayed Converse turn in the merged-message and cache point cells

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 04:24:50 -07:00
zoveki
5a18e6b6da
test(files): audit #40621 (#48788)
* test(files): audit #40621

Pin the raw cloud storage file id gate on /v1/files content and delete against a running proxy: twice-encoded gs:// ids stay blocked for non-admin keys, proxy admin keys read s3:// objects, and ordinary provider ids keep working

* test(files): check the full SigV4 signature on the admin raw s3 read

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 04:13:03 -07:00
m6atlas
e2d78b69cd
test(proxy): audit #46815 (#48768)
* test(proxy): audit #46815

Add integration tests for every count route #46815 changed, and count a Claude
subscription caller's tokens with their forwarded token when the deployment has
no configured key, which #46815 turned into a 401

* test(proxy): accept the forwarded subscription token in the count endpoint mocks

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 03:55:25 -07:00
rj-cairn
6ba77b7013
test(spend): audit #47353 (#48785)
A DeepSeek V4 stream that ends without a usage block now has a spend-row cell proving its prompt_tokens are counted with DeepSeek's tokenizer, the one path #47353's own cells did not reach

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 03:50:50 -07:00
norkelbyte
91db681532
test(responses): audit #47504 (#48787)
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 03:50:24 -07:00
tessellark
de40c0aa39
test(llms): audit #47535 (#48786)
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 03:38:21 -07:00
tavren27
5dc18fe909
test(proxy): audit #47418 (#48766)
* test(proxy): audit #47418

* test(proxy): hold the stream open past its first chunk and require cost headers in the audit cells

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 03:23:34 -07:00
norkelbyte
d84a2d4452
test(spend): audit #47815 (#48774)
Adds an integration cell proving a failed /v1/responses request counts its input from the newest compaction item on its failure spend row

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 03:04:11 -07:00
Mateo Wang
94e4ebd63d
test(proxy): pin the max_parallel_requests slot release when a streamed /v1/messages MCP auto-execute ends (#47788)
* test(proxy): cover the max_parallel_requests slot a streamed /v1/messages MCP auto-execute keeps

A streamed /v1/messages request that auto-executes MCP tools replays its answer
as a stream after its model rounds already logged success. The stream path frees
the request's max_parallel_requests slot only on a client disconnect, so after
message_stop the slot stays taken and the key's next request answers 429

The test streams such a replay to the end and expects the slot to be free. It
fails until the fix lands

* fix(proxy): free the max_parallel_requests slot when a replayed /v1/messages MCP stream completes

A streamed /v1/messages request that auto-executes MCP tools runs its model
rounds without streaming and replays the final answer as a stream. The first
round claims the request's only success log, and its limiter callback keeps the
slot because the tool call reused it afterwards. The stream path released the
slot only on a client disconnect, so after message_stop the key stayed at its
max_parallel_requests limit and its next request answered 429

Stream cleanup now releases the stashed slot when the stream completed and its
logging object already claimed the success log, which only happens when the
stream replays a response that was logged before it started. A real stream
still releases through its own success callback

* test(mcp): free the key's only parallel slot after a streamed auto-execute ends

Run a streamed MCP auto-execute on a key with max_parallel_requests 1 on
every LLM surface, then send the next request on that key and expect 200.
On main both /v1/messages surfaces answer 429 here, while /v1/responses and
/v1/chat/completions already pass

* test(proxy): keep only the MCP stream regression tests, since #48628 already frees the slot at stream end

* test(proxy): require the MCP stream regression test's response to stream and reach its end event
2026-10-28 02:47:27 -07:00
zoveki
d8a43a7cd3
test(gemini): audit #47542 (#48758)
* test(gemini): audit #47542

Proxy integration cells for the Gemini and Vertex default output budget: the env budget reaches Google on chat and responses, turns on long decode, yields to the caller's own limit, caps per deployment, skips resumes, and a camelCase two-candidate request is never continued

* test(gemini): annotate the budget wire module's pytestmark as Final

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 02:42:45 -07:00
rj-cairn
000d4f6bb3
test(sagemaker): audit #47495 (#48767)
Checks that a SageMaker stream failing after its first chunks reaches /v1/chat/completions, /v1/messages and /v1/responses callers as an error with no success end event, and that each writes a failed spend log row

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 02:40:45 -07:00
norkelbyte
1176e608b1
test(harness): audit #47530 (#48751)
Check that cancelling a LocalSandbox run stops the grandchild process it started, using a real child process

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 01:57:41 -07:00
Shivam Rawat
f1795e6d44
fix(responses): keep every MCP tool round in streaming follow-up input (#47698)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: ryan-crabbe-berri <ryan@berri.ai>
2026-10-28 00:51:10 -07:00
Kerry Lu
7acdf9f00d
fix(anthropic): count search results and reject unsupported token-count blocks (#48699)
* fix(anthropic): count search results and reject unsupported token-count blocks

Co-authored-by: Prasanna Jain <prassu017@gmail.com>

* fix(anthropic): avoid a Final assignment inside the token-count loop

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

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Prasanna Jain <prassu017@gmail.com>
2026-10-28 02:33:38 +00:00
devin-ai-integration[bot]
1171273511
fix(langfuse otel): emit request metadata under langfuse.observation.metadata and langfuse.trace.metadata on the OTEL v2 Langfuse mapper (#42697)
* fix(langfuse_otel): emit request metadata under langfuse.observation.metadata and langfuse.trace.metadata keys

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

* fix(langfuse_otel): name trace metadata identity keys after their request metadata fields

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

* fix(langfuse_otel): type metadata helper and drop null fields so they cannot clobber caller trace metadata

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

* test(langfuse_otel): annotate new request metadata tests

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

* test(langfuse_otel): build rig config and wire batches immutably

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

* fix(langfuse_otel): skip empty trace identity values so teamless requests cannot erase attribution

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

* test(langfuse_otel): drain wire batches per poll instead of accumulating

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

* test(langfuse_otel): cover null stripping and empty trace identities

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

* test(langfuse_otel): set redaction flag directly instead of monkeypatching

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

* test(langfuse_otel): add integration audit matrix for langfuse metadata keys

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

* test(langfuse_otel): tighten audit chaos and unauthenticated cells

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

* test(langfuse_otel): restore contracts.json formatting

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

* test(langfuse_otel): close SDK clients, pin worker identity, and normalize response id matching

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

* test(langfuse_otel): gate the slow sink reply instead of sleeping

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

* fix(otel v2): emit request metadata and identity under langfuse.observation.metadata and langfuse.trace.metadata

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

* fix(otel v2): satisfy type discipline gate for metadata export

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

* test: drop prohibited covers markers from langfuse otel integration tests

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

* fix(otel): put the mutable-ok suppression on the dict construction line the type-discipline gate reports

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

* fix(langfuse otel): share request metadata export across callbacks

* fix(langfuse otel): drop prompt variables, MCP tool I/O and vector store queries from exported request metadata

* fix(langfuse_otel): keep span export alive when nested request metadata has non-string keys

The content strip validated prompt_management_metadata, mcp_tool_call_metadata
and vector_store_request_metadata items as str-keyed mappings, so an SDK
caller passing one with a non-string key raised inside
exported_request_metadata: OTEL v2 dropped the LLM span for every vendor and
v1 langfuse_otel lost the request metadata and every attribute after it.
Validate those nested mappings with object keys instead. Also hoists the
langfuse_otel imports.

* fix(langfuse_otel): send the request metadata blob to Langfuse destinations only

- A requester_metadata that is not a mapping no longer raises. The v2 LLM span
  (every vendor) and the v1 Langfuse attributes survive it again.
- Prompt-quoting routing_decision fields stay out of langfuse.observation.metadata.
- An exporter or key/team destination for any other backend drops
  langfuse.observation.metadata and langfuse.trace.metadata.user_api_key_*, as on
  main. This covers a collector folded into the Langfuse logger.
- The v2 Langfuse key-set pin in the arize family test gains the two new keys.

* fix(langfuse_otel): export only caller identity under the Langfuse metadata keys

- The Langfuse metadata keys carried the whole request metadata. On OTel v2 that
  sent request headers, caller metadata, user email, user agent and auth metadata
  to a Langfuse destination that never received them on main; on v1 it doubled
  each span's metadata payload, so big caller metadata doubled the export size
- Both paths now export only the key alias, user, end user, team, team alias and
  org. On v2 that is the team plus the identity among the operator's
  `baggage_metadata_keys`, which v2 spans already carry as `litellm.metadata.*`.
  Everything else stays in the bare `metadata` attribute, as on main
- `output_messages` returns a list again, and the arize v2 b3 key pin is back to
  main's set

* fix(langfuse_otel): drop the caller identity first when a span hits its attribute limit

v1 wrote the identity attributes after the OpenInference ones, so a span near the
128-attribute limit (about 33+ tools) evicted openinference.span.kind, metadata
and llm.model_name instead. The identity now goes first on a fresh span, so the
SDK evicts it before anything else, and a caller's active span that already
carries attributes does not get it.

v2 sheds the Langfuse identity keys before any indexed message attribute when a
span is over budget, so a span keeps every attribute it carried without them.

* fix(langfuse_otel): keep other spans and dropped counts as main exports them

- v1 writes the caller identity only into the room the span's attribute
  limit leaves next to the error attributes a failed call adds, so the
  exported dropped-attribute count matches main
- v2 strips the Langfuse request metadata only from LiteLLM's own spans;
  application, instrumentation and server spans keep every key they set
- a stripped v2 span keeps the dropped attribute, event and link counts
- a config that lists the Langfuse mapper without the langfuse_otel
  exporter also keeps the request metadata away from other exporters

* fix(langfuse_otel): keep the caller identity off the caller's active span

v1 writes LiteLLM's attributes onto the caller's active span when one is
current instead of starting its own span. That span goes to the caller's
own exporter, so the identity keys reached a non-Langfuse collector and
could take attribute slots the caller fills later. The identity keys now
go only on the span LiteLLM starts, whatever the caller's span carries.

* fix(langfuse otel): keep the identity keys on the OTEL v2 Langfuse mapper only

The v1 langfuse_otel callback goes back to main: it sends no
langfuse.observation.metadata or langfuse.trace.metadata.user_api_key_*
keys. The OTEL v2 Langfuse mapper keeps them. The integration cells now
run every endpoint, client, redaction, failure and chaos case on v2.

* test(langfuse otel): count only accepted batches in the sink outage cell

The wire also records the batches the sink answered 503, so an exporter
retry made a span look exported twice. The chaos cells now also check the
spans that landed carry the caller identity.

* test(langfuse otel): split stacked span comprehensions and drop constant comments

* test(langfuse otel): split the metadata integration module into focused files

* fix(langfuse otel): send the org id in trace metadata too

The trace metadata keys now reuse IDENTITY_METADATA_FIELDS, so a key in an org gets langfuse.trace.metadata.user_api_key_org_id next to the generation's org id.

---------

Co-authored-by: yucheng <yucheng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 18:54:10 -07:00
mrinal-berri
86dd7187fa
fix(tavily): return raw_content, score, favicon and the response envelope from Tavily search (#48684)
* fix(tavily): return raw_content, score, favicon and the response envelope from Tavily search

Tavily search dropped every field except title, url and content, so
include_raw_content, include_answer, include_images and include_favicon
reached Tavily but their output never reached the caller. Each result now
carries raw_content, score and favicon when Tavily returned them, and the
top-level fields (answer, images, request_id, usage, ...) come back as
provider_metadata, same as the Exa fix in #47423

* test(tavily): move the Tavily search wire test's sample data to module constants, like the Exa test
2026-10-27 18:52:06 -07:00
Kerry Lu
017e08aba3
fix(databricks): price the openjev decision model at its published 0.6 DBU per 1M input tokens (#48694)
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-27 18:49:16 -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
tin-berri
8ced2cf433
fix(router): keep session tier pins coherent across workers (#48645) 2026-10-27 18:22:39 -07:00
yuneng-jiang
21ab2cb87b
test: update stale integration and guardrail e2e tests to intended behavior (#48665)
* test(integration): update stale expectations to intended provider behavior

Each expectation moved because a merged PR changed behavior on purpose.
- chatgpt string input is wrapped as one user message (#41522)
- Gemini 2.5 tool media sits beside function_response (#48428)
- sampling knobs move to Gemini 3.5 Flash, which keeps them (#47645)
- a body base_model is stripped without allow_client_pricing_override (#47639)
- ai_decide answers without usage return usage null (#48261)
- azure_ai forwards mapped client anthropic-beta values (#48033)
- the OpenCode scripted upstream mints a fresh id per request

* test(e2e-ui): type the guardrail name before deleting it

GuardrailDeleteModal requires the exact name before Delete enables since
#45893. All three guardrail delete flows now go through one helper.

* test(observability): accept any sampled traceresponse flags and expect a 400 for a non-object body

OpenTelemetry 1.45 (#48055) sets the W3C random trace id flag on new traces,
so the gateway's traceresponse now ends in -03. The producer copies the server
span's own trace flags, so the tests now accept any two-digit flags with the
sampled bit set instead of pinning -01

#48438 made a non-object JSON body a 400 "Invalid JSON payload: expected an
object" on purpose, so the [1] body case is renamed non_object_body and
expects that 400

* test(observability): stop waiting on OpenTelemetry's export log line in the langtrace outage test

The outage test waited for "Exception while exporting Span batch" in the proxy
log, which is OpenTelemetry's wording and no longer appears with 1.45. The test
keeps asserting what the gateway guarantees, callers unaffected during the
outage, a live gateway, and every span after recovery delivered once with no
outage span duplicated

* test(guardrails): count the Responses disconnect spend rows #47611 added

Since #47611 a streamed /v1/responses disconnect writes its own spend row, so
the concurrent disconnect test saw 20 rows where it counted 10 by model group.
It now reads each row's call type and expects ten acompletion rows with the
outage verdict and ten aresponses rows with success. /v1/messages disconnects
still write no row on main, so they are left out of the count rather than
pinned as absent

The responses-httpx case of the single disconnect test no longer needs its
LIT-8603 skip and passes

* test(observability): size K4's timeout from the harness boot budget

K4 boots three two-worker proxies in a row. On CircleCI each boot takes 43 to
58 seconds, so a passing run took 179.97s against its fixed 180s cap and 6 of
30 runs hit pytest-timeout. The cap and the restart gate wait now come from
readiness_seconds and graceful_stop_seconds, like other multi-boot tests
2026-10-28 01:11:25 +00:00
yucheng-berri
15eca24fc2
fix(guardrails)!: redact secrets in tool traffic and system prompts (#47122)
* fix(guardrails): redact secrets in tool traffic and Anthropic system

* fix(guardrails): bound tool payload scans and stop nested tool_result recursion

Scan each tool value beside its key instead of an indented dump, read
tool_result content one level deep, keep non-finite tool arguments as
raw text, and give read-only guardrails one fragment per tool payload

* test(guardrails): cover read-only guardrail consumers of tool and system text

* fix(guardrails): keep tool argument parsing, scanning and tests within bounds

Treat tool arguments that decode to a lone surrogate as raw text, decode
JSON-escaped detections back to the leaf value, quote unkeyed leaves
without repeating a parent key per list item, drop cycle tracking that
parsed JSON cannot reach, and use the CI license instead of a fake
license server

* fix(guardrails): scan list items beside a capped parent key

List items keep their parent key in the scanned JSON line, capped to its
last 64 characters, so a credential key over a list is still detected
while a long key over many items no longer multiplies the scan text

* fix(guardrails): cap the scanned key after JSON escaping

A key of control characters escaped to six times its length after the
cap, so every list item's scan line grew with it

* fix(guardrails): collapse whitespace in the scanned key context

Space-heavy keys made every inherited list item scan several times slower
than a short key

* fix(guardrails): build each key's scan context once per payload

Repeating the collapse, escape and cut for every list item made a long key
over many items cost key length times item count

* fix(guardrails): cover Responses tool call items and instructions

Responses carries the same tool call arguments and system prompt as chat
and messages under function_call.arguments, custom_tool_call.input and
instructions, so secrets there reached the provider unscanned

* perf(guardrails): skip rescanning text hide-secrets already found clean

Agents resend their whole conversation on every turn, so each turn
rescanned every earlier tool result. Each guardrail now keeps a bounded
set of digests of scan inputs that held no secret and skips those; text
with a secret is always rescanned and no secret material is kept

* fix(guardrails): size the clean-scan cache for many sessions and replace longest secrets first

4096 digests held about 20 long agent sessions per worker, so interleaved
sessions evicted each other and every resend rescanned its history;
65536 digests (about 10 MB) hold a few hundred. Tool payload secrets are
now replaced longest first after JSON unescaping, so a shorter overlapping
match cannot leave part of a secret behind. The key-context test checks
the work it bounds instead of wall-clock time, which CI could not meet

* fix(guardrails): keep unmasked tool argument text and redact keyword matches cut inside an escape

* fix(guardrails): write a tool visitor's whole payload when it changes more than string values

* fix(guardrails): read message text before tool and system text so a vendor error on the new text cannot skip it

* fix(guardrails): moderate tool and system text in its own call so a long payload cannot hide a flagged message

* fix(guardrails): skip the empty moderation call when only tool or system text is present

* fix(guardrails): scan long multi-line tool strings as their own lines so the scan stays linear

* fix(guardrails): keep the key beside every line of a long multi-line tool string

* fix(guardrails): confine tool-traffic coverage to hide-secrets

Restore iter_message_text to its main behavior, so banned keywords, Azure
Content Safety, IBM detectors and the OpenAI and Google moderation hooks
read exactly what they read on main. Only walk_user_text, which
hide-secrets alone calls, still reaches system, instructions and tool
payloads.

* test(guardrails): stop the tool-traffic integration tests from flaking on a busy host

Use the shared per-case timeout for the rig client, and open a new
connection per call in the worker-kill burst so a pooled keep-alive
socket to the killed worker is never reused.

* test(guardrails): drop the wall-clock check from the deep tool arguments unit test

The integration test test_deeply_nested_tool_arguments_do_not_stall_the_worker
already covers the timing; the unit test keeps the redaction check.

---------

Co-authored-by: yucheng-berri <293926326+yucheng-berri@users.noreply.github.com>
2026-10-27 18:11:03 -07:00