Commit graph

6594 commits

Author SHA1 Message Date
Devin AI
d33937dee4 fix(gemini): propagate batchEmbedContents usageMetadata for gemini-embedding-2 multimodal usage 2026-07-17 11:11:35 +00:00
devin-ai-integration[bot]
9cae6fa437
fix(logging): classify async anthropic_messages and generate_content as async (#33589) 2026-07-16 20:56:47 -07:00
tin-berri
b880ad3134
Merge pull request #33637 from BerriAI/litellm_lit4520_cache_min_tokens
fix(router): resolve prompt cache minimum per model instead of a flat 1024
2026-07-16 20:34:12 -07:00
Tin Chi Lo
fc5848174e fix(router): take the lowest minimum across a model group, not the highest
The read gate cannot cause a wrong pin. A deployment is only pinned when the cache
already holds an entry for the prefix, and async_log_success_event writes entries
against the deployment's real model rather than the group alias, so a model that
will not cache a prefix never records one and there is nothing to pin it to

That makes this gate purely a cheap short-circuit deciding whether the cache lookup
is worth doing, so the threshold must be the lowest minimum in the group. Taking the
highest skipped the lookup for a prefix a lower-minimum member had genuinely cached,
losing a hit it earned, and protected against nothing. It also broke the Fable 5
direction this ticket is meant to fix: its real minimum is 512, so a group gate stuck
at a higher value would skip the lookup for a prefix Fable 5 had actually cached
2026-07-16 19:53:10 -07:00
devin-ai-integration[bot]
4cfc987f56
fix(vertex_ai): surface Gemini grounding toolUsePromptTokenCount in Usage (#33533)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-16 19:50:16 -07:00
Tin Chi Lo
25b2f83f97 test(router): clear the lru_cache when forcing the local cost map
get_model_info is lru_cached, so swapping litellm.model_cost is not enough on its
own. An earlier test that resolved these models against the remote map, which does
not carry prompt_cache_min_tokens yet, leaves cached entries without it, and the
stale hit resolves to the default. The assertions would then pass for the wrong
reason or fail depending on execution order

Clear on teardown as well, so entries these tests warm against the local map do not
leak into later tests, matching the fixture already used in test_utils.py

Also pin that a wildcard route resolves the underlying model's minimum. That works
only because pattern_match_deployments substitutes the real model name into
litellm_params before the deployment reaches the check; without the assertion that
claim is unpinned and the threshold would silently fall back to the default
2026-07-16 19:26:28 -07:00
Tin Chi Lo
ba70189e32 fix(router): resolve prompt cache minimum per model instead of a flat 1024
MINIMUM_PROMPT_CACHE_TOKEN_COUNT was a flat 1024 described as "minimum number of
tokens to cache a prompt by Anthropic". Anthropic's minimum cacheable prefix is
per-model and ranges from 512 to 4096, and it can differ per platform for the same
model, so one constant is wrong in both directions

is_prompt_caching_valid_prompt gates PromptCachingDeploymentCheck, which is what
optional_pre_call_checks: ["prompt_caching"] turns on. When it believes a prompt is
cacheable, async_filter_deployments pins routing to whichever deployment previously
served that prefix. For a prompt between 1024 and 4096 tokens on Opus 4.6, Opus 4.5
or Haiku 4.5, litellm judged it cacheable and constrained routing while the provider
never cached it, so the pin cost load balancing for nothing. In the other direction
Fable 5 caches from 512 tokens, so a 512 to 1024 token prefix was refused a pin it
had earned

The minimum now resolves from prompt_cache_min_tokens in the model cost map, which
keeps it current with new models and lets the Bedrock override for Fable 5 fall out
of the existing per-entry keys with no special casing. MINIMUM_PROMPT_CACHE_TOKEN_COUNT
stays as a global escape hatch when explicitly set, and as the fallback for models the
cost map has no entry for

async_filter_deployments only ever receives the model group alias, never a model name,
so it resolves the threshold from healthy_deployments instead. A group may mix models
with different minimums, so it takes the max: a prompt is only treated as cacheable when
it clears every member's minimum, because an unnecessary pin is the defect being fixed
while a missed pin only forfeits an optimization

Gemini context caching shares this gate and has the same defect; its entries are left
unset so they keep today's behavior, tracked separately in LIT-4525
2026-07-16 19:10:23 -07:00
tin-berri
ecef9e6c9b
Merge pull request #33586 from BerriAI/litellm_mcp_oauth_challenge_mode_aware
fix(mcp): make the preemptive-401 OAuth challenge decision mode-aware
2026-07-16 18:54:45 -07:00
mubashir1osmani
9b6289e497
fix(sso): stop stamping the UI session budget on CLI login tokens (#33312)
A `lite login` token 429'd with "Budget has been exceeded! Max budget:
0.25" even when no budget was configured anywhere. cli_poll_key stamped
the minted CLI session token with litellm.max_ui_session_budget ($0.25)
as a fallback whenever the user and team had no budget of their own. That
cap was designed for the Admin UI "Test Key" chat pane; the CLI reused
the same session-token machinery, so it inherited a playground-sized
budget baked into the encrypted token at login (unchangeable without
re-login), which trips fast under real CLI/agent use.

The cap is also redundant: the token already carries user_id and team_id,
so the real user/team budgets are enforced independently at request time.
Pass max_budget=None so the CLI token is governed only by those real
budgets, and drop the now-dead user/team budget lookups. The UI login
token's guard (get_experimental_ui_login_jwt_auth_token) is untouched.
2026-07-16 17:47:38 -07:00
Tin
f023c819ec test(mcp): add transport-level M2M regression tests for the preemptive-401 gate
Grafted from PR #33582 (closing as superseded by this PR): drives
handle_streamable_http_mcp with real MCPServer objects, parametrized over a
stamped client_credentials row and a legacy unstamped M2M-shape row; both must
reach the session manager without the per-user token store being consulted
2026-07-16 17:39:38 -07:00
Tin Chi Lo
287a89e2ad fix(mcp): make the preemptive-401 OAuth challenge decision mode-aware
The preemptive-401 gate for auth_type=oauth2 MCP servers keyed the challenge
on whether an Authorization header was present (not oauth2_headers). Because
the header parser classifies any Authorization bearer as an OAuth token before
the target server is resolved, a LiteLLM virtual key presented as
Authorization: Bearer sk-... suppressed the challenge on a gateway-managed
authorization_code server; the session then opened with no upstream token and
tools/list masked the failure as 200 with an empty tool list. The same gate
also wrongly challenged client_credentials (M2M) servers, which the gateway
authenticates by minting its own token at egress.

The decision is per oauth2 sub-mode, not per header. Gateway-managed modes
never receive a client-supplied upstream token: client_credentials mints at
egress so it is never challenged, and gateway-managed interactive
(authorization_code, non-delegate) is challenged whenever no stored per-user
token exists, regardless of any bearer. Only the delegate/upstream-PKCE mode,
where a present bearer genuinely is the upstream token, keeps keying on the
Authorization header. oauth2_headers itself is left untouched so the
delegate/passthrough egress paths that forward the client bearer are
unchanged.
2026-07-16 17:39:38 -07:00
tin-berri
68f0fb0346
Merge pull request #33584 from BerriAI/litellm_lit4451_semfilter_chat_tool_format
fix(mcp): keep the MCP reference intact when the semantic filter narrows tools
2026-07-16 16:46:22 -07:00
devin-ai-integration[bot]
0d7b0f708b
fix(model_armor): restore reference attachments via skip_unscannable_attachments and remove the attachment count cap (#33554)
* fix(model_armor): add skip_unscannable_attachments to allow reference-only attachments through

* fix(model_armor): wire skip_unscannable_attachments through guardrail config

* fix(model_armor): make max_file_attachments configurable and scan overflow instead of dropping

* fix(model_armor): remove the per-request attachment count cap and scan all attachments

---------

Co-authored-by: yucheng <yucheng@berri.ai>
2026-07-16 16:06:41 -07:00
tin-berri
669ef389b9
Merge pull request #33587 from BerriAI/litellm_lit4448_scim_entitlements
Some checks are pending
CodSpeed Benchmarks / benchmarks (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
feat(scim): ingest and round-trip SCIM entitlements and roles user attributes
2026-07-16 15:23:00 -07:00
Yassin Kortam
ae8dc1f39f
fix(proxy): stop stale auth cache re-publish so key updates and deletes propagate across replicas (#33565)
With enable_redis_auth_cache and multiple replicas, /key/update and
/key/delete delete the Redis auth blob and the handling pod's in-memory
entry, but two read-path writers re-published the stale blob from any other
replica's per-pod memory back to Redis with a fresh 60s TTL on every
request: the post-auth re-cache in user_api_key_auth and the spend writeback
in update_cache. Replicas whose in-memory entries expired then re-primed
themselves from the poisoned Redis entry, so key limit and access changes
never took effect fleet-wide while traffic continued, and a deleted key kept
authenticating.

The auth object is now written only by the DB-load paths
(IdentityStore._resolve_key, get_key_object): the post-auth re-cache is
removed outright (even a local-only write could race an invalidation and
resurrect a revoked key on this worker) and spend tracking no longer writes
the auth object back at all; spend is tracked through the spend🔑*
counters. The remaining spend writebacks for user, team, end-user, and tag
objects become local-only so they cannot republish stale management objects
either, with one deliberate exception: the proxy-wide
{litellm_proxy_admin_name}:spend scalar keeps its shared Redis write because
the global max_budget check reads it between authoritative DB reloads, and
it carries no limits or permissions so sharing it cannot resurrect an
invalidated auth blob.

DualCache's redis-to-memory read backfill also ignored default_in_memory_ttl,
pinning backfilled entries for InMemoryCache's 600s default instead of the
configured 60s auth TTL; the backfill now injects the configured default like
every write path already does, so a replica primed from Redis converges
within the auth cache TTL as well.

Consolidates the sibling stale-auth-recache branch; the delete-propagation
case is the duplicate ticket LIT-4350.

Resolves LIT-4219
2026-07-16 15:00:33 -07:00
ryan-crabbe-berri
5ab160113f
feat(proxy): add disable_auto_add_proxy_admin_to_teams flag (#33563) 2026-07-16 14:50:40 -07:00
Yassin Kortam
21ba9692c3
fix(router): apply team/key enable_tag_filtering to tag routing (#33436)
Team/key router_settings.enable_tag_filtering was stored and echoed by
/team/info but never applied at request time: the per-request override
whitelist in route_llm_request.py dropped it, tag filtering only read the
router-level flag, and UpdateRouterConfig silently discarded the field on
/key/generate and /config/update. Requests from teams with the toggle on
were load balanced across all deployments instead of tag-matched ones.

- add enable_tag_filtering to the router_settings_override whitelist and
  strip any client-supplied copy from the request body first, so only the
  key/team value reaches the router
- run tag filtering when the request carries enable_tag_filtering=True; a
  request-level False cannot disable a router-level True, so per-request
  settings can only scope down, never escape the global policy
- add the field to UpdateRouterConfig so key and config update paths stop
  dropping it, and to all_litellm_params so it never leaks into provider
  request bodies
- allow it through Router.update_settings/get_settings so the global UI
  toggle persists across DB config reloads

Resolves LIT-4390
2026-07-16 14:41:24 -07:00
Tin Chi Lo
90bbe706c0 fix(scim): harden PATCH multi-valued ops and fail-soft directory metadata reads 2026-07-16 14:23:40 -07:00
Tin Chi Lo
53e5b22c60 fix(mcp): let filter_tools own the undecidable-selection policy
The hook returned early when the semantic filter selected no tools, which
restated a policy that SemanticMCPToolFilter.filter_tools already owns: it
returns the full tool set when nothing matches, so the selection is never
empty. The branch was unreachable, and reachable or not it changed nothing,
since the gateway reads the union of every reference's allowed_tools and
treats an empty union as unset. Its only effect was to suggest the reference
path and the plain tool path resolve a zero-match query differently.

Drop it so a single policy governs both paths, and pin that with a test
covering an unmatched query on each path. Flipping filter_tools to fail
closed now fails the test on both instead of quietly hard-limiting one
surface and not the other.
2026-07-16 14:07:43 -07:00
Tin Chi Lo
ccfa78046a feat(scim): ingest and round-trip SCIM entitlements and roles user attributes 2026-07-16 13:50:00 -07:00
Yassin Kortam
2162da5015
fix(langfuse_otel): build per-request OTLP exporter from key and team dynamic Langfuse credentials (#32437)
* fix(langfuse_otel): build per-request OTLP exporter from key/team dynamic Langfuse credentials

Key-scoped langfuse_otel callbacks only injected Authorization headers into the
init-time exporter, so a proxy without global LANGFUSE_* env vars kept its
fallback exporter and never exported traces to Langfuse. Dynamic params now
build a full per-request OTLP config (endpoint from the key's langfuse_host,
otlp_http, basic auth from the key's credentials).

Resolves LIT-3976

* fix(otel): log dynamic config endpoint in span processor debug output

* fix(otel): redact authorization headers in exporter debug logs
2026-07-16 13:39:10 -07:00
Yassin Kortam
903219a8b1
fix(redis): honor ssl value instead of key presence when building async connection pool (#32590)
* fix(redis): honor ssl value instead of key presence when building async connection pool

* ci: rerun codspeed after cross-runtime-environment flake
2026-07-16 13:38:01 -07:00
Yassin Kortam
c6778b79c3
fix(router): honor per-request routing_strategy from key/team router_settings (#33429)
* fix(router): honor per-request routing_strategy from key/team router_settings

Key and team router_settings.routing_strategy was stored and shown in the
UI but never forwarded to the shared Router, so the global strategy always
won. Forward it through router_settings_override and resolve it in
_get_routing_context: a validated per-request strategy takes precedence
over routing groups and the top-level strategy, with lazily built cached
selectors for strategies that need one. Unknown or unsupported strategy
values are ignored with a warning instead of failing the request, and
routing_strategy is registered in all_litellm_params so it is stripped
before the provider call.

* fix(router): sweep override selectors on strategy re-init and cover coverage-gate helpers

routing_strategy_init now unregisters cached per-request override
selectors so a later update_settings strategy change cannot leave a
zombie selector receiving callback events. Adds direct tests for the
two new helpers so the router code coverage gate passes.

* docs(team): document mcp_rpm_limit in update_team docstring

The documentation CI job walks management_endpoints and requires every
UpdateTeamRequest field to appear in the update_team docstring;
mcp_rpm_limit was added to the model without a docstring line, failing
the job on unrelated PRs depending on walk order. Regenerates
schema.d.ts since the docstring feeds the OpenAPI spec.
2026-07-16 13:36:03 -07:00
Yassin Kortam
51305536bf
fix(proxy): coerce default_internal_user_params.max_budget to float on config load (#32434)
* fix(proxy): coerce default_internal_user_params.max_budget to float on config load

* fix(proxy): log coerced default_internal_user_params and cover absent max_budget in tests
2026-07-16 13:34:43 -07:00
Tin Chi Lo
5421fdfb7e fix(mcp): keep the MCP reference intact when the semantic filter narrows tools
The semantic tool filter replaced each litellm_proxy MCP reference in
data["tools"] with the tools it expanded from that reference. The expansion
defaults to the Responses API tool shape, so a /chat/completions request came
out carrying flat {"type": "function", "name": ...} entries where the provider
transformations expect {"type": "function", "function": {...}}. Anthropic then
raised KeyError: 'function' and Bedrock dropped every MCP tool silently, so the
model answered as if no MCP server were connected.

Replacing the reference also removed the marker the MCP gateway matches on, so
acompletion_with_mcp never ran and tool calls were no longer auto-executed for
require_approval="never", on /responses as well as /chat/completions.

Narrow the reference through allowed_tools instead and leave it in place, so the
gateway still owns expansion and keeps both the per-endpoint tool shape and tool
auto-execution. Expansion already applies any caller-supplied allowed_tools, so
the selection can only narrow a reference further, never widen it.
2026-07-16 13:25:59 -07:00
Yassin Kortam
03e7dc4ac5
build(deps): bump uvicorn lock to 0.51.0 so worker health-check and jitter flags take effect (#33574) 2026-07-16 13:21:29 -07:00
Yassin Kortam
a8ae515bee
fix(proxy_cli): reap orphaned prisma query-engine processes when a worker dies (#33424)
* fix(proxy_cli): reap orphaned prisma query-engine processes when a worker dies

When the proxy runs multi-worker (uvicorn multiprocess supervisor or the
gunicorn arbiter), a worker that crashes or is force-killed never runs its
in-process atexit cleanup, so its prisma query-engine subprocess reparents
to PID 1 and keeps its database connection pool established forever while
the replacement worker opens a fresh pool. Active DB connections then grow
past database_connection_pool_limit with every worker death.

Run a reaper thread in the supervisor process that marks itself a child
subreaper on Linux, scans for adopted query-engine children whose worker
is gone, and terminates them with SIGTERM escalating to SIGKILL after a
bounded grace period. Engines owned by live workers are children of those
workers, never of the supervisor, so they are structurally out of reach.

Resolves LIT-4449
Fixes https://github.com/BerriAI/litellm/issues/33023

* fix(proxy_cli): address review findings on the query-engine reaper

Make start_query_engine_reaper idempotent, reap simultaneous orphans
under one shared grace period instead of serially, and log when a PID
survives SIGKILL. Also regenerate schema.d.ts for the update_team
docstring line that documents the existing mcp_rpm_limit param (fixes
the walk-order-dependent documentation CI failure) and avoid a cast in
the prctl wrapper

* test(proxy): fix reaper idempotency-test isolation and widen coverage

The daemon-thread startup test now stubs threading.enumerate so a
reaper thread left running by an earlier test in the same xdist worker
cannot satisfy the idempotency guard and skip the code under test. Add
coverage for stat-file truncation, non-numeric ppid, non-child reap,
signal-to-dead-pid, subreaper capability, and reaper-loop resilience
2026-07-16 13:16:41 -07:00
Mateo Wang
fba7ac4428
Merge pull request #33222 from BerriAI/litellm_fix_stream_reset_empty_200
fix(streaming): surface upstream connection resets instead of empty 200 streams
2026-07-16 12:43:07 -07:00
yucheng-berri
7fe3dd86a4
feat(logging): add structured budget fields to budget rejection failure logs (#33460) 2026-07-16 12:39:04 -07:00
mateo-berri
40aeb33d61 Merge branch 'litellm_internal_staging' into litellm_fix_stream_reset_empty_200 2026-07-16 12:09:35 -07:00
Yassin Kortam
c012373e1c
fix(router): cast model_info cost values to float in _set_model_group_info (#33556)
Cost values read from deployment model_info can be strings when the
config YAML contains scientific notation with an integer mantissa
(e.g. 1e-05), which YAML 1.2 parsers such as PyYAML 6.x treat as a
string. Comparing that string against the running float aggregate in
_set_model_group_info raised TypeError and broke /model_group/info,
the prometheus remaining-usage callback, and the
x-litellm-response-cost header. Coerce input/output cost values to
float before comparing and storing them.
2026-07-16 12:05:36 -07:00
yucheng-berri
899ddef219
feat(logging): add user and team level spend and budget to StandardLoggingPayload metadata (#33459)
* feat(logging): add user and team level spend and budget to StandardLoggingPayload metadata

* fix(logging): include user and team budget fields in dummy standard logging payload
2026-07-16 12:01:11 -07:00
tin-berri
db800152c0
Merge pull request #33450 from BerriAI/litellm_mcp_issuer_anchored_discovery
feat(mcp): issuer-anchored OAuth discovery (RFC 8414 §3.3) to close the authorization-server mix-up
2026-07-16 11:55:35 -07:00
Mateo Wang
582907d1ab
Merge pull request #32255 from BerriAI/litellm_fix_openrouter_streaming_usage_cost
fix(streaming): use provider-reported usage cost for OpenRouter streams
2026-07-16 11:18:01 -07:00
tin-berri
748ccde5fd
Merge pull request #33318 from BerriAI/litellm_semantic_filter_lazy_sync
fix(mcp): index authed request-time tools missing from the semantic filter startup index
2026-07-16 11:08:32 -07:00
Yassin Kortam
df51cebcd3
fix: remove dead user-cache lookup with None key in spend-update path (#33555)
With litellm_settings.enable_redis_auth_cache enabled, user_api_key_cache
is Redis-backed. _update_user_db performed a cache lookup with
key=user_id where user_id can be None; the in-memory cache tolerates a
None key but Redis raises redis.exceptions.DataError (Invalid input of
type: NoneType) on every spend update for requests without a user_id.

The looked-up value was never used by any subsequent code, so the lookup
is removed along with the user_api_key_cache parameter it existed for.
Spend updates for users, end users, and the global proxy budget are
unchanged
2026-07-16 10:39:53 -07:00
devin-ai-integration[bot]
260d1eae8e
fix(cli): make CLI output ASCII-only so it doesn't crash legacy Windows consoles (#33465)
* fix(cli): force UTF-8 output so emoji don't crash the CLI on Windows

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

* test(cli): drop dead flush calls flagged by review

* fix(cli): replace non-ASCII CLI output with ASCII so legacy Windows consoles don't crash

---------

Co-authored-by: ryan <ryan@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-16 10:35:06 -07:00
yuneng-jiang
ff06119aa9
Merge pull request #32853 from BerriAI/litellm_/guardrail-monitor-details-fix-8845d6
fix(guardrails): show YAML-defined guardrails in the Guardrail Monitor
2026-07-16 07:10:10 -07:00
devin-ai-integration[bot]
ebc6fdb4c2
fix(cli/anthropic): unblock lite autoroute proxy deps, adaptive thinking, and thinking+signature streaming (#33507) 2026-07-16 00:44:00 -07:00
devin-ai-integration[bot]
5a0e1dd1dd
feat(autoroute): prompt for semantic keywords per tier in configure wizard (#33508) 2026-07-16 07:43:41 +00:00
devin-ai-integration[bot]
bbd52984b1
fix(anthropic): stop 500 on combined thinking+signature streaming chunk (#33505) 2026-07-16 00:24:02 -07:00
Tin Chi Lo
afd7917b8b fix(mcp): separate issuer identity from anchoring so carry-forward keeps endpoints
Making the in-memory issuer reflect a trust-on-first-use discovered value fixed
the registry/row token-identity drift, but it overloaded a single field: the
carry-forward gate keyed on issuer truthiness as a proxy for "endpoints are
anchored to a pinned issuer, fail-closed". A discovered issuer is truthy yet not
anchored, so a resource-rooted server that had learned its issuer would drop its
last-known-good endpoints on a transient discovery blip instead of carrying them
forward.

Anchoring is now a first-class property rather than a proxy. MCPServer carries
issuer_is_anchored, set at both build paths from the single _uses_issuer_anchor
definition (a pinned issuer on a discovery auth type). issuer stays the identity
value used by the token-identity tuple and the serializers; issuer_is_anchored is
the provenance value the carry-forward gate reads to decide fail-closed. The two
properties can no longer be conflated, so a discovered issuer keeps its
resource-rooted endpoints carrying forward while a pinned issuer still fails
closed.

Regression tests pin both directions: a discovered-but-not-anchored server
restores its endpoints on a discovery blip, an anchored server does not, and the
build sets issuer_is_anchored true only when the issuer is pinned
2026-07-15 23:11:37 -07:00
devin-ai-integration[bot]
bf3a058781
feat(complexity_router): enable session_affinity by default (#33500)
Pin a session's first-turn model for the rest of the session by default
instead of reclassifying every turn. Keeps multi-turn sessions on a single
model, preserving provider prompt caches and avoiding cross-model
conversation-history errors (e.g. Anthropic rejecting a thinking block
produced by a different model). Requests without a resolvable session_id are
unaffected. Set session_affinity: false to opt out.

Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-15 21:54:15 -07:00
Krrish Dholakia
cf90445574
feat(cli): add lite up/down to ambiently route Claude Code through the proxy (#33231)
* feat(cli): add `lite up`/`lite down` to ambiently route Claude Code through the proxy

Patches ~/.claude/settings.json in place (env.ANTHROPIC_BASE_URL + apiKeyHelper
via `lite auth print-token`) so any `claude` session started afterward, from
any terminal, routes through the local LiteLLM proxy with no wrapper command
needed, unlike the existing `lite claude` subprocess-exec approach. Backs up
the original file first and restores it on Ctrl-C/SIGTERM, or via `lite down`
after an unclean exit. Cursor is not supported: no equivalent file-based config
to patch.

* feat(cli): add lite autoroute to QA complexity-based auto-routing against a real proxy (#33249)

* feat(cli): add lite autoroute to QA complexity-based auto-routing against a real proxy

Lets a customer try litellm's complexity_router against models they already
have on their existing, unmodified production proxy, with no config.yaml
edits and no new infra. lite autoroute configure discovers accessible
models via /model_group/info and walks through tier assignment (plus
optional LLM classifier / semantic matching / adaptive selection); every
referenced model becomes its own litellm_proxy/<name> deployment forwarding
back to the real proxy with the real key, so every actual call, routed
completions, classifier calls, embedding calls, still lands on their real
proxy. lite autoroute up launches that generated config as an ephemeral
local proxy, patches ~/.claude/settings.json to point Claude Code at it, and
streams routing decisions live; Ctrl-C/SIGTERM (or lite autoroute down
after an unclean exit) restores everything.

Also adds lite model-groups list (a thin CLI wrapper over the existing
ModelGroupsManagementClient), and generalizes up.py's settings-backup/restore
helpers to take explicit paths so this feature can reuse them instead of
duplicating the logic.

Depends on litellm_lite_up_down (#33231) for that generalization.

* feat(cli): allow multiple models per autoroute tier

complexity_router already supports a pool of models per tier (randomly
picked per request; adaptive mode specifically needs a pool to choose
within), but the configure wizard only ever let you assign one. Tiers are
now a tuple of model names; the wizard prompt accepts comma-separated
indices to pick more than one per tier.

* feat(cli): fuzzy model picker and auto-route Claude Code to autorouter

Numbered-index selection didn't scale past a handful of models, so switch
the tier picker to InquirerPy's fzf-style fuzzy search. Also set
ANTHROPIC_DEFAULT_{SONNET,HAIKU,OPUS}_MODEL to "autorouter" in Claude
Code's settings, since Router resolves auto-router deployments by literal
model name with no wildcard support, so a "*" catch-all model_name would
never match real traffic.

* feat(cli): allow installing lite CLI from source via LITELLM_CLI_REF

Lets testers try an unreleased branch's CLI changes with the same
curl-piped installer, instead of waiting for a PyPI release.

* fix(ci): modernize type hints to clear ruff strict-rule budget

* fix(ci): bump httplib2 and setuptools to patched versions

Clears osv-scan findings for PYSEC-2026-3444 and PYSEC-2026-3447.

* fix(cli): write autoroute's secret-bearing files with mode 0600

commands.py wrote config.yaml (embeds the real proxy key) and Claude
Code's settings.json (embeds the ephemeral proxy's master key) with
plain open(), landing at the umask-derived default (commonly 0644)
until a later chmod call caught up. That window, and the missed case
where settings.json already exists (chmod never ran at all there),
left a credential-bearing file readable by another local account.

secure_create() fixes the mode via fchmod on the fd before any
content is written, covering both the brand-new-file and
already-exists cases, and commands.py/wizard.py now route their
sensitive writes through it.

* docs(cli): warn that a stale Claude Code session can leak to a squatted port

lite autoroute up's master key is embedded statically (unlike lite up's
apiKeyHelper, resolved per request), so a Claude Code session still
running after teardown keeps sending it, along with prompt content, to
a now-unbound loopback port that another local account can bind. This
is the same one-time-patch tradeoff lite up already accepts, just with
a static secret instead of a re-resolved one -- document it in the
README's Caveats section and surface it in the teardown message itself.

* fix(cli): address greptile review feedback on autoroute PR

- terminate the ephemeral proxy child process when its health check
  fails, instead of leaking an orphaned, unrecoverable process bound
  to the port
- replace bare assert isinstance checks (no-ops under python -O) with
  click.ClickException in the model-groups list and configure wizard
  code paths
- close launch_proxy's log file handle once the child process has
  inherited its fd, instead of leaking it
- add build_generated_proxy_config to config.py's __all__

* fix(cli): close TOCTOU window in lite up's settings backup write

write_backup wrote the backup (which can embed the original
apiKeyHelper/settings content) with plain open() + a chmod call after
the fact -- the same permissive-until-corrected window already fixed
for autoroute's config.yaml and Claude settings writes, and missed
entirely when the backup file already exists with broader permissions.

Moves secure_create (atomic-enough 0600 via fchmod before any content
is written) to up.py, the module both lite up and lite autoroute
share, and has autoroute/process.py import it from there instead of
keeping its own copy.

* fix(cli): refuse autoroute up when a stale backup exists from a crash

The pid-record check only catches a still-live duplicate process; a
SIGKILL'd `up` leaves no live pid but does leave AUTOROUTE_BACKUP_PATH
behind. Without this guard, a fresh `up` overwrote that backup with
the currently-patched Claude settings instead of the true originals,
so `down`/Ctrl-C would restore the wrong content permanently. up.py's
`lite up` already guards the analogous case; mirror it here.

* fix(cli): bind the ephemeral autoroute proxy to loopback only

proxy_cli.py defaults --host to 0.0.0.0 when not passed explicitly.
launch_proxy never passed it, so the ephemeral proxy -- despite every
base_url in this module being built from 127.0.0.1 -- was actually
reachable from other hosts on the network, including its
unauthenticated-until-config-lands routes before the master key is
wired in.

* docs(cli): show curl install for the autoroute QA flow

Points readers at scripts/install-cli.sh's curl one-liner instead of
assuming uv/pip is already set up, and documents the LITELLM_CLI_REF
override for trying an unreleased branch or commit.

* fix(cli): surface a clean error on an empty or corrupt autoroute config

A configure run killed between secure_create's O_TRUNC and the write
completing leaves an empty config.yaml on disk. The next up read that
via yaml.safe_load (None) into the generated-config TypeAdapter
uncaught, surfacing a raw pydantic.ValidationError instead of pointing
the user back at `lite autoroute configure`.

* fix(cli): bind lite up's apiKeyHelper to the proxy it was started against

_ensure_fresh_login only checked token freshness, not which proxy the
cached token belonged to, and resolve_api_key_helper built a bare
`lite auth print-token` command with no --base-url. A user logged into
proxy A who ran `up --base-url proxy-b` (or LITELLM_PROXY_URL=proxy-b)
would silently get proxy A's real token wired into Claude Code's
apiKeyHelper; since apiKeyHelper is invoked bare, print-token's
existing origin check never engaged, so proxy B -- attacker-controlled
or not -- received every subsequent request's Authorization header
carrying proxy A's credential.

_ensure_fresh_login now requires the cached token's base_url to match
before treating it as usable, forcing a fresh login for the selected
proxy otherwise. resolve_api_key_helper now takes that base_url and
threads it through as an explicit --base-url, so print-token's
existing (but previously unreachable in the apiKeyHelper flow)
base_url_explicit check actually enforces the match at request time
too.

* fix(cli): surface clean errors instead of raw tracebacks in lite up/down

load_json_or_empty and read_backup both delegate to pydantic's
validate_json, which raises ValidationError on invalid JSON or a
non-object root -- neither up() nor down() caught it, so a corrupt
settings or backup file surfaced an unformatted Python traceback
instead of a clean CLI error. Both now convert to UpError, and down()
(previously uncaught entirely) and up()'s teardown path now handle it.

restore_claude_settings also gained a parent.mkdir guard before
rewriting CLAUDE_SETTINGS_PATH: if ~/.claude/ was removed while `lite
up` was running, the restore would crash before deleting the backup
file, permanently stranding it and breaking every future `lite down`.

* docs(cli): call out env-var auth for autoroute commands

* fix(cli): clean up leaked proxy and surface clean errors in autoroute

Three related gaps, all following an UpError getting raised somewhere
that wasn't catching it yet:

- up() left the just-launched ephemeral proxy running with no pid
  record if load_json_or_empty/write_backup/secure_create raised after
  the health check passed, mirroring the existing ProcessLaunchError
  cleanup for the health-check-failure branch.
- _teardown() didn't catch restore_claude_settings raising UpError
  (e.g. a corrupt backup at stop time), which would otherwise escape
  to Click as an unhandled error in the normal-exit path, or print
  "Error in atexit" in the atexit path. up.py's own _restore_once
  handles the identical case the same way.
- read_pid_record let a corrupt PID file surface a raw
  pydantic.ValidationError instead of a clean message, and did so in
  down(), the command specifically meant for crash recovery. down()
  now clears an unreadable pid record and continues cleanup instead of
  aborting, since a corrupt pid file must never block the one command
  meant to recover from exactly this kind of crash.

* docs(cli): warn against running lite up and lite autoroute up together
2026-07-15 21:46:02 -07:00
yucheng-berri
3cea243116
fix(key management): enforce minimum custom key length and mask short keys in key_name (#33462)
* fix(key management): enforce minimum custom key length and mask short keys in key_name

* fix(key management): validate new_key before assignment and sync generated schema docstrings

* fix(key management): lower minimum custom key length default from 20 to 16
2026-07-15 18:27:38 -07:00
Tin Chi Lo
ad73f3a7a2 fix(mcp): keep issuer provenance consistent when it changes or is discovered
Two lifecycle gaps let the issuer trust anchor drift out of sync with the
endpoints it governs. Changing or clearing a previously pinned issuer left the
authorization_url and token_url that were resolved under the old issuer in the
row, so clearing the anchor could revive stale, possibly untrusted endpoints
instead of re-discovering. And a build that discovered an issuer
trust-on-first-use persisted it to the row while the returned in-memory server
kept the issuer unset, so the registry and the row disagreed and the per-user
OAuth token identity, which includes the issuer, differed between that build and
the next rebuild and forced a spurious re-auth.

update_mcp_server now treats a change to a previously pinned issuer the same as a
url or auth_type change and clears the auth-flow-scoped endpoint fields that were
resolved under it. The trigger fires only when an issuer was already pinned and
is now changed or cleared, so establishing one for the first time, including the
trust-on-first-use discovery write-back, does not wipe the fields it just
resolved.

Both build paths, build_mcp_server_from_table and load_servers_from_config, now
construct the server with effective_issuer = manual_issuer or the discovered
issuer, skipping an origin-fallback guess exactly as the persistence does, so the
in-memory object always reflects what the row will hold.

Regression tests pin each case: clearing and re-pointing a pinned issuer clear
the stale endpoints, a first-time establish preserves the discovered fields, and
a build reflects the discovered issuer while an origin-fallback guess is not
reflected
2026-07-15 18:15:12 -07:00
devin-ai-integration[bot]
fac43df9b9
fix(complexity_router): return empty dict from _classifier_call_metadata when metadata is absent (#33452)
* fix(complexity_router): return empty dict from _classifier_call_metadata when metadata is absent

The LLM classifier reads request_kwargs.get("litellm_metadata"), but the proxy stores request metadata under "metadata", so this returned None. _classifier_call_metadata then passed None straight through to the classifier acompletion call, which assumes a dict and blows up with 'NoneType' object has no attribute 'update'; the router swallowed it and silently fell back to heuristic scoring, so the configured LLM classifier never ran. Returning an empty dict keeps the classifier call well-formed.

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

* test(e2e): cover complexity-router LLM classifier routes over the proxy

Add a live e2e regression for the complexity auto-router: a lexically simple but hard prompt ("Is P equal to NP?") is routed by the LLM classifier to the higher-tier anthropic backend, read back from the spend log's model. Before the metadata fix the classifier silently crashed and the router fell back to heuristic SIMPLE scoring on the openai backend, so this test fails pre-fix and passes post-fix.

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

---------

Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-15 17:46:00 -07:00
Tin Chi Lo
032a2f2d76 fix(mcp): enforce the issuer trust anchor at every endpoint adoption site
When an admin pins an issuer, RFC 8414 section 3.3 makes that issuer the sole
authoritative source of the authorization and token endpoints, so a compromised
or misconfigured upstream cannot smuggle a token endpoint by echoing the pinned
authorize URL. The first cut enforced that only on the database build path; the
carry-forward, persistence, config-load, serialization and sanitization paths
could still restore or emit upstream-derived endpoints for an issuer-anchored
server, which is the class of gap the review flagged.

Every site now routes through one predicate. _endpoints_yield_to_issuer returns
all-None whenever the issuer is the anchor, so both build paths,
has_all_upstream_oauth_fields, needs_discovery and the endpoint merge defer to
the issuer. _carry_forward_resolved_oauth_endpoints carries only scopes for an
issuer-anchored server and fails closed on endpoints.
_persist_discovered_oauth_endpoints skips endpoint writes under the anchor. The
two table serializers round-trip the issuer and both non-admin sanitizers redact
it. Scope selection stays resource-driven per the MCP authorization spec:
_fetch_issuer_anchored_oauth_metadata takes endpoints from the issuer document
and scopes from the resource document.

The OAuth metadata resolution and corroboration gating for the database build
path move into _resolve_table_oauth_metadata so build_mcp_server_from_table
stays within the cyclomatic-complexity budget without changing behavior.

Regression tests pin the invariant at each site: the issuer overrides stored
endpoints even when they are populated, carry-forward does not restore endpoints
under the anchor, persistence does not write endpoints under the anchor, a url or
auth_type change clears stale issuer-scoped fields even when resubmitted
unchanged, the Azure heuristic stays reachable under a required issuer, and
anchored metadata takes endpoints from the issuer while scopes come from the
resource
2026-07-15 17:44:21 -07:00
Tin Chi Lo
b3af125078 fix(mcp): keep scopes resource-driven under a pinned issuer
The issuer anchor is for the token/registration endpoints only (the RFC 9700
mix-up). Scope selection stays resource-driven per the MCP authorization spec
Scope Selection Strategy: _fetch_issuer_anchored_oauth_metadata now validates
the issuer document (RFC 8414 §3.3) for the endpoints and separately fetches the
resource's advertised scopes (WWW-Authenticate challenge, else RFC 9728
scopes_supported) for the scope value, instead of using the issuer document's
own scopes_supported. The resource can influence only the requested scope, which
the authorization server and user consent bound (RFC 6749 §3.3), never the token
endpoint.
2026-07-15 16:59:01 -07:00
Tin Chi Lo
031922eec6 feat(mcp): discover-by-default issuer (trust-on-first-use) + stale-clear on url/auth_type change + UI
Discover the issuer from the upstream and persist it trust-on-first-use (fill-empty-only,
frozen thereafter), so admins do not have to type it; an admin-configured issuer always wins
and is never overwritten. Re-pointing the server url now clears the discovered issuer and
endpoints (matching the existing auth_type-change clearing) so a new upstream re-discovers
instead of anchoring on the previous upstream's issuer. Adds the issuer to the per-user OAuth
token identity so re-pointing it purges stale tokens. Surfaces the issuer as an optional,
auto-discovered, overridable field in the create and edit MCP server forms.

C901 gate shows +1 vs staging; that is inherited from the #33317 stack base (delta 0 against
2026-07-15 16:59:01 -07:00