Commit graph

11796 commits

Author SHA1 Message Date
Abhimanyu Kapur
c136797805
Merge pull request #32944 from BerriAI/litellm_translate_effort_chat_completions
fix(anthropic): translate raw adaptive thinking for pre-4.6 models on chat completions and Bedrock Converse
2026-07-11 19:59:16 -07:00
yucheng-berri
f61fd2fb6d
fix(xecguard): sanitize scan result before recording it for logging (#32935) 2026-07-12 02:46:02 +00:00
Krrish Dholakia
f717e3b2f0
feat(router): random-pick multi-model complexity tiers (#32967)
* feat(router): random-pick multi-model complexity tiers

Tier pools already make sense without adaptive; stop pinning lists to
index 0 and shuffle within the classified tier instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ci): format complexity router config

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ci): use PEP 585 types for tier pools

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 19:27:11 -07:00
Tin Chi Lo
55ff3a242c fix(mcp): treat a positive sub-second upstream lifetime as alive, not expired
_classify_upstream_lifetime decided "expired" from int(float(expires_in)), which truncates toward
zero, so a positive fractional lifetime in (0, 1) became 0 and was misread as already elapsed. That
rejected the mint with 502 in _finish_bridge_mint after the single-use upstream code had already been
consumed, even though the upstream reported a positive remaining lifetime.

Decide expired on the parsed numeric value rather than its truncated int, so only a genuinely
non-positive value is expired. The envelope works in whole seconds and cannot represent a sub-second
lifetime, so a positive value that truncates to 0 clamps up to the 1s floor instead of being rejected.
Values >= 1 still truncate toward zero so the envelope never claims more life than the upstream stated,
and NaN / Infinity / oversized input still read as unparseable ("unspecified").

Regression covers the classifier (0.5 and 0.001 clamp to 1, 1.9 truncates to 1, -0.5 stays expired) and
the mint (a 0.5s upstream lifetime mints a 200 envelope rather than a 502); reverting to the
truncate-then-check reddens both.
2026-07-11 19:20:03 -07:00
Tin Chi Lo
a07aba0579 refactor(mcp): make bridge-mint resolvers return tagged unions so status is truthful by construction
Three findings landed together, all one defect: a resolution step crushed several distinct outcomes
into a single None or a silent default, so the mint's error mapper could not tell them apart and
assigned the wrong status. Identity resolution mapped a database outage to the same None as a missing
credential, which the mint reported as 400 invalid_request, blaming the caller for a gateway outage
while admission statuses the same outage 503/500. Lifetime coercion mapped an explicit non-positive
expires_in to the same None as an absent one, so an upstream token the IdP reports as already dead was
sealed into an hour-long envelope. And the refresh_token grant was run through the upstream exchange
(which can rotate the client's upstream refresh credential) and its result then discarded, even though
a bridge server seals no refresh_token and the client never holds one to present.

Rather than add a mapping branch per finding, the fix changes the return types so a wrong status is not
representable. Each resolution step now returns a precise tagged value instead of None: identity
resolution returns a _ResolvedKey or one of no_active_key / unavailable / unresolvable, classified the
same way admission's _reload_admitted_key classifies the same conditions; upstream-lifetime
classification returns a positive number of seconds, "unspecified" (absent or unparseable, which the
envelope caps), or "expired" (a parseable non-positive value, an already-dead token); and upstream-grant
validation returns a typed grant or one of no_access_token / expired_lifetime. Thin exhaustive mappers
(match plus assert_never) lift each vocabulary into one bridge-mint taxonomy of eight named failures,
and a single _bridge_mint_error_response gives each its truthful RFC 6749 §5.2 status: 400 for the
caller's missing credential or an unsupported grant, 503 for a transient auth-DB outage, 500 for a
gateway that cannot resolve identity or is not configured, and 502 for an upstream response with no
usable token, an already-expired lifetime, or a token too large to seal. Adding a failure mode now
requires a new literal and a match arm the type checker forces, so the class of wrong-status bug cannot
recur silently.

The refresh_token grant is rejected in _prepare_bridge_mint before the exchange with
unsupported_grant_type, so it can never rotate or consume the client's upstream refresh credential;
renewal is re-running authorization_code, as the sealed refresh_token=None already intends. An absent or
unparseable expires_in still mints a capped envelope (the by-design behaviour for an upstream that omits
the field); only an explicitly-dead lifetime is rejected.

Tests cover the resolver's three failure classes (including a real connection-error outage and a missing
prisma_client), the mint statuses for each (503 before the upstream exchange, 500, 502 on an expired
upstream lifetime, and a capped mint on an unknown one), and the refresh-grant rejection before any
exchange. The three findings are mutation-checked: reverting each fix turns its regression test red.
2026-07-11 19:20:03 -07:00
Tin Chi Lo
4ba7221b7a fix(mcp): let the bridge envelope report expires_in 0 at the jwt exp boundary
_finish_bridge_mint floored the reported expires_in at 1. Admission expires the
envelope against the JWT's second-truncated exp, so when the mint lands in the same
second that exp falls on (a sub-second upstream lifetime, for instance), the true
remaining life is 0 and reporting 1 tells the client the bearer lives one second past
the point admission already rejects it. Floor at 0 instead so the reported lifetime
never overstates the exp; the value still cannot go negative.

The regression pins the boundary directly: minting at now=100.25 with a 1s upstream
token seals exp=101, and the reported expires_in is max(0, 101 - ceil(100.25)) = 0.
Under the old floor of 1 it reads 1, so the test fails on that mutation.

Also drops the unused mcp_server parameter from _prepare_bridge_mint; identity and
key derivation there never referenced the server.
2026-07-11 19:20:03 -07:00
Tin Chi Lo
2f0ddc82f7 refactor(mcp): make the bridge delegate mint a phased failures-as-values pipeline
The dcr_bridge oauth_delegate token mint validated its preconditions in two
places: a pre-exchange guard inside exchange_token_with_server (master_key set,
resolvable litellm identity) and an authoritative re-check inside the post-exchange
_mint_bridge_delegate_token_response. Keeping the two in step by hand is what kept
producing the same class of finding: a precondition guarded on one grant branch but
not the other, master_key checked after the exchange on one path, identity resolved
twice, and each failure raising an ad-hoc HTTPException with its own status and body
shape.

Model the mint as three phases whose failures are values. _prepare_bridge_mint runs
before the exchange, checks every precondition once (master_key, then identity), and
returns either a frozen _BridgeMintReady carrying the resolved key hash and the
master-key-derived envelope keys, or a _BridgeMintError literal. Because every
precondition lives in prepare, and prepare runs before the upstream POST, no failure
can burn the single-use code or rotate a refresh token, for either grant type, by
construction rather than by a guard we have to remember to keep in sync.
_finish_bridge_mint runs after the exchange and has no preconditions left that can
fail; its only failure values are properties of the upstream response itself (no
usable access_token, or a token too large to seal). One mapper,
_bridge_mint_error_response, turns each _BridgeMintError into an RFC 6749 section
5.2-shaped body with a status truthful about where the failure is (400 for the
caller, 500 for gateway config, 502 for the upstream), with an exhaustive match plus
assert_never so a new failure mode cannot be added without a matching status.

Behavior is unchanged for the client. Every failure that previously raised now
returns the same status as an OAuth error body, which is the correct token-endpoint
contract; the three tests that asserted a raised HTTPException now assert the
returned response. _exchange_for_bridge_server additionally asserts the identity
resolver is awaited exactly once for a bridge server and never for a non-bridge one.
2026-07-11 19:20:03 -07:00
Tin Chi Lo
e16ad044c3 fix(mcp): close the burn-before-check gate for both grants and validate master_key first
Follow-up to the pre-exchange identity gate, which I had only added to the
authorization_code branch and which left the master_key check inside the mint
(after the upstream exchange) - so the very burn-then-fail pattern it was meant to
prevent still applied to refresh_token grants and to a misconfigured gateway.

- Hoist a single pre-exchange gate above the upstream call that covers BOTH grant
  types: it fails closed (invalid_request) on an unresolvable litellm identity and
  500s on an unset master_key BEFORE the single-use code or refresh token is
  exchanged/rotated, so a bad key or a misconfigured gateway never burns the
  upstream credential.
- Report expires_in from the envelope JWT's own second-truncated exp (rounding the
  elapsed portion up) instead of the raw expires_at - now delta, so the client is
  never told the bearer is valid past the ~1s point admission already expires it.

Regression tests assert the upstream exchange is never called on the no-identity
refresh grant and the master_key-unset path, and that the reported expires_in does
not overstate the JWT exp.
2026-07-11 19:20:03 -07:00
Tin Chi Lo
7a63e51625 fix(mcp): harden the bridge token mint (multi-lens review pass)
Findings from a full adversarial review of the mint path across security,
correctness, error-handling, concurrency, and OAuth-protocol dimensions.

- expires_in coercion is now total: int(float(...)) can raise OverflowError on
  Infinity / a giant numeric string, which escaped the ValueError/TypeError catch
  and 500'd the token endpoint. Unified to catch OverflowError too.
- Resolve the litellm identity BEFORE exchanging the single-use upstream code, so
  a missing or transiently-unresolvable identity fails closed with invalid_request
  without burning the code (the mint re-resolves via a cache hit).
- The no-identity failure is now an RFC 6749 5.2-shaped invalid_request
  (JSONResponse, top-level error, no-store) instead of a detail-wrapped
  HTTPException, matching the BYOK OAuth endpoint.
- EnvelopeTooLarge (upstream token too big to seal) surfaces a 502, not a 500.
- The upstream refresh_token is no longer sealed into the envelope: the edge
  never consumes it, so it was dead weight embedding a long-lived upstream
  credential in the client bearer and enlarging the envelope; refresh is a
  follow-up (a dedicated refresh-envelope).

Security review found no exploitable defect (forgery, cross-server/user replay,
leakage, confused-deputy all closed). Regression tests cover the OverflowError,
the code-not-burned path, the RFC-shaped error, the 502, and the dropped refresh.
2026-07-11 19:20:03 -07:00
Tin Chi Lo
2f349f6cd1 fix(mcp): coerce numeric expires_in and make the active-key check total
Two correctness gaps in the bridge mint. _bridge_grant_from_token_response only
accepted an int expires_in, dropping a float (3600.0) or numeric-string ('3600')
lifetime to None so the envelope fell back to its 1h cap and could outlive a
shorter-lived upstream token; coerce it to a positive int (bool excluded). And
_key_is_active called datetime.fromisoformat on the str|datetime expires outside
the resolver's try, so a malformed stored expiry raised an unhandled 500 instead
of the fail-closed invalid_request; it now fails closed (inactive) on an
unparseable expiry. Regression tests cover int/float/string/bool coercion, the
short-float TTL, and the malformed-expiry fail-closed path.
2026-07-11 19:20:03 -07:00
Tin Chi Lo
362c78e308 fix(mcp): let a keyless-user active key mint a bridge envelope
_resolve_active_litellm_key gated on _active_key_user_id, which returns None both
for blocked/expired keys AND for valid keys with no user_id, so a team-scoped or
service-account key was wrongly rejected with invalid_request at bridge token
exchange. Split the active-state gate (_key_is_active: blocked/expiry only) from
the user_id extraction; the mint seals the key hash, not the user, and admission
already handles a keyless-user key. The per-user token store still gets no user
for such a key, as there is none to key a stored credential by.
2026-07-11 19:20:03 -07:00
Tin Chi Lo
7df848aa6c fix(mcp): return 502 not KeyError when a bridge upstream response lacks access_token
The eager access_token = token_response["access_token"] extraction ran before
the dcr_bridge branch, so a missing upstream access_token raised an unhandled
KeyError and _bridge_grant_from_token_response's nil guard (which maps to a clean
502) was dead code. Move the extraction onto the non-bridge result path so the
bridge branch reaches its 502 guard.
2026-07-11 19:20:03 -07:00
Tin Chi Lo
85255c96fb feat(mcp): seal the authorizing key hash in the dcr_bridge envelope
The mint bound only user_id/server_id into the envelope, which gave admission
no way to reload the caller's key and enforce its current restrictions. Seal the
hashed authorizing key instead (a one-way digest, not a usable credential), so
admission reloads the live UserAPIKeyAuth by it and the key's team/org/tool
permissions and revocation apply per request.

Extract the token endpoint's key resolution into a shared _resolve_active_litellm_key
so the per-user token store (user_id) and the bridge mint (key hash) derive from one
active-key-gated path, and fail the mint closed with invalid_request when no active
key accompanies the request.
2026-07-11 19:20:03 -07:00
Tin Chi Lo
34c6cce705 feat(mcp): mint gateway-bound envelope at the token endpoint for dcr_bridge oauth_delegate 2026-07-11 19:20:03 -07:00
Mateo Wang
053e64e11b
Merge pull request #32979 from BerriAI/litellm_anchor_bedrock_routing_regex
fix(proxy-auth): stop unrecognized model namespaces slipping through provider wildcard keys
2026-07-11 19:18:27 -07:00
mateo-berri
d0d1c0e346 fix(proxy-auth): deny provider-wildcard access inferred through an unrecognized model namespace 2026-07-11 19:00:03 -07:00
mateo-berri
2ed4ceb12e fix(model-cost-map): anchor the bedrock-claude-ids routing rule to the start of the id 2026-07-11 19:00:03 -07:00
yucheng-berri
f947ef14a2
fix(xecguard): use StandardLoggingGuardrailInformation in logging hook (#32911)
XecGuard's async_logging_hook wrote a bare dict to
standard_logging_object["guardrail_information"] while the typed
contract is Optional[List[StandardLoggingGuardrailInformation]].
Readers that iterated the field walked dict keys, raised on
info.get, or silently dropped the entry from guardrail usage
tracking and spend-log writes

Construct the typed entry and append it to the existing list or
create a new one, matching the shared helper pattern. Record the
configured guardrail name instead of a hardcoded "xecguard" and
pass the GuardrailEventHooks enum for guardrail_mode
2026-07-11 17:50:13 -07:00
mateo-berri
a76dd9bf8e test(e2e): cover model-aware mid-conversation system handling on Bedrock Invoke /v1/messages 2026-07-11 17:08:16 -07:00
ryan-crabbe-berri
cb24864a7f
feat(proxy): add expires filter to GET /key/list (#32953)
* feat(proxy): add expires filter to GET /key/list

Add an opt-in expires query param to GET /key/list so callers can fetch
only expired or only active keys without paginating every page and
filtering client-side. 'expired' matches keys whose expires is in the
past (NULL expires excluded); 'active' matches keys that never expire or
expire in the future. Omitting the param preserves existing behavior for
every caller. An unrecognized value returns HTTP 400 rather than silently
returning all keys.

The filter is pushed to the database via the existing Prisma where
builder so callers avoid pulling the full key table into application
memory.

Resolves LIT-3387

* refactor(proxy): declare VALID_EXPIRES_FILTER_VALUES before its first use
2026-07-11 16:48:25 -07:00
mateo-berri
cc27528d4f test(main): assert the responses bridge forwards static aws keys as well as web identity params 2026-07-11 16:47:00 -07:00
Mateo Wang
5e1928c753
Merge pull request #32928 from BerriAI/litellm_quota_management_registry
refactor(e2e): bucket rate limits, budgets, and spend tracking under quota_management
2026-07-11 16:40:43 -07:00
mateo-berri
201730efe5 fix(bedrock): allow bedrock-mantle:CreateInference in the web identity session policy 2026-07-11 16:35:13 -07:00
yucheng-berri
e7f41442d0
feat(guardrails): add pre_mcp_call support to Content Filter (#32936)
* feat(guardrails): add pre_mcp_call support to Content Filter

* test(guardrails): cover canonical MCP key gate under pre_mcp_call mode

* fix(guardrails): scan MCP arguments per value and gate mixed-mode scans by call type

* fix(guardrails): cap MCP argument scan depth and register the walker with the recursion detector

* test(guardrails): update LIT-4226 UI settings tests for content filter pre_mcp_call support

* fix(guardrails): use builtin generics in MCP scan annotations to satisfy strict-rule budget

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 16:17:49 -07:00
mateo-berri
36dff0f63a test(e2e): source the ratelimit suite's model from E2E_CHEAP_ANTHROPIC_MODEL 2026-07-11 16:15:16 -07:00
mateo-berri
ca55fc2deb refactor(e2e): model the tpm spend loop's two outcomes as values 2026-07-11 16:15:16 -07:00
mateo-berri
9f55925c31 test(e2e): name the tpm window deadline's latency margin 2026-07-11 16:15:16 -07:00
mateo-berri
6f2435b54a test(e2e): fail the rpm reset test when the limiter resets early 2026-07-11 16:15:16 -07:00
mateo-berri
1ca1a9cd03 test(e2e): assert the tpm block at its exact token crossing instead of a call-count heuristic 2026-07-11 16:15:16 -07:00
mateo-berri
b9aef1b810 test(e2e): cover key rpm/tpm rate limiting, window reset, and pacing headers 2026-07-11 16:15:16 -07:00
mateo-berri
d37daf0b9f refactor(e2e): move budgets and spend_tracking suites under quota_management 2026-07-11 16:14:21 -07:00
mateo-berri
8225ddf2a4 fix(e2e): name the route the spend_calculate registry cell actually exercises 2026-07-11 16:13:48 -07:00
mateo-berri
320a55f01f refactor(e2e): bucket rate limits, budgets, and spend tracking under quota_management 2026-07-11 16:13:48 -07:00
mateo-berri
e0463a38ff fix(completion): forward aws credential kwargs into litellm_params so the responses bridge keeps WIF auth
Chat-completions requests to responses-only Bedrock Mantle models are
bridged to the Responses API, but completion() forwarded only
aws_bedrock_project_id into get_litellm_params, so aws_role_name,
aws_web_identity_token, aws_session_name and the other SigV4 credential
kwargs never reached sign_request and botocore fell back to the default
credential chain ("Bedrock Mantle auth failed: no Bearer token and no
usable AWS credentials"). Forward the whole AWS credential kwarg family,
extracted from the OPTIONAL_KWARGS_KEYS set get_litellm_params already
supports.
2026-07-11 16:10:39 -07:00
Shivam Rawat
173193d717
Merge pull request #32942 from BerriAI/litellm_dashscope_price_issues
fix(proxy): enforce budget and cost tracking for Dashscope tiered pricing
2026-07-11 15:40:20 -07:00
tin-berri
0772668f33
Merge pull request #32824 from BerriAI/litellm_lit4338_delegate_admission
feat(mcp): admit dcr_bridge oauth_delegate clients via a single envelope bearer
2026-07-11 14:59:35 -07:00
Abhimanyu Kapur
7d1e48d8b1
Merge pull request #32943 from BerriAI/litellm_complexity_router_logging
feat(complexity_router): log the cause of each routing decision
2026-07-11 14:55:38 -07:00
Shivam Rawat
0bd831dc5e fix(proxy): reserve tiered output at the higher reasoning rate
Some tiered Dashscope models price reasoning output above standard output
(output_cost_per_reasoning_token > output_cost_per_token). The reservation charged
all output at the standard rate, so a reasoning-heavy request reserved too little
and concurrent calls could exceed the budget before reconciliation. The reasoning
share is unknown before the request runs, so reserve every output token at the
higher of the two configured rates, for both tiered and flat pricing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 14:55:32 -07:00
yucheng-berri
69c5839cc0
fix(guardrails): filter Add-Guardrail mode dropdown per provider (#32712)
* fix(guardrails): filter Add-Guardrail mode dropdown per provider

The GET /guardrails/ui/add_guardrail_settings endpoint returned every
GuardrailEventHooks value in one flat supported_modes list, so the Admin
UI rendered pre_mcp_call as a selectable Mode for every guardrail. Saving
Content Filter or Tool Permission with pre_mcp_call then failed with a
400 because those guardrails' server-side supported_event_hooks list
excludes it.

Expose each guardrail's supported hooks as a get_supported_event_hooks
classmethod on CustomGuardrail (mirrors the existing get_config_model
pattern) and have the endpoint iterate guardrail_class_registry to build
a supported_modes_by_provider map. The UI Mode dropdown filters by that
map when the selected provider is known and falls back to the global
list otherwise. __init__ now sources its own supported_event_hooks list
from the classmethod so the two sides can't drift.

Also register BedrockGuardrail, ToolPermissionGuardrail, lakera,
lakera_v2, and presidio in guardrail_class_registry so they participate
in the map (they were previously only in guardrail_initializer_registry
and had no class-registry entry).

Behavior change: guardrails that previously had no supported_event_hooks
declared (aim, javelin, azure/text_moderation, cato_networks,
crowdstrike_aidr, headroom, hiddenlayer, lasso, noma, onyx,
prompt_security, qualifire, repelloai, zscaler_ai_guard, aporia_ai,
lakera_ai, lakera_ai_v2, mcp_jwt_signer, model_armor, presidio) now
validate the configured mode at instantiation. Existing configs where
the mode was silently a no-op will fail at proxy startup with a clear
validation error rather than running as a broken guardrail.

Resolves LIT-4226

* fix(guardrails): add LITELLM_STRICT_GUARDRAIL_MODES escape hatch, preserve current mode in edit form

Address Greptile P1 (startup break) and P2 (edit form UX):

LITELLM_STRICT_GUARDRAIL_MODES defaults to true (raise on unsupported
event_hook, unchanged behavior for the guardrails validated pre-PR).
Setting it to false logs a warning and continues, giving deployments an
opt-out while they fix configs that now surface as errors instead of
silently no-op'ing. Regression test covers both modes.

Edit form now surfaces the currently-saved mode even when it is not in
the filtered per-provider list, so a legacy row (e.g. content_filter
saved with pre_mcp_call before this fix) no longer disappears from the
dropdown; the option renders with a 'not supported by <provider>' note
so the user knows to pick another.

* fix(guardrails): correct audited hook lists, prune stale modes on provider switch, clean form lint

Audited every get_supported_event_hooks classmethod against the hooks
each guardrail's own tests exercise and its handler methods. Five were
too narrow and their tests caught it in CI: rubrik gains pre_call,
presidio gains during_call and pre_mcp_call, prompt_security, onyx and
qualifire gain during_call. The remaining classes match either their
original __init__ declarations or their exercised modes exactly.

Cursor review fixes: the Add form now drops selected modes the new
provider does not support when the user switches providers, so a
pre_mcp_call selection cannot ride along into a provider that rejects
it at save; the edit form handles list-shaped stored modes instead of
treating mode as always a string.

Extracted shared toModeArray and getSupportedModesForProvider helpers
into guardrail_info_helpers so both forms use one implementation, typed
the remaining any usages in both forms, removed nested ternaries, and
committed the ratcheted-down eslint metrics and pruned suppressions
2026-07-11 14:51:27 -07:00
Abhimanyu Kapur
a5b0f32a84 test(bedrock-converse): cover adaptive-thinking drop when max_tokens too small
Adds the regression test for the warning-drop branch in the Converse
adaptive-thinking translation, mirroring the chat completions path's
test_raw_adaptive_thinking_dropped_when_max_tokens_too_small.
2026-07-11 14:50:32 -07:00
Tin Chi Lo
1c3c1af529 fix(mcp): run proxy-wide pre-DB gates on bridge envelope admission
The envelope arm bypasses user_api_key_auth, so it never ran
pre_db_read_auth_checks (request-size and body-safety limits, the IP allowlist,
and the general_settings route allowlist) that the normal MCP admission path
runs before any key lookup. A caller blocked by IP or a disallowed proxy route
could be admitted through an envelope where the same principal on the normal
path is rejected. Run those gates before the envelope crypto, mirroring the
pipeline's pre-DB ordering; a blocked IP or route surfaces its own 403.
2026-07-11 14:39:57 -07:00
Abhimanyu Kapur
741220c80f fix(bedrock-converse): translate adaptive thinking for pre-4.6 models
Follow-up to #32867 (native /v1/messages) and the /chat/completions
commit earlier on this branch, extending the same adaptive-thinking
translation to the Bedrock Converse path.

Clients like Claude Code send thinking={type: "adaptive"} on every
request. When routed via Bedrock Converse to pre-4.6 models
(claude-haiku-4-5, claude-sonnet-4-5), this was forwarded as-is and
rejected by the model. Mirrors the translation already applied on the
/chat/completions and /v1/messages paths: map to legacy
thinking={type: enabled, budget_tokens}, capped below max_tokens.

Also fixes the missing custom_llm_provider arg in the chat completions
path's call to AnthropicConfig._map_reasoning_effort.
2026-07-11 14:38:20 -07:00
Abhimanyu Kapur
3385a32413 feat(complexity_router): log the cause of each routing decision
The complexity router's info log didn't say what drove a routing decision.
Literal and semantic keyword matches logged an identical "keyword rule fired"
line (no way to tell which mechanism fired), and the scorer's line carried no
consistent marker tying it to the same question.

Emit one greppable line per decision naming the cause: literal_keyword_match,
semantic_keyword_match, or complexity_scorer. The hook already knows which ran
(the config's semantic_keyword_matching flag distinguishes lexical from
semantic; the override-vs-scorer branch distinguishes keyword match from
scorer), so this is label-only: no behavior change, no new types, no added
latency.

Adds regression tests asserting each decision path logs its cause; they fail if
a label is swapped or the cause= marker is dropped.
2026-07-11 14:26:11 -07:00
Shivam Rawat
c132a03ca7 fix(proxy): reserve tiered budget all-or-nothing across all deployments
Alibaba Model Studio (Dashscope) tiered pricing is all-or-nothing: the tier is
selected by a request's total input tokens and every token, input and output, is
billed at that one tier's rate. The reservation path used graduated slicing and,
worse, picked the output tier from the output-token count, so a long-context
request with a large output allowance reserved far less than the provider charges
and could slip past a depleted budget. Select the tier from input tokens and apply
its rates to all input and output tokens.

Reservation also read tiered pricing from only the first deployment in a model
group. A caller could hit an alias whose cheaper deployment was listed first and
exceed the budget once routed to a costlier sibling. Estimate against every
eligible deployment's pricing and reserve the maximum.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 14:22:15 -07:00
Abhimanyu Kapur
831dbbc4df fix(anthropic): translate raw adaptive thinking for chat completions on pre-4.6 models
Clients that pass thinking={"type": "adaptive"} directly (not via the
reasoning_effort alias) on the /chat/completions interface had it forwarded
unmodified to pre-4.6 Anthropic models, which reject the shape. Mirrors the
translation already applied on the native /v1/messages passthrough (#32867):
translate to legacy thinking={type: enabled, budget_tokens}, capped below
max_tokens, dropping thinking when max_tokens can't fit even the minimum
budget. Hoists the shared budget-capping helper onto AnthropicConfig so both
paths use one implementation.
2026-07-11 14:20:48 -07:00
Abhimanyu Kapur
92dfbdbb21
Merge pull request #32859 from BerriAI/litellm_complexity_router_keyword_tiers
feat(auto_router): keyword tier overrides and semantic keyword matching for the complexity router
2026-07-11 14:18:21 -07:00
Shivam Rawat
8548816ae4 fix(cost): coerce string tiered-pricing costs and share tier helper
YAML-parsed tier costs can arrive as strings (e.g. "4e-07"), which broke
arithmetic in the graduated tiered-pricing calculation. Coerce per-token
costs to float in both the in-range and remaining-tokens paths.

Move the tiered-cost helper out of the Dashscope module into a
provider-neutral home so the proxy budget reservation no longer depends on
a provider-specific module.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 14:05:26 -07:00
Mateo Wang
ead7ad3804
Merge pull request #32874 from BerriAI/litellm_thread_provider_capability_probes
fix(anthropic): thread real provider through capability probes instead of pinning anthropic
2026-07-11 14:00:59 -07:00
Abhimanyu Kapur
973a86adab Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_complexity_router_keyword_tiers
# Conflicts:
#	ui/litellm-dashboard/eslint-metrics.json
2026-07-11 13:54:59 -07:00
Shivam Rawat
65e2432a37 fix(proxy): bill tier-only deployments instead of $0
Route cost calculation to the deployment's router_model_id entry when it carries tiered_pricing but no flat per-token rate, so models like dashscope/qwen3.7-plus are billed via their tier table rather than the pricing-stripped shared alias.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 13:47:56 -07:00