Commit graph

56120 commits

Author SHA1 Message Date
Devin AI
d4205f01ce fix(auth): match Python Unicode word regex semantics 2026-11-09 02:57:00 +00:00
Devin AI
28eb490f93 feat(auth): fence native key fills and prove cross-worker storage 2026-11-09 02:35:30 +00:00
Devin AI
3a17272195 feat(db): complete auth reads and validate pool lifecycle 2026-11-09 02:29:48 +00:00
Devin AI
e5c28a5b1f feat(auth): complete native effect reads and preserve spend IO fallback 2026-11-09 02:25:01 +00:00
Devin AI
e00029bd69 feat(auth): add bounded Moka cache and native invalidation subscriber 2026-11-09 02:22:48 +00:00
Devin AI
9d477ad9b4 feat(auth): own native database pools in proxy lifecycle 2026-11-09 02:16:47 +00:00
Devin AI
684d945bcf feat(db): add bounded SQLx auth reads and native pools 2026-11-09 02:15:03 +00:00
Devin AI
dd55f2c292 feat(auth): route effect reads through the native database handle 2026-11-09 02:13:41 +00:00
Devin AI
ebf40cc8d3 refactor(auth): wire native Claude normalization into request auth 2026-11-09 02:10:43 +00:00
Devin AI
3dffc683d0 refactor(auth): move Claude normalization into native read stages 2026-11-09 02:10:43 +00:00
Devin AI
77979758cd refactor(auth): delegate shared Codex normalization to native stages 2026-11-09 01:49:49 +00:00
Devin AI
145413a01e refactor(auth): port Codex model and turn policy to typed native stages 2026-11-09 01:44:51 +00:00
Devin AI
f6204e95c0 test(auth): register budget lookup exports in bridge surface 2026-11-09 01:42:28 +00:00
Devin AI
4020155d72 refactor(auth): stage model budget aliases in native core 2026-11-09 01:41:03 +00:00
Devin AI
52075f734f refactor(auth): preserve tool and vector access read order natively 2026-11-09 01:36:16 +00:00
Devin AI
4297255514 refactor(auth): replay native policy on current main 2026-11-09 01:17:40 +00:00
Devin AI
bc363397cc feat(auth): add the Rust AuthCore decision boundary 2026-11-09 01:06:50 +00:00
tavren27
2f430873ae
fix(bedrock): derive the web identity session policy per AWS caller (#50592)
* fix(bedrock): derive the web identity session policy per AWS caller

Each AWS caller (Bedrock embeddings, batches, files, SageMaker, Polly, S3 Vectors, and the S3 and SQS loggers) now sends its own STS session policy on AssumeRoleWithWebIdentity, so a role that allows those actions is no longer capped by the Bedrock-only policy. The default Bedrock policy is unchanged, and the credential cache key includes the policy so callers never share a narrower session.

* fix(bedrock): let async invoke write its S3 output under the web identity session policy

Bedrock writes an async-invoke job's output with the caller's session, so the embedding session policy now allows s3:PutObject only when Bedrock makes the call (aws:CalledViaLast bedrock.amazonaws.com).

* fix(opensearch): give OpenSearch vector store search its own web identity session policy

* fix(bedrock): give every remaining AWS caller its own web identity session policy

Secrets Manager, knowledge base ingestion, model listing, the S3 config file reader, the SageMaker chat handler, and the Comprehend Medical and Transcribe pass-throughs each send a session policy with only the actions they call

Bedrock embeddings now use the Bedrock policy, so chat and embeddings share one STS session. That policy adds async-invoke status, S3 reads and writes only through Bedrock, KMS only through S3, and Mantle token counting

bedrock:InvokeModel gets its own statement without aws:SecureTransport, since Bedrock checks a Nova Sonic realtime stream as InvokeModel with that key false

KMS grants are limited by kms:ViaService, file downloads get a read-only S3 policy, and batches and speech config export skip underscore attributes

* test(batches): expect config export to skip every underscore attribute

The session policy class attribute is private, so batches config export no longer returns single-underscore names

* fix(bedrock): scope knowledge base ingestion role grants to the roles it creates

Knowledge base ingestion only creates and passes litellm-bedrock-kb-* roles, so its session policy no longer lets the session create, edit, or pass any other role, the proxy's own included

* fix(bedrock): give embeddings and the agent-runtime pass-through their own web identity session policies

* fix(bedrock): grant enforced guardrails and the remaining agent-runtime operations over web identity

Every web identity ceiling that grants bedrock:InvokeModel now also grants bedrock:ApplyGuardrail, so an
account or organization enforced guardrail no longer denies embeddings, the agent-runtime pass-through,
or realtime

The agent-runtime ceiling grants every POST operation the pass-through forwards, adding flow executions,
document content, and ingested document ACLs, and a test derives that list from botocore

Only the realtime caller gets bedrock:InvokeModel without the aws:SecureTransport condition, since chat and
embeddings never open a bidirectional stream

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-08 19:13:53 +00:00
paveln83
141a580951
fix(proxy)!: price expanded wildcard /model/info rows as the model their calls route to (#49128)
* fix(proxy): price expanded wildcard /model/info rows as the model their calls route to

* fix(proxy): read wildcard routing through the router registry entry points

* fix(proxy): price a wildcard row only when the router sends its name there

Use the router's own route candidates for the listed name, so an alias, a routing group, a more specific pattern, or a team's own deployment keeps a wildcard row unpriced

* fix(proxy): show no price on a wildcard row whose calls go to another deployment

* fix(proxy): keep the listed price on catch-all wildcard rows and match the edit path to the read path

* fix(proxy): price a wildcard row aliased to a sibling name as the alias target

* fix(proxy): price wildcard rows through the caller's alias renames and show nothing for rows another deployment takes

* fix(proxy): keep same-model wildcard row prices, skip deleted team aliases, use the caller's team

* fix(proxy): keep live team aliases when skipping deleted team alias targets

* fix(proxy): show no wildcard row price when the same-model deployment serving it sets custom rates

* fix(model_info): recognize a replayed wildcard row on a worker that has not loaded the wildcard

* test(proxy): feed wildcard discovery real provider data in model info tests

* fix(proxy): price a wildcard row another deployment serves as the model its calls bill

A row whose calls reach another deployment now shows that deployment's model
and limits, and goes blank only when those deployments bill more than one model
or declare their own rate. A name no route serves keeps its own price, and a
replayed row stores none of the metadata it showed.

Drop the router's per-call scan of every deployment for a team key, since the
team and name indexes already cover those names.

* fix(proxy): price a wildcard row aliased to an aliased group as the group its calls reach

An alias target that is itself aliased onward listed the onward group's deployments too, so the row showed no price while calls billed the first target's model. The billed model now comes only from the route candidates

* fix(proxy): price a wildcard row billed by another deployment without the wildcard's base_model

A row aliased to another deployment priced as that deployment's model, but the lookup still swapped in the source wildcard's base_model, so the row showed a rate its calls never bill

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-08 19:08:22 +00:00
zoveki
abee9c945b
fix(ui): label every budget reset time in the Admin UI with UTC (#50591)
* fix(ui): label every budget reset time in the Admin UI with UTC

Budget reset times rendered in the browser's timezone with no label, while the proxy resets budgets on UTC boundaries by default. Every budget reset time and budget period now renders in UTC with a UTC label, and both keys tables' Budget Reset column widens to fit it.

* fix(ui): keep the key page's 12-hour clock for the UTC budget reset time

* fix(ui): show clock times in a budget period that does not start at midnight UTC

* fix(ui): show the UTC clock time in the Teams list reset line when it is not midnight

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-08 16:30:31 +00:00
zoveki
cfddaafe18
fix(cli): end the local login when lite logout cannot check the OS keychain (#50098)
* fix(cli): end the local login when logout cannot check the OS keychain

With no usable keyring, lite logout kept a secret-free token.json as a note
that the keychain went unchecked. Every reader took that note for a live
keychain-backed login, so lite whoami, lite auth print-token, and the bare
lite shell all reported a login the user had just ended, and a keychain that
answered again could pair the note with an earlier login's entry and sign
the machine back in.

Logout now removes token.json whatever the keychain answers. When the
keychain could not be checked and there was a login to end, it records that
in a separate owner-only keychain_unchecked file beside it, written after
the removal so a full disk still has room for it. A repeat logout still
warns while that file is there, and a logout that confirms the keychain is
clean removes it.

* fix(cli): keep warning after a logout the keychain refused to clear

A keychain that will not give up its entry gets the same note as one that
could not be checked, so a later logout made while the keychain is out of
reach does not report a clean logout while the credential is still there.

* fix(cli): revoke the login a logout left in an unreachable keychain once it answers

A logout made while the keychain was locked ended the local login and dropped the metadata that pairs with the refresh token still in the keychain, so the next logout had nothing to revoke. The not-cleared note now keeps that record without its secret, and a later logout pairs it with the keychain entry and revokes it before erasing.

* fix(cli): revoke a locked-keychain login on the next login, and only once after a refused erase

A login made after a logout that could not reach the keychain now revokes the
login it replaces, read from the keychain_not_cleared note, as the merge base
did through the kept token.json. A keychain that refused the erase had already
handed that logout the refresh token, so its note no longer pairs with the entry
and the next logout does not revoke the same token again.

* fix(cli): keep the first login revocable after a second locked-keychain logout

A logout whose login kept its secret in token.json never reached the
keychain, so it now leaves the note an earlier logout wrote in place, or
writes an empty one, instead of overwriting it. The first login's refresh
token stays revocable once the keychain answers, and a keychain-less
machine no longer keeps user and team ids in the note.

Also documents the limits that remain in the CLI README and drops the
docstrings from the tests this PR added.

* fix(cli): revoke the keychain's login after a login made while the keychain was locked

* fix(cli): stop lite from reading the stored login before login and logout

The cli group resolved the stored API key before every command, and that read moved a
locked-keychain login's secret over the keychain entry the logout note names, so logout and
login never saw the first login to revoke it.

* fix(cli): keep the login a locked logout left in the keychain revocable

While the keychain still holds the login an earlier logout could not
end, reading the stored login now serves the secret token.json kept and
leaves that keychain entry alone, whatever the login times say. A clock
that stepped back no longer serves the ended login, and commands other
than logout and login no longer move the newer secret over it. A silent
renewal that writes over the entry revokes that login on the proxy.

* fix(cli): keep a login revocable when its note cannot be written, and never block logout on an earlier login's proxy

A locked logout whose note cannot take the login's record now puts token.json back, so the next logout that reaches the keychain still revokes that login, as the base did. A logout that ends both a current login and an earlier one left in the keychain warns when the earlier login's proxy cannot record the revocation, as lite login does, instead of refusing to log out of the current proxy.

* fix(cli): move a locked keychain login's token file into its note instead of putting it back

* fix(cli): never tell a logout to delete the record of a keychain login it could not move aside

A read-only ~/.litellm made the locked-keychain logout say to delete token.json, which would leave the keychain's login unrevoked. It now says the logout did not happen and asks to make the directory writable. The README also says an older lite cannot finish a logout this release started.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-08 11:17:59 +00:00
joshua-berri
43112547fb
feat(proxy): total_timeout bounds a whole non-streaming request (#50496)
* feat(proxy): total_timeout bounds a whole non-streaming request

A `total_timeout` body field or `x-litellm-total-timeout` header (the header wins) gives a non-streaming
/v1/chat/completions, /v1/messages or /v1/responses request one budget for preparation, retries,
moderation and post-call processing. Past it the proxy answers 408 and cancels the upstream call, and
a retry whose backoff cannot fit ends early with the error it would have retried. A body value on a
streaming or background request is refused with a 400, a header value leaves it unbounded. A timed out
request is billed like a client disconnect.

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

* fix(proxy): ignore any total_timeout header on requests it cannot bound, and never interrupt pre-call admission

A globally configured x-litellm-total-timeout that is malformed no longer fails the streaming and
background requests it is ignored on. The deadline holds while pre-call admission runs, since admission
reserves shared quota before it records it, and an expired deadline then ends the request before
dispatch, where the failure path hands the reservation back.

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

* fix(proxy): a total_timeout cuts slow pre-call hooks short, holding only the rate limit reservation

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-08 00:43:50 -08:00
Mateo Wang
7ea4028625
fix(proxy): align ownership and scope handling (#50600)
Some checks failed
unit / ui-unit (push) Waiting to run
unit / docs (push) Waiting to run
unit / helm (push) Waiting to run
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 / 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 / rust-bridge (push) Waiting to run
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 / 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
Cost map pins recheck / rerun-cost-map-guard (push) Has been cancelled
LiteLLM Rust DB / db-queries (push) Has been cancelled
2026-11-08 07:03:36 +00:00
devin-ai-integration[bot]
cb7c3a65c6
test(observability): match the full card number instead of a 4111 substring that timestamps can contain (#50612)
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-08 06:01:49 +00:00
devin-ai-integration[bot]
e58354f055
fix(gemini): add the Flex above-200k cache read price to gemini-3.1-pro-preview rows (#50607)
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-08 04:32:42 +00:00
dingdangmao
c61ec51108
fix(key/info): return 400 instead of 500 when no key is provided (#43577)
* fix(key/info): return 400 instead of 500 when no key is provided

When GET /key/info is called without a ?key= query parameter and the
authenticated principal has no virtual key (e.g. an admin-UI session
token whose UserAPIKeyAuth.api_key is None), info_key_fn passed
hashed_key=None to Prisma find_unique(where={"token": None}), which
raised MissingRequiredValueError and surfaced as an unhandled 500.

Add an explicit None/empty guard that raises a clean 400 ProxyException
before the DB lookup, and add a regression test asserting that prisma
find_unique is never called with token=None.

Fixes #43571

Signed-off-by: apex-mochen <2756823972@qq.com>

* test: remove redundant assertion comment

* fix(key/info): remove redundant None check to satisfy basedpyright delta gate

After if not key: raise, key is narrowed to str. The subsequent
if key is not None: guard is always true and triggers basedpyright
reportUnnecessaryComparison, which fails the delta gate.

Collapse to a single hash_token_if_needed(token=key) call.

Signed-off-by: apex-mochen <2756823972@qq.com>

---------

Signed-off-by: apex-mochen <2756823972@qq.com>
Co-authored-by: ryan-crabbe-berri <ryan@berri.ai>
2026-11-08 02:49:34 +00:00
joshua-berri
82e43cd55a
fix(accounting): stop delivery metrics from scanning retained receipts on every flush (#50565)
* fix(accounting): stop delivery metrics from scanning retained receipts on every flush

The backlog query now reads only pending and parked deliveries, so its cost follows the backlog instead
of retained receipt history. Batch count and table size are measured once per check interval per pod,
and the storage and delayed-delivery alerts repeat at most once per interval instead of every flush

Fixes #50475

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

* test(accounting): prove the storage query reruns only when its interval is due

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

* fix(accounting): read the check interval on each flush so YAML environment_variables apply

The writer is built at proxy import, before the config's environment_variables load, so reading
the interval there ignored values set in config.yaml

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

* test(router): wait for router accounting instead of sleeping in user-config limit tests

Each request ended with a 0.3s sleep and hoped the router's usage writes had landed, so under CI load a
concurrent request could read counters mid-update. The tests now flush the logging worker and wait for
every task the requests scheduled, and a retired router's counter flush lands before the next router reads

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-07 18:21:32 -08:00
Kerry Lu
7e6479b3a0
fix(completion): accept a tool_choice that forces an OpenAI hosted tool (#50578)
* fix(completion): accept a tool_choice that forces an OpenAI hosted tool

litellm.completion rejected tool_choice={"type": "web_search"} before any request, while /v1/responses took the same choice. The chat validator now forwards a hosted tool type unchanged and a provider gate turns it into a 400 off openai, azure and litellm_proxy, where the choice would otherwise be dropped or mangled.

Fixes #50566

* fix(utils): keep a non-string tool_choice type a 400, not a TypeError

A list or dict in tool_choice["type"] is unhashable, so the hosted tool lookup needs the string check first. The bridge test that used file_search as an untranslatable choice now uses a type that really is unknown.

* refactor(completion): forward any Responses API tool_choice type instead of keeping a hosted tool allowlist

Chat completions now accepts any tool_choice dict with a string type it has no shape for and sends it as is on openai, azure and litellm_proxy, where OpenAI validates the type itself (a typo is still a 400, from OpenAI). The provider gate keeps the 400 everywhere else. The HOSTED_TOOL_CHOICE_TYPES list is gone, so a new OpenAI hosted tool needs no change here.

* Revert "refactor(completion): forward any Responses API tool_choice type instead of keeping a hosted tool allowlist"

This reverts commit 5a6d5a8e08.

* fix(utils): add tool_search to the hosted tool_choice types

OpenAI lists it among the supported tool_choice.type values (live error on 2026-11-08), alongside the ones already in the set.

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-07 17:54:57 -08:00
Kerry Lu
07c7229895
feat(proxy): per-request header opts a stream into usage.cost without the global include_cost_in_streaming_usage flag (#50574)
* feat(proxy): let one streaming request opt into usage.cost with x-litellm-include-cost-in-streaming-usage

The global include_cost_in_streaming_usage flag moved every stream off the
fast path. A request can now send x-litellm-include-cost-in-streaming-usage: true
to get usage.cost on its usage-carrying stream event (/v1/messages and the
Anthropic and OpenAI pass-throughs) while requests without the header keep the
fast path. The global flag keeps working unchanged.

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

* test: type the new stream cost header tests

* fix(types): keep reportUnknownArgumentType at its merge-base count

* fix(proxy): the stream cost header only accepts true

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-07 16:57:20 -08:00
joshua-berri
3c3f266322
fix(router): a cancelled or retried attempt releases its io-token reservation and least-busy count (#50486)
* fix(router): a cancelled or retried attempt releases its io-token reservation and least-busy count

A routed attempt cancelled mid-call (client disconnect with cancel_on_disconnect) kept its ITPM/OTPM
reservation and its least-busy in-flight count, and retries sharing one logging object leaked the
count of every attempt after the first. Each async attempt now runs under its own attempt id, so the
attempt and its own callbacks release what it holds exactly once, in tasks a cancellation can't stop.

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

* fix(router): settle an io-token reservation once even through a logged copy of its metadata

/v1/messages and /v1/responses stash the reservation in litellm_metadata, which the logging object
copies. A cancelled attempt refunded it, then the proxy's failure logging refunded the copy again.
Each stash now carries an id, and a reservation already settled under that id is not settled again.

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

* fix(router): keep spent tokens counted, refund a previous attempt's stash, and fit every call in flight

A request cancelled after its provider answered now keeps the tokens it spent counted instead of
refunding them. A retry refunds the reservation its previous attempt left behind even though it runs
under a new attempt. The least-busy reservation store holds twice the keys, since each call in flight
now keeps one more.

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

* fix(router): never evict a settled reservation id before its counters' window has passed

Settled ids lived in a capacity-bounded cache, so past 10,000 settlements in two minutes a late copy
of an evicted reservation was refunded a second time. They now leave only when their window expires.

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

* fix(router): start releases eagerly and keep attempt settlement out of router_utils

A release that never waits (an in-memory counter) now finishes before its caller goes on, as it did
when the caller awaited it inline, so a websocket turn's usage is counted by the time its logging is
flushed. Each reservation carries its own id, and the attempt settlement module moves to
litellm_core_utils, since litellm/utils.py may not import from router_utils.

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

* fix(router): count only finished answers as spent, and declare the least-busy redis family

A stream's wrapper returning is not an answer: a stream cancelled before its first chunk now hands
back its whole reservation. Only a non-streaming result marks its attempt as answered. The module
level least-busy shared decrement carries its redis family, as every direct redis caller must.

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-07 16:54:26 -08:00
yuneng-jiang
c024a4d2e3
test(proxy): delete 28 dead skipped tests in test_key_generate_prisma.py (#50550)
57 of the 62 tests in this file are hard-skipped because they need a live
Prisma DB, so they never run. This removes the 20 whose behavior a running
integration or e2e test already asserts, and the 8 that assert nothing
meaningful or can never pass. The imports and the one helper only they used
go with them. The other 29 skipped tests stay as a list of behavior still to
port into tests/integration.
2026-11-07 16:54:23 -08:00
Kerry Lu
432efc9b2d
fix(anthropic): exchange Google identity tokens again on refresh and read oidc/ references out of ANTHROPIC_IDENTITY_TOKEN (#50583)
A Google metadata-server identity token carries no jti, so Anthropic lets it be exchanged more than once. The shared
on-disk token store still answered a mandatory refresh with the stored token minted from that same assertion, which
was the almost expired token the refresh existed to replace. Google references now skip the store, so every refresh
is a real exchange and a Kubernetes token file keeps today's cross-worker reuse.

ANTHROPIC_IDENTITY_TOKEN was always wrapped as oidc/env/ANTHROPIC_IDENTITY_TOKEN, so a value that was itself an
oidc/google/ reference was posted to Anthropic as the literal assertion text. A value starting with oidc/ is now the
reference, with oidc/env_path/ still rejected, and a raw token in the variable resolves exactly as before.

Fixes #50553

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-07 16:48:48 -08:00
yuneng-jiang
c22dc25316
refactor(ui): clear hardcoded English from dashboard JSX (#50575)
* refactor(ui): clear hardcoded English from dashboard JSX

Move the 283 strings the planned no-literal-string guard flags so main
starts at zero:

- missed UI text (Switchyard router form, Create User custom ID field,
  advanced settings placeholders) and our own fallbacks ("unknown",
  seconds suffixes) onto t() with new en.json keys
- product names into src/i18n/untranslated/productNames.ts and code
  samples, sample values and JSON placeholders into
  src/i18n/untranslated/codeSamples.ts
- values that never display (filter ids, enum fallbacks, query keys,
  CSS, ids) onto literal-union types or module constants

The rendered English is byte-identical.

* refactor(ui): put Bedrock policy chips and test panel error on t()

The six Bedrock policy chips and the compliance test panel's error word are our own display labels, not server values, so they go through t() instead of module constants. Rendered English is unchanged.
2026-11-07 16:42:38 -08:00
devin-ai-integration[bot]
7617e66acf
test(translation): expect encrypted reasoning in message responses (#50576)
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-08 00:03:40 +00:00
Kerry Lu
c5ad400f0b
fix(anthropic): refresh the WIF bearer token on the connection error retry (#50570)
* Refresh the WIF bearer token on the connection error retry

* Only refresh the bearer when the retry goes to the host the token was minted for

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-07 16:02:59 -08:00
Kerry Lu
51b49973c9
fix(proxy): hold DB_RECONCILE_LOCK around credential create, update and delete (#50551)
* fix(proxy): hold MODEL_RECONCILE_LOCK around credential create, update and delete

The config reload snapshots the credential table, awaits config rows, then prunes and
upserts litellm.credential_list from that snapshot. A credential write landing inside
that window was undone in memory until the next reload. The three endpoints now take
the same lock the reload holds.

Fixes #50273

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

* fix(proxy): take MODEL_RECONCILE_LOCK before the credential update reads its row

Two PATCHes queued behind a reload read the same old row and the second
overwrote the first. The relay now keeps enough bytes to spot a split
after marker, and the new unit tests are fully typed.

* test(credentials): annotate the reconcile lock test locals with Final

* refactor(proxy): rename MODEL_RECONCILE_LOCK to DB_RECONCILE_LOCK

The lock gates every db-to-memory reconcile and now the credential writes too, so
the model-only name undersold what it protects.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-07 16:02:44 -08:00
yucheng-berri
b83a1f0608
fix(otel): name the routed model on auto-router LLM spans (#50498)
On an auto-router request the LLM span reported the name the caller asked
for (the router or an alias of it) as gen_ai.request.model (OTel v2) and
gen_ai.response.model (v1 and v2). Both now name the model group the router
picked, read from the request's routing_decision. In v2 the span name and the
vendor keys built from the request model (Langfuse, OpenInference, Langtrace,
Weave) follow. Requests that were not auto-routed are unchanged.
2026-11-07 15:52:00 -08:00
Mateo Wang
83e2e5bf99
test(integration): budget proxy restart tests from calibrated boot time (#49761)
Both restart tests boot two 2-worker proxies but capped the case at a fixed
180s, below even one calibrated 2-worker readiness budget. On a loaded runner
each boot took 80-90s, so pytest-timeout fired mid-boot or mid-shutdown

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-07 15:37:18 -08:00
Mateo Wang
0d0bf8f75f
test(integration): give each kill-burst its own prompts so cached control replies cannot stand in for late traffic (#50569)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-07 23:08:23 +00:00
ryan-crabbe-berri
fa254b9346
fix(responses): surface mid-stream upstream errors that arrive without an event type (#50466)
* fix(responses): surface mid-stream upstream errors that arrive without an event type

Bedrock's native Responses route can end a stream with a bare {"error": {...}}
chunk. It parsed as a typeless GenericEvent and crashed the iterator with
AttributeError, so clients saw that instead of the upstream error. Such chunks
are now read as error events, which raise the mapped upstream error and reach
the client as response.failed.

* refactor(responses): inline the untyped error chunk check to keep LIT001 at the base count
2026-11-07 14:53:07 -08:00
yucheng-berri
b603e84c02
fix(mcp): derive MCP bridge and session keys with the FIPS provider's KBKDF (#50032)
* fix(mcp): derive MCP bridge and session keys with the FIPS provider's KBKDF

With OpenSSL enforcing FIPS, hashlib.scrypt raises, so every MCP session
token and bridge envelope request failed with a 500. Under FIPS the keys
now come from LITELLM_SALT_KEY through SP 800-108r1 KBKDF run by the
provider's EVP_KDF. Without FIPS the scrypt keys are unchanged

* fix(mcp): keep master key rotation invalidating MCP tokens under FIPS

Pass the master key as the KBKDF context, so rotating it changes the
three FIPS-mode keys as it changes the scrypt keys

* test(mcp): annotate new FIPS key test locals with Final

* fix(mcp): derive the FIPS MCP keys only when general_settings.fips_mode is on

On a FIPS OpenSSL 1.1.1 host (RHEL 8) scrypt still works, so keying the switch on
openssl_enforces_fips() sent those hosts to the OpenSSL 3 only KBKDF and broke MCP
sessions and bridge envelopes that worked on main. fips_mode_active() is the same
reader B1 adds, set by enforce_fips_mode at startup.

* refactor(fips): move the salt key KBKDF into encrypt_decrypt_utils

The KBKDF helper derives keys from LITELLM_SALT_KEY, so it now sits next to the other salt key helpers, and fips.py goes back to main's version.

* Pin the MCP key KBKDF fetch to the FIPS provider

Fetch KBKDF with the property query fips=yes, so a change to the process's default
properties after boot can't move the derivation to a non-FIPS provider. The shared
fips_openssl test fixture drops the pin, since the test OpenSSL has no FIPS provider.
2026-11-07 13:33:27 -08:00
joshua-berri
152a5cac7b
feat(ui): set deployment weights in key and team router settings (#50552)
* feat(ui): set deployment weights in key and team router settings

The proxy already applies router_settings.weights per key and team, but no
dashboard screen could set them, and saving a team in the UI dropped weights
set through the API

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

* fix(ui): only offer deployments the key or team may weight

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

* fix(ui): show weight load errors, inherited strategy and team deployments on key create

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

* fix(ui): keep reliability fields on create forms and label weight inputs by deployment id

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

* style(ui): name the accordion value identity object

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

* test(ui): expect weights in the key create router settings payload

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

* fix(ui): keep weight drafts, loading state and team switches correct

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

* fix(ui): only accept weight keystrokes that save as a number

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-07 13:25:59 -08:00
ryan-crabbe-berri
093caa52fa
test(e2e): add a Codex row where a sub-agent must read the task its parent hands it (#50494) 2026-11-07 21:16:39 +00:00
ryan-crabbe-berri
02d610f1a9
fix(anthropic): disable optional default thinking for reasoning effort none (#50541) 2026-11-07 12:51:37 -08:00
Yogeshwaran Ravichandran
8d9ddb349e
feat(guardrails): add prebuilt credential patterns for OpenAI, Anthropic, and Gemini (#50268)
* feat(guardrails): add prebuilt credential patterns for JWT, and OpenAI, Anthropic, Gemini API keys

* fix(guardrails): prevent quadratic backtracking in jwt_token pattern

* test(guardrails): move credential pattern tests into test_patterns.py

* feat(guardrails): remove jwt_token to keep credential patterns strictly bounded

* fix(guardrails): mask whole provider keys and keep openai_api_key to OpenAI's key shapes

End each pattern so a key ending in "-" is masked in full, require OpenAI's
T3BlbkFJ marker on bare sk- keys so other vendors' sk- keys and LiteLLM
virtual keys are left alone, add sk-None- user keys, and replace the
registration-only tests with whole-match, length-boundary, and negative cases

* fix(guardrails): require the T3BlbkFJ marker on every OpenAI key form and match keys after escaped separators

openai_api_key now needs the marker on the sk-proj-, sk-svcacct-, sk-admin- and sk-None- forms too, as gitleaks and TruffleHog do. anthropic_api_key and gemini_api_key also start a match right after a literal \n or \t or a percent-encoded byte, so keys in pasted JSON strings and URL-encoded logs are masked.

* fix(guardrails): bound the OpenAI key regex, match Gemini prefixes case-sensitively, and hold back long keys in streamed replies

* fix(guardrails): size the streaming holdback per pattern, stop OpenAI key tails before the next key, and allow non-ASCII text after a key

* fix(guardrails): mask content filter pattern matches found in the original text so overlapping patterns cannot split a key

* fix(guardrails): let a block pattern see the text after earlier masking patterns, as before

Masking patterns still match the original text and merge overlapping matches into one tag, but a BLOCK pattern checks the text with the masking patterns listed before it applied, so a request main masked and passed is not blocked now. Detections name only the patterns whose tag was written.

* fix(guardrails): keep content filter masks in order and hold back the longest OpenAI key

Masking patterns run in configured order on the masked text as on main, so a contextual
pattern still matches next to an earlier tag, and each also matches the original text so an
earlier tag cannot split a key. Detections follow the configured pattern, not its name, and
the OpenAI streaming holdback covers the pattern's 419-character longest match.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-07 12:29:12 -08:00
devin-ai-integration[bot]
5bc25ff8ea
fix(cost-map): update Together Qwen3.7-Max pricing and add Rnj-1 Instruct (#50548)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-07 12:01:36 -08:00
Kerry Lu
9ec3b5fd8b
feat(cost-map): move -latest model aliases into the cost map (#50491)
* feat(cost-map): move -latest model aliases into the cost map

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

* test: load the finalized local cost map in the moonshot alias test and assert alias pricing by behavior

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

* test: compare alias cost map rows to their targets

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

* feat(cost-map): give each -latest alias its own alias_of row

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

* fix(cost-map): keep alias_of rows out of alias lists and add them to the registry schema

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
(cherry picked from commit 91e6cf2c27)

* fix(cost-map): let alias lists win over alias_of rows in both loaders and never serve a malformed pointer

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
(cherry picked from commit a54c42218a)

* feat(cost-map): add -latest alias rows for the tier 1 and 2 hosting providers

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

* test(cost-map): derive hosted alias targets from the cost map

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

* test(databricks): skip alias rows when checking cache-write rates

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-07 11:56:47 -08:00
DDT
d0c16008c8
fix(chatgpt): round-trip encrypted reasoning through /v1/messages (#49076)
* fix(chatgpt): round-trip encrypted reasoning through /v1/messages

The chat bridge stored encrypted reasoning on reasoning_items, the Anthropic
adapter never read that field, stream chunks that only carried thinking
blocks were dropped, and the messages adapter stripped the signed blocks
before a chatgpt request could turn them back into reasoning items.

Co-authored-by: DDT <1786035110@qq.com>

* fix(anthropic): keep Responses reasoning on tool turns and retry once when a backend cannot decrypt it

* fix(anthropic): stop logging rejected encrypted reasoning

Quote the retry helper return type so importing litellm no longer evaluates a string alias with |. The warning keeps provider, model, status, and error code, and leaves the ciphertext out.

* fix(anthropic): keep encrypted reasoning on OpenAI thinking and xAI web search turns, and stream signatures only to Messages clients

The keep check now knows when the handler moves an OpenAI thinking turn to Responses, and when web_search_options sends an xAI turn there.

The Responses-to-chat stream iterator adds the signed reasoning chunk only when the caller is /v1/messages, so /v1/chat/completions streams stay unchanged.

The reasoning retry fires only when the translated request still carries encrypted reasoning.

* fix(anthropic): keep each streamed reasoning summary with its own item, and read the Messages marker before the bridge call

* fix(anthropic): send openai/ Messages turns again once without encrypted reasoning a backend rejects

The openai/ Responses path on /v1/messages had no retry, so a chatgpt/ block left in history answered 400 on every later openai/ turn. That path now sends the turn once more without encrypted reasoning, the same way the chat bridge does. The retry matches only invalid_encrypted_content or a "could not be verified" message naming the content, so the exception mapper's own hint no longer triggers it. The xAI web search keep path is backed out, since xAI sends no encrypted reasoning over the bridge.

* fix(anthropic): keep every non-streamed reasoning item for Messages clients and read a deployment's reasoning_summary in the keep check

* fix(anthropic): log only the outcome of a turn sent again without rejected reasoning

A /v1/messages turn whose replayed reasoning the backend rejects is sent once more without it. The rejected first attempt ran the failure callbacks on the shared logging object, which logged a failure for a turn that then succeeded, kept the retry's own failure from being logged, and freed the parallel-request slot while the retry was still in flight.

The first attempt now runs inside resent_on, and the client wrapper skips failure logging and the budget unbind for a failure that matches it. The retry runs outside that context, so its own outcome logs as usual.

* test(anthropic): type the parameters of the failure-recording test loggers

* fix(anthropic): skip the SDK num_retries retry for an attempt that is sent again

* fix(anthropic): stop SDK retries from resending a rejected block or clearing the global num_retries

A transient 429 or 500 on a /v1/messages turn that carries a replayed encrypted
block was retried by an SDK call's num_retries loop, which sent the block again
until every retry was rejected. On async calls the loop then raised the first
transient error, so the turn failed instead of being sent again without the
block. The retry loop now stops at that rejection and passes it on, so the turn
is sent again once without encrypted blocks.

A rejected first try on a sync call also cleared the process-wide
litellm.num_retries, so later calls in the process were no longer retried. That
setting now stays as configured.

* fix(anthropic): stop a cancelled SDK retry from starting another call

The resent-failure retry predicate returned true for BaseException
subclasses such as asyncio.CancelledError, so a cancelled
acompletion_with_retries or aresponses_with_retries call could start
another provider call. Retry only Exception subclasses again, as
tenacity's default did, while still skipping a resent failure.

* test(anthropic): type the retry helper parameter in the cancellation test

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-07 11:32:59 -08:00
Mateo Wang
cdbb487926
refactor: read lazy attributes with getattr instead of a TypedDict wrapper (#50543)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-07 10:56:30 -08:00
Gau·Beo
268bc0345c
feat(cost-map): add moonshot/kimi-k3.1-pro-highspeed pricing (#50521) (#50538) 2026-11-07 10:35:18 -08:00