Commit graph

115 commits

Author SHA1 Message Date
tin-berri
d8fda675cc
feat: pre-adoption shadow eval for the auto-router (blind pairwise judge, derived state) (#36587) 2026-08-13 13:15:45 -07:00
devin-ai-integration[bot]
3864e12415
fix(spend): stop losing spend log rows when a flush is cancelled (#34826) 2026-08-12 20:10:50 -07:00
ryan-crabbe-berri
2d12a3ea41
fix(proxy): expand config-defined model access groups when resolving team models for /v2/model/info (#34211)
* fix(proxy): expand config-defined model access groups when resolving team models for /v2/model/info

Teams whose only model grant is a config-defined access group (a model_info.access_groups
name listed in team.models) got an empty /v2/model/info?include_team_models=true result.
_add_team_models_to_all_models passed each team.models entry straight to
llm_router.get_model_list(model_name=...), which never matches an access-group name, so
the group's member deployments were dropped. Runtime auth and /v1/models were unaffected
because they expand team.models through get_team_models first.

Resolve team.models through the same get_team_models resolver before iterating, reusing the
exact path runtime auth and /v1/models trust so the two can't drift again. The get_model_names
and get_model_access_groups accessors are hoisted above the team loop so they run once.

* fix(proxy): keep a literal model whose name collides with an access-group name in listings

A grant string that names both a deployed model and a config access group grants
BOTH at runtime (_check_model_access_helper unions them), but the listing resolver
dropped the literal and substituted the group members, hiding a callable model from
/v1/models and /v2/model/info. Keep the literal when it is also a deployed model so
listings match runtime access exactly. Pure-group names (no collision) are still
replaced by their members. Also rewrites _get_models_from_access_groups to build
its result without mutating the input list.

Addresses the Greptile P1 on this PR.

* fix(proxy): type proxy_model_list param as Sequence to satisfy LIT001 budget
2026-08-12 12:54:36 -07:00
mateo-berri
e53f044d20 fix(proxy): resolve the global SSE keepalive interval through the per-deployment engine
The outer wrap_sse_stream_with_keepalive_pings layer duplicated the
keepalive engine that PR #34423 already runs inside async_data_generator
for chat completions and responses streams, and it kept pinging
deployments whose operator set keepalive_seconds: 0 as a hard disable.
sse_keepalive_ping_interval_seconds is now the global fallback inside
_resolve_keepalive_seconds, so deployment and request values keep
precedence, an explicit 0 still disables, the [1, 300]s clamp applies,
and router-less proxies arm the wrap when the global default is set.
2026-08-11 23:37:43 -07:00
ryan-crabbe-berri
be71a8fdbf
fix(alerting): dedupe scheduled Slack spend reports across pods (#36489)
* fix(alerting): dedupe scheduled Slack spend reports across pods

Every pod ran its own weekly/monthly spend report jobs, prometheus
fallback stats cron, and daily report loop, so deployments with
multiple replicas or uvicorn workers received one copy per pod.

Gate each scheduled send behind the shared PodLockManager redis lock.
The lock is never released: its TTL (the full reporting window for the
weekly interval job, whose per-pod anchors drift by boot time and
jitter) doubles as a sent-this-window marker. acquire_lock returning
None (no redis wired) proceeds, preserving single-pod behavior.

Also generalize the pod lock could-not-acquire log line, which claimed
to be about spend tracking for every consumer.

Fixes #14809

* fix(alerting): harden spend report locks after adversarial review

Weekly lock TTL gets an hour haircut: with ttl equal to the interval,
the winner re-fires just before its own key expires, reacquires without
a TTL refresh, and the key then lapses in time for a trailing pod to
re-send. Job/lock ids move to litellm/constants.py per convention, and
spend_report_frequency now rejects non-positive day counts, which
previously coerced to an every-second schedule and would now compute a
negative lock TTL that silently never sends.

Adds the missing test coverage the review flagged: startup_event's
pod_lock_manager wiring (identity-asserted), the prometheus closure's
positive path, and the ungated immediate prometheus send pinned to
exactly one await.

* test(alerting): consolidate spend_report_frequency validator coverage

Drops a duplicate non-positive-days test and parametrizes the survivor
over the suffix half of the validator too

* fix(alerting): route the startup prometheus fallback send through the pod lock

Greptile caught that the boot-time send still ran once per pod when
PROMETHEUS_URL is set, the same duplication class this PR removes

* fix(alerting): make report lock acquisition non-reentrant

Greptile caught that a pod booting within an hour of the fallback stats
cron sent twice: the startup send takes the lock, then the cron fire
hits acquire_lock's reacquire branch, which returns True for the
holder. Window-marker gates now pass allow_reentrant=False so a live
lock blocks everyone including its holder; leader-election consumers
keep the reentrant default

* test(proxy): give spec'd ProxyLogging mocks a db_spend_update_writer

_initialize_slack_alerting_jobs now reads it for the pod lock manager,
and spec=ProxyLogging blocks instance-only attributes
2026-08-11 12:41:11 -07:00
mubashir1osmani
b0fac57fe4
fix(email): stop duplicate legacy invitation email and fix its onboarding link (#36455) 2026-08-10 23:26:27 -07:00
Deepanshu Lulla
363d56f917
feat(proxy): add per-deployment keepalive_seconds SSE heartbeat to prevent load-balancer timeout on long streams (#34423)
* feat(proxy): add per-deployment keepalive_seconds SSE heartbeat for long-running streams

Adds _iter_with_keepalive, _keepalive_from_deployment_config, and
_resolve_keepalive_seconds helpers to proxy_server.py. When enabled
(keepalive_seconds > 0 in request body or deployment litellm_params),
async_data_generator emits ': ping\n\n' SSE comment frames every N
seconds during idle upstream intervals, preventing load-balancer
idle-timeout drops on long chain-of-thought reasoning streams.

The hot path (keepalive_seconds absent or 0) is a plain async-for with
no per-chunk Task wrapping — zero overhead. Includes 8 new unit tests
covering sentinel emission, hot-path pass-through, early-close cleanup,
priority resolution, deployment-config lookup, and end-to-end heartbeat
emission through async_data_generator.

Registers keepalive_seconds in all_litellm_params (types/utils.py) so
the parameter is not stripped from request bodies. Adds the field to
LiteLLMParamsTypedDict and GenericLiteLLMParams (types/router.py) so
deployment YAML config is parsed and validated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(proxy): narrow BaseException to CancelledError to fix BLE001 strict lint gate

* fix: use explicit None check instead of truthiness in keepalive_seconds extraction

`float(raw or 0)` would treat any falsy value (including the integer 0)
as absent and substitute 0.0 before float() saw it. Replace with
`float(raw) if raw is not None else 0.0` so a caller-supplied zero is
correctly passed through to the `value <= 0` guard that disables
keepalive, rather than being silently overwritten.

* fix(proxy): don't guess a deployment's keepalive_seconds when model_id is missing

When a streaming response lacks _hidden_params.model_id, the fallback that
looks up keepalive_seconds by model_name previously returned the first
configured deployment's value, which could apply the wrong interval (or
override an explicit disable) when multiple deployments share the same
model_name with different keepalive_seconds settings. Only resolve the
fallback when every deployment agrees; otherwise leave it unset.

* fix(proxy): also treat an unset keepalive_seconds as disagreement in the fallback

The model_name fallback for keepalive_seconds only compared configured
values, filtering out deployments that leave the field unset entirely.
That meant a deployment with no keepalive_seconds configured could still
inherit a sibling deployment's interval when model_id is unavailable.
Compare the raw per-deployment value (including None for unset) so an
unconfigured deployment never silently adopts another's heartbeat.

* fix(proxy): deployment-level keepalive_seconds: 0 is a hard disable clients can't override

Previously an authenticated client's request-level keepalive_seconds always
took precedence over the deployment default, including when a deployment
operator explicitly set keepalive_seconds: 0 to disable heartbeats. That let
any client re-enable heartbeats for a deployment the operator opted out of,
using them to keep an idle-looking stream alive past a load balancer's idle
timeout and hold a parallel-request slot open longer than intended.

Treat an explicit deployment-level 0 as authoritative: resolve the
deployment's configured value first, and short-circuit to disabled before
ever looking at the request body if the deployment hard-disabled it.

* fix(proxy): a stale (unresolvable) model_id must not fall through to model_name guessing

A populated _hidden_params.model_id names the specific deployment that
served a stream. If that ID no longer resolves (e.g. a deployment removed
by a config reload mid-stream), the resolver was falling through to the
model_name-based fallback, letting a currently-live sibling deployment's
keepalive_seconds silently apply to a stream it never served. Return None
once a populated model_id fails to resolve, rather than degrading to a
guess.

* fix(proxy): keepalive_seconds is operator-only by default; require deployment opt-in for client override

A security review flagged that a client's request-level keepalive_seconds
could unilaterally enable heartbeats for any deployment, even one that
never configured keepalive_seconds at all, letting an authenticated client
defeat load-balancer idle timeouts and hold a parallel-request slot open
for longer than the deployment operator ever intended, with no way for
the operator to prevent it short of explicitly setting keepalive_seconds: 0.

Add allow_client_keepalive_override (default False) to LiteLLMParamsTypedDict
and GenericLiteLLMParams. _resolve_keepalive_seconds now ignores the request
body's keepalive_seconds entirely unless the resolved deployment explicitly
grants override permission; only the deployment's own configured value (or
disabled, if unset) applies otherwise. An explicit deployment-level 0 still
takes priority over everything, including a grant of override permission.

* fix(proxy): register allow_client_keepalive_override in all_litellm_params

Caught during live proxy verification against the real Anthropic API:
allow_client_keepalive_override was added to LiteLLMParamsTypedDict and
GenericLiteLLMParams but never registered in all_litellm_params, so it
leaked straight through into the provider request body as an unrecognized
field. Anthropic rejected every call on a deployment that had this field
configured with a 400 ("Extra inputs are not permitted"), regardless of
its value. Register it alongside keepalive_seconds so it's stripped
before reaching the provider, matching what keepalive_seconds already
does.

* feat(proxy): support keepalive_seconds via x-litellm-keepalive-seconds header

Some clients (e.g. the Vercel AI SDK) can set custom headers more easily
than extra JSON body fields. Add x-litellm-keepalive-seconds, following
the existing x-litellm-timeout/x-litellm-stream-timeout/x-litellm-num-retries
convention in LiteLLMProxyRequestSetup: the header merges into the same
data["keepalive_seconds"] field the request body already populates, so it
goes through the exact same _resolve_keepalive_seconds precedence and the
allow_client_keepalive_override gate -- a header can't enable heartbeats
for a deployment that hasn't opted in any more than the body field can.

Verified live against the real Anthropic API: the header produces real
heartbeats on an opt-in deployment (88 pings over a genuine long-reasoning
stall) and is silently ignored on a deployment without override permission
(0 pings), matching the existing body-field behavior exactly.

* chore: rebase onto litellm_internal_staging, drop unrelated credential_migration.py reformat, fix budget-ratchet drift

Rebased onto the current litellm_internal_staging (merge-base was 5 days
stale). Dropped the now-redundant schema.d.ts-only regen commit entirely
(the new base's own schema.d.ts already supersedes it) and regenerated
schema.d.ts fresh against the new base.

Reverted litellm/proxy/management_endpoints/credential_migration.py to
exactly match litellm_internal_staging: it was a pure reformat with no
semantic change, unrelated to this PR, flagged by review as unnecessary
noise in an encryption-migration file.

Fixed two lint-budget-ratchet failures caused by the base's ceilings
tightening since this branch last synced (other merged work lowered
ANN401/LIT001 budgets; this code was previously under budget and didn't
change):
- _iter_with_keepalive's aiter param: Any -> AsyncIterator[Any], a real
  narrowing (it's always the result of .__aiter__()).
- _keepalive_from_deployment_config/_resolve_keepalive_seconds's
  request_data param: dict[str, Any] -> Mapping[str, Any], matching the
  existing read-only-dict convention already used elsewhere in this file
  (_apply_ssrf_general_settings, _build_redis_usage_cache, etc.) for
  params that are only ever read, never mutated.
- response/raw params: dropped the explicit `Any` annotation to match
  async_data_generator's own (deliberately unannotated) `response` param,
  its actual caller.
- litellm_pre_call_utils.py's new headers param: dict -> Mapping[str, str],
  same read-only-dict rationale.

* fix(proxy): freeze the transient collections in the keepalive helpers

_iter_with_keepalive and _keepalive_from_deployment_config built a set
literal for asyncio.wait, a set comprehension for the per-deployment
config-agreement check, and two dict-literal fallbacks, all flagged by
the LIT002 mutable-collection-construction gate. Switched to a tuple
for asyncio.wait, a frozenset-wrapped generator plus next(iter(...))
for the config check, and a shared MappingProxyType({}) empty mapping
for the fallbacks.

* fix(proxy): trust metadata.model_info.id over the stale model group after a router fallback

Greptile P1: when a streaming request falls back from model group A to
group B and the response's _hidden_params carries no model_id,
_keepalive_from_deployment_config fell straight through to guessing
via request_data["model"], which still names the pre-fallback group A
since the fallback handler mutates its own local **kwargs copy, not
this dict. request_data[metadata|litellm_metadata]["model_info"]["id"],
by contrast, is mutated on this same dict by
Router._update_kwargs_with_deployment on every attempt including
fallbacks (the same source ProxyLogging._build_litellm_call_info uses
for logging), so check it before falling through to the model-name
guess.

Added two regression tests that fail on the prior code (assert
get_model_list is never called once metadata.model_info.id resolves)
and pass with the fix.

* Revert "fix(proxy): trust metadata.model_info.id over the stale model group after a router fallback"

This reverts commit d7790678645695b20f25880315238b49c31a9143.

* fix(proxy): satisfy the new LIT010/ANN001 gates in the keepalive helpers

litellm_internal_staging picked up a LIT010 (every local/module variable
must be declared Final unless it's genuinely rebound) and tightened
ANN001 (missing parameter annotations) since this branch last synced.
Annotated every single-assignment local and module constant with
Final, suppressed pending's loop-carried reassignment with
# rebind-ok, and typed the previously-bare response/raw parameters as
object with isinstance narrowing at their use sites instead of cast
(LIT006 discourages cast; validate into a concrete type instead).

Also swapped the hand-rolled getattr(response, "_hidden_params", None)
+ isinstance(hidden, dict) check for the existing
get_hidden_params_dict() helper already used for this exact purpose
elsewhere in this file and in common_request_processing.py.

* fix(proxy): re-resolve keepalive_seconds per chunk to track mid-stream fallback

Greptile P1: the router can perform a mid-stream fallback to a
different deployment partway through a stream (MidStreamFallbackError
in router.py), and Router._apply_fallback_hidden_params_to_item merges
the fallback deployment's hidden params onto every subsequent chunk.
But _resolve_keepalive_seconds was only ever called once, before
iteration started, against the pre-fallback response wrapper, so a
stream that fell back to a deployment with a different (or disabled)
keepalive policy kept using the original deployment's interval for the
rest of the stream.

_iter_with_keepalive now takes a resolve_keepalive_seconds(item)
callback and re-resolves after every real chunk using that chunk's own
_hidden_params (which do carry the fallback deployment's identity),
rather than trusting the value picked before iteration began. Updated
the three existing timing tests to inject a constant-returning
resolver, since they pin the sentinel/cancellation mechanics rather
than re-resolution, and added two regression tests (interval lowered
and raised mid-stream) that fail against the prior static-resolve
signature and pass with the fix.

* fix(proxy): keep re-resolving keepalive even when a stream starts disabled

Greptile P1: a stream that starts on a deployment with keepalive off
(or unset) skipped _iter_with_keepalive entirely at the call site, so
a mid-stream fallback to a deployment that enables it never got a
chance to activate heartbeats for the rest of that stream, risking the
exact load-balancer idle-timeout this feature exists to prevent.

_iter_with_keepalive now has an internal fast path for
keepalive_seconds <= 0 that still re-resolves after every chunk (no
asyncio.create_task/wait overhead while inactive, same cost as a bare
async for), so activation from a disabled start works the same way
deactivation and interval changes already do. The caller now only
skips wrapping entirely when there's no router to ever fall back
through in the first place (llm_router is None), rather than whenever
the first chunk's deployment happens to start with keepalive off.

Added a regression test that starts keepalive_seconds=0, has the
resolver enable a short interval on a later chunk, and asserts
sentinels appear afterward; it fails against the prior
call-site-gated code and passes with the fix.

* perf(proxy): memoize keepalive resolution per chunk's model_id

_resolve_keepalive_seconds ran a full llm_router.get_deployment() Pydantic
rebuild after every streamed chunk, even when keepalive was unconfigured
anywhere in the deployment list, since async_data_generator wraps every
stream once a router exists. Caching the result by model_id keeps mid-stream
fallback re-resolution correct while paying the router lookup once per
deployment instead of once per token.

* fix(proxy): expire cached keepalive resolution after a bounded TTL

veria-ai flagged that caching by model_id alone lets an already-in-flight
stream keep evading a live config reload (deployment removed, keepalive
disabled, or client override revoked) for the rest of the stream. Expiring
the memo after _KEEPALIVE_CACHE_TTL_SECONDS bounds that window instead of
freezing the resolved value for the stream's full lifetime, while still
avoiding a full deployment rebuild on every chunk in the steady state.

Also fixes add_litellm_data_for_backend_llm_call's now-required request_data
kwarg in the header-merge test, picked up by rebasing onto
litellm_internal_staging.

---------

Co-authored-by: Deepanshu <deepanshu.lulla@alpha-sense.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-10 16:47:17 -07:00
yucheng-berri
ade5a425e8
fix(proxy): isolate guardrail load failures per row (#36432)
* fix(proxy): isolate guardrail load failures per row

One DB guardrail row that fails to initialize aborted the whole
_init_guardrails_in_db loop, so a single typo'd guardrail type or a
missing required param left the proxy running with zero DB guardrails
registered and requests that should have been blocked reaching the
provider.

Catch per row around sync_guardrail_from_db, log the guardrail name, id
and error, and continue with the remaining rows. The failing row's id is
still added to db_guardrail_ids before the attempt so reconcile_db_guardrails
cannot mistake a live row for a deleted one.

* test(proxy): drop inline note and record reconcile via a handler double

Replaces the patched bound method with an InMemoryGuardrailHandler subclass
that records what reconcile_db_guardrails received, so the test injects a
double instead of swapping a method on a live object.
2026-08-10 14:37:09 -07:00
mateo
860e37597f fix(a2a): align agent list annotation and test with the tuple return type
PR #36020 changed AgentRegistry.get_agent_list to return tuple[AgentResponse, ...], and PR #35163 added a test asserting the result equals []. Both were green on their own branches and only collided once they were both on litellm_internal_staging, so proxy-server has been failing on every PR since with 'assert () == []'.

Nothing user-facing was wrong: get_agents only iterates the result and rebuilds it with comprehensions, and FastAPI serializes a tuple to the same JSON array. The test expectation was simply stale, so it now compares against ().

The get_agents local was still annotated list[AgentResponse] while two branches assign the registry tuple straight into it, so it widens to Sequence[AgentResponse]. That covers both the tuple and the list branches without pretending the value is mutable.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-07 14:08:58 -07:00
devin-ai-integration[bot]
b7749f67f1
fix(proxy): warn at startup when max_budget is set but no database is connected (#36041)
* warn at startup when a proxy-wide budget is set but no DB is connected

litellm.max_budget is only enforced via DB-loaded global spend, so a DB-less proxy silently ignores it. Log a one-time startup warning.

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

* refactor(proxy): inject max_budget into DB-less budget warning

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

* test(proxy): cover DB-less budget warning startup call

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

* test(proxy): pin DB-less budget warning call site

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

* test(proxy): stabilize budget warning call-site pin

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

---------

Co-authored-by: tin <tin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-06 16:27:41 -07:00
Yassin Kortam
09dd167b5a
feat(sgr): make the gateway middleware the source of truth for successful requests (#35717)
SGR has had two independent definitions. The admin UI derived it from
SpendLogs, so it counted what litellm's logging callbacks observed and could
attribute and price. BillableRequestMetricsMiddleware counted what the proxy
actually answered at the ASGI edge, but only exported to OTLP for enterprise
metering. The two disagree by design in places, and the SpendLogs figure goes
quiet whenever spend logging is disabled or the callbacks are bypassed.

This adds LiteLLM_DailyGatewayRequests, written by the middleware, and points
the dashboard's Successful Requests tile at it.

Requests fold into an in-memory map at record time rather than going through a
queue like the spend path. A count is a pure aggregate, and every dimension of
the key is chosen by the proxy from a closed set: the date, the category, and a
route that the classifier maps to one of a fixed list of strings rather than
passing the raw path through. Nothing a caller sends can add a key, so the fold
and the table are bounded by (days x categories x routes) however much traffic
arrives; the spend queue blocks once full, which is not acceptable in the
response path. A scheduler job drains it on the existing batch interval, and a
failed flush merges its counts back so a database blip undercounts nothing.

The middleware previously returned early when no billing recorder was
injected, which is the unlicensed case. The new sink is not license-gated, so
that early return now requires both sinks to be absent. The billing recorder
keeps its 2xx-only gate; the sink takes every status so failed_requests is
real. The sink is not told which deployment served the request, unlike the
billing recorder. That id is a sha256 over litellm_params, credentials
included, so a caller who puts a credential in the request body mints a fresh
one per distinct value. No configuration is needed for that: api_base and
base_url are on _BANNED_REQUEST_BODY_PARAMS and need allow_client_side_
credentials, but api_key is not on that list, and both reach the same
_handle_clientside_credential branch. The read endpoint aggregates the
dimension away regardless, so the key is better off without it.

The new table carries no key, user or team dimension, so /gateway/daily/activity
is restricted to proxy admin roles and the per-key and per-model breakdowns
keep reading the daily spend tables. The old path is left running and marked
with TODOs.

A fetched result carries the range key it was fetched for, and the render
selects it only when that key matches the range on screen. Both the gateway
counts and the spend aggregate go through that rule: the request tiles read the
first and fall through to the second, so stamping only one of them would leave
the tile showing a superseded range by the other route.

The paginated pages behind that aggregate are reached through a failure flag,
so the flag is stamped too. A flag left over from the previous range would let
those pages through while a new range is in flight, which is the same defect
one fallback further down.
2026-08-05 12:40:47 -07:00
ryan-crabbe-berri
2792887e47
fix(proxy): give proxy_admin_viewer read parity with proxy_admin (#35851)
* fix(proxy): give proxy_admin_viewer read parity with proxy_admin

Route-level checks already default-allow management GETs for the viewer
role, but ~15 handlers compared user_role to PROXY_ADMIN only, dropping
viewers into regular-user scoping (/key/list, /user/info, /model/info,
guardrails, prompts, agents, memory, workflows, MCP catalog, coordination
redis settings, credential migration check, enterprise projects). Swap
those read paths to user_api_key_has_admin_view; write gates unchanged.

The dashboard now presents the viewer session as Admin for all gating
(effectiveSessionRole) so every page fetches with admin visibility, with
userRoleLabel/isViewOnly preserving the account-menu label and the
playground cost guard. The server remains the write authority.

* refactor(agents): remove side-effectful health_check param from GET /v1/agents

Addresses a security review finding on the admin viewer read parity change:
listing agents with health_check=true made the proxy issue a server-side GET
to every agent URL, so a read-scoped caller could trigger request fan-out
beyond their object permissions. The list endpoint is now a pure read for
every role.

Removes the query param, the URL probing helper and its timeouts, the
AgentHealthCheck httpx provider tag, and the dashboard's Health Check
toggle. Requests still passing health_check=true get the full list back
with the param ignored.

* fix(proxy): keep credential encryption check proxy_admin only

The residual scan behind GET /credentials/migrate-encryption/check loads
every model, credential, MCP, team, and verification-token row and runs a
decryption attempt on each stored value. Extending it to proxy_admin_viewer
let a read-only account repeatedly trigger deployment-wide scans, so the
route keeps its original full-admin gate.

* fix(agents): restore health_check, keep list fast path proxy_admin only

Restores the agent health_check feature exactly as before this PR: the
query param, the URL probing helper, the httpx provider tag, and the
dashboard toggle all return, so existing callers keep the filtering
contract. The viewer expansion is instead reverted at its source: the
GET /v1/agents admin fast path stays PROXY_ADMIN only, so a
proxy_admin_viewer goes through the object-permission scoped branch as
before and cannot fan out health checks beyond their allowlist. The
viewer read of a single agent stays viewer-inclusive since it has no
side effects.
2026-08-05 18:33:55 +00:00
ryan-crabbe-berri
124d08d592 perf(proxy): read budget-window spend from the maintained window table
Per-window budget enforcement aggregated LiteLLM_SpendLogs on every cold
counter and on every 5s authoritative floor check. SpendLogs has no index on
api_key or team_id, so each check range-scanned the highest-volume table.

Reads now hit the LiteLLM_BudgetWindowSpend row by primary key and only fall
back to the aggregate when no row exists for the window being enforced. A row
is current when its window_start is at or past the caller's expected start, so
a pod holding a stale reset_at trusts a window another pod already rolled
instead of summing the previous window back in.

window_duration is threaded from the budget_limits entry through to the read
rather than parsed back out of the counter key. The reader never writes rows.
2026-08-04 18:56:32 -07:00
ryan-crabbe-berri
9ea5cfce0e
fix(proxy): persist periodic reload schedule state so status survives restarts and fires without store_model_in_db (#35165)
* fix(proxy): persist periodic reload schedule state so status survives restarts and fires without store_model_in_db

The model cost map and Anthropic beta headers reload schedules kept their
last-run time in a per-pod module global, so GET /schedule/*/status reported
last_run null after any restart and the Admin UI showed the reload as never
having run. The reload check also only ran from the add_deployment job, which
is registered only when store_model_in_db is true, so config-file deployments
stored a schedule that never fired.

Persist last_run_at and reload_requested_at as dedicated columns on
LiteLLM_Config, owned by the reload job and manual reload endpoints, while the
schedule endpoints own the param_value JSON (interval_hours); no writer can
clobber another's fields. Serve status entirely from the row. Register the
check as its own periodic_reload_job outside the store_model_in_db gate.
Replace the force_reload boolean with a reload_requested_at timestamp each pod
compares against its own in-memory last reload, so a manual reload reaches
every pod exactly once instead of being cleared by the first poller. Run the
blocking fetches via asyncio.to_thread, and stamp last_run_at with update_many
so a schedule cancelled mid-poll is not resurrected.

* fix(proxy): compare reload requests against pod data age seeded at boot

A pod that had never reloaded kept its in-memory clock at None, and with no
interval configured nothing ever set it, so every manual reload request was
ignored by every pod except the one serving the click (Greptile P1 on the
previous commit). Seed the per-pod timestamp at boot as the time its data was
loaded and reload whenever a request or the interval is older than that, which
also removes both None special cases from the due predicate. A schedule whose
row has no last_run_at fires on the next tick so the first run does not wait a
full interval.

* fix(proxy): scope reload persistence to the model cost map and seed the pod clock from the actual load time

Revert the Anthropic beta headers reload path to its previous JSON-flag
implementation so this PR only changes the price data reload; the beta headers
path keeps working exactly as before and can migrate to the shared module in a
follow-up. The unused columns on its config row are inert.

Seed model_cost_map_loaded_at from the timestamp get_model_cost_map records at
the actual import-time fetch instead of ProxyConfig construction time, closing
the startup window where a manual reload request stamped between the fetch and
the constructor compared as older than the pod's data and was skipped
(Greptile P1 on the previous commit).

* refactor(proxy): drop the legacy force_reload backfill from the reload tracking migration

The backfill only carried over a manual reload clicked in the seconds before an
upgrade, and every upgrade restarts the pods, which re-fetch the cost map at
import and so already deliver what that request asked for. Removing it makes
the migration schema-only, so prisma db push and prisma migrate deploy leave
the database in the same state instead of diverging on a data statement that
only one of them runs.

* fix(proxy): stamp reload timestamps at the precision they are stored at

Postgres stores these columns as TIMESTAMP(3) while Python stamps microseconds,
so a pod comparing its in-memory clock against the persisted copy of the same
instant read as newer and skipped the reload request it had just recorded.
Truncate every stamp to milliseconds at the source, and floor the boot seed the
same way, so the in-memory value and its persisted copy compare exactly.

* fix(proxy): identify manual reloads by revision instead of comparing timestamps

Comparing a request timestamp against each pod's data age made correctness depend
on clock resolution: Postgres stores TIMESTAMP(3) while Python stamps microseconds,
and two events inside the same millisecond are indistinguishable no matter how the
comparison is written.

Replace reload_requested_at with a reload_revision counter the manual reload
endpoint increments atomically in the database. Each pod records the revision it
last applied and reloads whenever the row's differs, so a request reaches every pod
exactly once regardless of clock skew or precision, and concurrent requests publish
distinct revisions instead of overwriting one another. A pod adopts the current
revision on its first poll, since data it loaded at boot already satisfies any
earlier request. Interval reloads still key off the pod's own data age, where hour
scale comparisons make precision irrelevant.

* fix(proxy): seed the applied reload revision at startup

A pod adopted whatever revision it found on its first poll, so a manual reload
published while the pod was starting was marked applied without ever being
served and the pod kept the prices it fetched at import. Read the row once at
startup instead, right after that fetch, and treat a missing row as revision 0

* style(tests): revert incidental reformatting of test_proxy_server.py

An earlier ruff format run reflowed the whole file from its 88-column
formatting, adding ~1150 lines of churn unrelated to this PR. Replay only
the real test changes onto the original formatting

* fix(proxy): serve an outstanding reload request on a booting pod

Seeding the applied revision at startup left a window: a manual reload
published after the import-time cost map fetch but before startup read the
row was marked applied without ever being fetched, stranding that pod on
stale prices when no interval was configured. A pod now starts unapplied and
serves any outstanding request on its first poll, which costs one redundant
fetch per boot and removes the window along with the seeding step

* fix(proxy): accept a reload interval still encoded as JSON text

param_value is written with safe_dumps, and a raw row read can return it
decoded or as a string depending on the driver. Strict validation rejected
the string, so the schedule read as disabled and an admin's configured
reloads silently stopped. Mirrors the guard ConfigRepository.get_param
already carries for the same column

* fix(proxy): cancel a reload schedule without resetting the revision

* fix(proxy): null the interval in JSON so cancelling keeps the revision

prisma rejects a null literal for a Json? column, so update_many writes an
interval-less object instead. The fake config table now rejects the same input
the database does, which is what the live run caught and the mock did not.

Also records the run before adopting the revision, so a failed status write
leaves the request unserved for the next poll rather than reporting a run that
never landed.

* fix(ui): match the CI-generated user_role union order in schema.d.ts
2026-08-04 15:42:57 -07:00
devin-ai-integration[bot]
355ae9989b
fix(proxy): propagate user_email and bind api_key on JWT auth attribution paths (#34331)
* fix(proxy): propagate user_email and bind api_key on JWT auth paths

Standard JWT auth built UserAPIKeyAuth with user_id but never user_email, and the first auto-registered request early-returned a key with token set but api_key unset, so spend-log attribution logged user_api_key_user_email and user_api_key_hash as null. Bind api_key to the token hash on the auto-registered key, copy user_email from the resolved user object on both the standard and auto-register JWT paths, and warn when enable_jwt_auth/litellm_jwtauth are placed at the config top level where they are silently ignored.

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

* test(proxy): cover misplaced top-level JWT config warning

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

---------

Co-authored-by: shivam <shivam@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: ryan <ryan@berri.ai>
2026-08-04 19:05:39 +00:00
ryan-crabbe-berri
abe3289398
fix(proxy): retry model cost map fetch with Retry-After-aware backoff and keep current map on reload failure (#35739)
* fix(proxy): retry model cost map fetch with Retry-After-aware backoff and stop downgrading to the packaged backup on reload failure

A 429 or transient network error during a manual or scheduled model cost map
reload used to silently replace litellm.model_cost with the stale backup JSON
bundled in the installed wheel, stamp the reload as successful, and clear the
force_reload flag, so a fleet could serve months-old pricing until the next
interval. Runtime reloads now go through refetch_model_cost_map, which retries
429/5xx/transport errors up to 3 times honoring Retry-After (capped at 30s,
exponential backoff with jitter otherwise) and returns a failure value instead
of the backup when the fetch or integrity validation fails. On failure the pod
keeps its currently loaded map, the periodic job leaves last_run and
force_reload untouched so it retries on the next config poll, and the manual
endpoint returns 502 with the reason instead of reporting a fake success.
Startup behavior is unchanged: boot still falls back to the packaged backup
since there is no previously loaded map to keep.

* fix(proxy): use shared async httpx client for cost map reload and make retry tests CI-env-proof

The reload fetch now goes through get_async_httpx_client with a dedicated
httpxSpecialProvider.ModelCostMap pool instead of constructing a raw
httpx.AsyncClient, so it inherits deployment-level TLS and transport settings
and passes the ensure_async_clients gate. Tests inject a MockTransport-backed
client through the same seam. An autouse fixture clears
LITELLM_LOCAL_MODEL_COST_MAP, which CI exports and which short-circuited the
retry tests; the two TestPriceDataReloadAPI tests and the config sync pubsub
reload test that still patched get_model_cost_map now patch
refetch_model_cost_map instead.
2026-08-03 22:08:58 -07:00
Yassin Kortam
8cf2e2eb43
fix(proxy): apply key/team router_settings.model_group_alias (#35486)
Key and team `router_settings.model_group_alias` was accepted, persisted and
echoed back by `/key/info`, but never applied at request time, so the request
ran on the group the caller asked for. `route_request` forwards only the
settings the Router accepts as per-request kwargs, and `model_group_alias` is
not one of them: the Router resolves aliases from its own instance attribute,
which holds the global config map and is shared across requests.

Resolve the alias in the proxy instead, alongside the existing model-alias
rewrites and ahead of the pre-call hooks, so per-model limits and guardrails
key off the group that actually serves the request. Authorize the alias target
before the rewrite; model access was checked against the requested group, so a
key whose alias points at a group it cannot call gets the usual 403 rather than
being quietly served it.

Resolves LIT-4879
2026-08-03 22:09:47 +00:00
Mateo Wang
9161e3ba67
Merge pull request #35436 from BerriAI/litellm_redis_pubsub_config_sync
feat(proxy): push config sync to pods via redis pub/sub
2026-08-01 14:06:59 -07:00
Yassin Kortam
704b9da8ab
fix(a2a): keep config-defined agents registered and accept the documented agents: key (#35163)
The public A2A guide tells users to declare agents under a top-level
`agents:` key, but the proxy only ever read `agent_list:`, so the
documented config was silently ignored and GET /v1/agents returned an
empty list. Accept `agents` as the documented spelling and keep
`agent_list` working for anyone who found it by reading the source.
Selection is by key presence, so an explicitly empty `agents: []` is not
overridden by leftover legacy entries.

Config-defined agents were also dropped on any database-backed gateway:
the periodic reload rebuilt the registry from the DB rows plus a module
global that was declared and never assigned. The registry now remembers
the agents it loaded from config.yaml and replays them on every rebuild.
A database row wins a name collision, mirroring how config-declared MCP
servers are unioned under the database registry, so name lookups and
deregistration keep addressing exactly one agent.

Resolves LIT-4978
2026-08-01 12:39:13 -07:00
mateo-berri
b2fd79f487 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_redis_pubsub_config_sync
# Conflicts:
#	litellm/proxy/proxy_server.py
2026-08-01 09:29:24 -07:00
mateo-berri
77e490a695 fix(proxy): publish router_settings changes so peer pods apply them on resync
add_deployment already reapplies DB router settings through _update_llm_router,
so gating router_settings out of the pub/sub publish set left the push path
covering less than the resync actually applies
2026-07-31 22:52:00 -07:00
ryan-crabbe-berri
b4ff05be8e
fix(proxy): stop model writes 500ing on another pod's delete (#35400)
* fix(proxy): stop model writes 500ing on another pod's delete

A model write judges the reload it triggers by diffing this pod's router before and
after, and reports anything that stopped serving as damage. On a pod that has not yet
polled a delete another pod made, the snapshot still lists that model; the reload then
evicts it because the db no longer has it, and the guard reads its own correct
reconcile as degradation. The row is written and served, but the caller gets a 500.

Since propagation between pods is a 30s db poll, any delete followed by a create
inside that window can land on a pod that has not caught up, so a delete-then-create
pair returns 500 whenever the two requests hit different pods.

_delete_deployment already computes exactly the set that settles it: the ids the db
and config still want. Thread it up through _update_llm_router, add_deployment and
clear_cache to the verdict, and intersect the drop set with it so an id the db no
longer has stops counting as collateral. Where no reconcile ran the set is None and
every drop is still reported, so a genuinely broken reload is caught as before.

_delete_deployment now returns that set instead of a delete count; the count had no
callers in the proxy, and the tests asserting it already assert the eviction calls.

* test(proxy): fold reload-verdict test commentary into docstrings and assertions

Greptile flagged the inline comments against the repo's no-new-comments rule. The
case-by-case context moves into the test docstring, and the two return-contract
assertions carry their reasoning as failure messages instead.

* test: fix clear_cache mock return type in model block/unblock tests
2026-07-31 18:10:48 -07:00
Tin Chi Lo
fec7f5f246 feat(ui): give auto-routers their own tab on Models + Endpoints
Auto-routers had no home and no list. The create form was mounted in two unrelated places,
inside Models + Endpoints > Add Model and again under Cost Optimization, and neither showed
which auto routers already existed; seeing or editing one meant finding its row in the models
table and drilling in. They now get a dedicated Auto-Routers tab beside All Models, listing
every auto_router/* deployment with create, edit and delete in one place, and both former
entry points are removed.

Creating opens in a shadcn dialog rather than swapping the whole panel out, so the list stays
on screen behind it; the dialog caps its height and scrolls, since the complexity form is long.
The form's own heading goes with it, the dialog header owning that now.

An auto router is a routing construct rather than a deployment, so it also comes off the All
Models table. That table pages server-side off total_count, so a client-side filter would page
over a total including rows it never renders; /v2/model/info therefore gains
exclude_auto_routers (default false, so every existing caller is unaffected) and the filter
runs before the count. /v1/models is untouched, so clients still see auto-routers as models.

Clicking a router opens the same `?model=` drill-in the All Models table uses, so it lands in
ModelInfoView with the full Model Settings, Edit Settings, Edit Auto Router and Delete. An
earlier revision had a bespoke detail page here, which was a partial reimplementation of that
view and showed the router's type twice, once as a Type pill and again as a "Routing strategy"
field saying the same thing. Both are gone.

The auto-router list is keyed under the same `models/list` namespace as the models table
rather than a private one. It reads the same /v2/model/info data, and six call sites across
the app already invalidate ["models","list"] after a write; a separate key meant an edit made
through ModelInfoView left the tab stale until a full reload, and every future writer would
have had to remember a second key.

An auto router has no upstream credential, so its detail header drops Update API Key and
Re-use Credentials, and the destructive action names what it removes rather than saying model.
Test Connection was gated on the editor-aware predicate, which let adaptive and quality routers
through to a check that builds its targets from complexity config they do not have; it now
gates on the deployment predicate.

The edit modal also applies the semantic-matching guard the create form has. It renders those
controls now, and the backend raises on semantic_keyword_matching without an embedding model or
keyword rules, so skipping the shared validator turned an inline message into a raw 400.

Whether a row is writable has two independent axes and the dashboard needs both. STRATEGY:
there are four auto_router/* kinds and only complexity and semantic have a form here, so
adaptive and quality must not be handed an editor that would write auto_router_config onto a
deployment storing its settings elsewhere. ORIGIN: a config.yaml row reports db_model false and
the API refuses it whatever its strategy (PATCH /model/{id}/update 404s, POST /model/delete
400s). Capability is derived per capability rather than as one editable flag, because the
constraints differ: editing needs an editor, deleting removes a row by id and never reads its
config, so a DB-created adaptive router stays deletable. Both axes live in
add_model/auto_router_strategies.ts as a declarative table, one record per strategy, so a fifth
strategy is a table row rather than another branch. That also retired four copies of "is this a
complexity router", one of which was written twice in a row in model_info_view.

Creation narrows to the complexity router, which the UI calls Auto-Router v2; the semantic
option was already badged "to be deprecated" in the picker, so the picker goes away along with
the semantic submit path and its validation helper. Existing semantic routers stay editable.

The edit modal mounted ComplexityRouterConfig without the keyword, escalation and
semantic-matching handlers, so those sections never rendered and could only be set at create
time. It now hydrates them from the stored config, and the five keys become managed only when a
caller supplies that state, so a caller rendering no such control still carries them through. A
component-level round-trip test covers it: a payload-builder test cannot see a hydration bug.

A complexity tier is str | list[str] on the backend, and the UI carried three readers of that
rule, one of which dropped a pinned string. They collapse into one owner,
add_model/complexity_router_tiers.ts.
2026-07-29 19:45:02 -07:00
Yuneng Jiang
c3edf2402b
test(proxy): pin both branches of the validation exception handler
Same cause as the otel handler test: this file builds its request as a
SimpleNamespace carrying only `state`, and the validation handler now reads
`request.url.path` to pick an error contract, so the fake needs a url

While here, cover what the two existing tests do not. They only exercise the
proxy-wide 422, and the control plane's 400 problem document was reachable only
through the route test, which registers its own copy of the handler in a local
app rather than the real one. Two cases now pin the real handler directly: a
`/management/v1` path returns problem+json with a `detail` string, and paths that
merely resemble the prefix (`/management`, `/v1/management/foo`) keep the 422
shape their callers parse
2026-07-27 09:59:47 -07:00
Tin Chi Lo
a78130461f feat(mcp): gateway DCR session admission at the aggregate /mcp endpoint (LIT-3637)
Admits a keyless SSO user (no virtual key) at the aggregate /mcp endpoint from a gateway DCR
session bearer, resolving team/org/SCIM/budget authorization fresh on every call.

- Aggregate DCR front door: stateless /register (sealed llm_dcrc_ client ids), SSO-backed
  /authorize + /authorize/complete, and /token minting identity-only session tokens with PKCE,
  single-use codes/flows, and rotating refresh tokens.
- Admission: a session-shaped Authorization at the aggregate scope opens via _admit_gateway_session,
  reloads the live user, and runs the centralized policy gate; failures return the RFC 9728
  invalid_token challenge. Gated on the un-forgeable, server-only mcp_admitted_user_subject marker,
  so virtual-key and JWT auth are unchanged.
- Authorization model: an admitted subject is resolved as one plain UserAPIKeyAuth per grant source
  (its own grants, plus each team it is a live roster member of), each answered by the SAME resolver
  virtual keys use, then unioned. That branch is the FIRST statement of BOTH public resolvers, so no
  single-credential prelude runs for it and a fault in a lookup it never uses cannot deny its grants. A source team counts only while it is a live grantor: roster membership, not
  blocked, and neither the team nor its owning org over budget (enforced through the SAME
  _team_max_budget_check / _organization_max_budget_check owners common_checks uses for keys).
  Each team source carries that team's own org, so the existing org
  ceiling caps it; for a keyless source the org list only ever intersects (a ceiling must not become
  a grant) and an unresolvable ceiling denies rather than silently uncapping, on both the server and
  tool axes. _roster_team_object is the single owner of "which teams count": a team whose roster no
  longer lists the user neither grants servers nor throttles, in one place.
- Rate limits: the subject is bounded by its user rpm/tpm AND by the per-server mcp_rpm_limit of
  the team a call is ATTRIBUTED to — the same single source billing charges, from the same owner. A key charges its one pinned team's bucket; a keyless
  subject has no team_id, so admission stamps each granting team's limit map onto the auth
  (server-only field, stripped from validated input like the marker) and the limiter emits that
  team's mcp_per_team descriptor. Charging every granting team instead would let one cross-team user
  drain several teams' SHARED buckets on a single call and block their other members; and a server
  the user's OWN grant reaches charges no team bucket at all, because no team provided it. Per-KEY
  MCP limits do not apply because there is no key.
- Wrapper channels: the manager-level union treats the admitted subject by the same grant model.
  The admin-role short-circuit and the absolute no_mcp_servers early-return are key-credential
  rules and never apply to it (a session bearer is a third-party client credential, not the
  dashboard, and the subject's opt-out silences only its own source). Operator-open channels
  (allow_all_keys, the user's own BYOM submissions) are owned by one operator_open_server_ids
  helper that BOTH the server union and the admitted tool resolution consult (suppress-BYOM-when-
  explicitly-scoped is a key-credential rule and never applies to the subject, whose user row
  carries the DB-default empty mcp_servers), so an open-channel
  server is default-open for tools instead of listable but uninvokable.
- Redirect URIs: one owner, validate_redirect_uri_shape, decides redirect-URI hygiene (bad scheme,
  fragment, missing host, userinfo, backslash host) and resolves allowlisted native callbacks, shared
  by DCR registration and the OAuth endpoints. Registration keeps a deliberately wider trust policy
  than validate_trusted_redirect_uri: public dynamic registration accepts any https client, and its
  controls are mandatory S256 PKCE plus the consent screen.
- Egress leak-defense: a gateway admission credential (session bearer / bridge envelope) is scrubbed
  from EVERY egress header context, anchored to the credential shape, so it can never be forwarded
  upstream and replayed.
- Single-use guard: auth-code, refresh and connect-flow claims resolve the proxy's cross-worker redis
  cache themselves rather than trusting the cache passed in, and fail CLOSED on a Redis fault instead
  of falling back to a per-worker count that a captured id could replay through another worker.
- Sign-in return_to: one shared, never-raising helper persists a safe return_to for every sign-in
  branch (SSO/Okta/generic and username/password), and every branch RESUMES through the same
  _sso_return_to_redirect the SSO callback uses, so however a deployment signs in the stored value
  is honored identically (same-origin path directly; control_plane_url via the one-time login-code
  handoff). A stale cookie is ignored rather than failing a completed sign-in.

- Budgets, both halves: ENFORCEMENT (an already over-budget team or its owning org stops being a
  grantor, in the source gate) and ACCOUNTING (a team-derived tool call is billed to the granting
  team and ITS org, so that budget accumulates and the right organization is charged). A server the
  user's own grant reaches bills the user; when several teams grant one server the pick is the
  lowest team_id, stable and auditable. Billing rides a COPY, so authorization still sees the full
  union, and it is inert when the target server cannot be resolved from the tool name.

Deferred (tracked): client-selected server scoping of the session token (LIT-4680).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 00:24:28 -07:00
Yassin Kortam
c2ae52709c
feat(proxy): make DB config-reload interval configurable via config.yaml and UI (#34130)
The add_deployment and get_credentials background jobs that keep a multi-pod
deployment in sync with config-in-DB objects (models, credentials, guardrails,
general settings, etc.) polled the database on a hardcoded 30s interval, with
no way to trade convergence latency against DB load.

Expose it as the general_setting proxy_config_reload_interval_seconds (env
PROXY_CONFIG_RELOAD_INTERVAL_SECONDS parsed via get_env_int, default 30),
threaded like the existing proxy_batch_polling_interval knob, and surface it on
the admin general-settings page so it is reachable from the dashboard and
persists to the DB for all pods. Non-positive values are rejected at the UI
(gt=0) and fall back to 30s with a warning on the env/config/DB paths.
2026-07-21 22:03:22 +00:00
Yassin Kortam
e9ac84dc8b
fix(proxy): stop save_config from snapshotting environment_variables into the DB (#34119)
save_config wrote the entire merged config to the DB config table on every call. Because get_config() resolves os.environ/ placeholders to plaintext and merges the environment_variables section, any endpoint that does get_config() then mutates one section then calls save_config() (/add/allowed_ip, delete_callback, model and cost-tracking settings, and others) accidentally persisted an environment_variables row holding all YAML/OS-sourced env vars. Once that row existed the DB overlay shadowed YAML and container env on every subsequent startup, so config/env changes were silently ignored

save_config now pops environment_variables from the DB write unless the caller passes include_env_vars=True. The dedicated /config/update path already writes env vars per-section via _upsert_section, so no current caller needs to opt in

Resolves LIT-2009
2026-07-21 21:23:28 +00:00
mateo-berri
44604620a4 test(proxy): make model_info endpoint tests hermetic to kill an order/merge-skew flake
The model_info / get_model_info_with_id endpoint tests drove refactored
endpoints with bare, unspec'd MagicMock routers and models. Because the
mocks were unspec'd, any attribute or method the (refactored) endpoints
newly read auto-materialized a child MagicMock, and whether that child
was reached depended on process-global state (premium_user, and the real
get_available_models_for_user chain reading litellm globals) that sibling
tests in the same xdist worker mutate. When reached, the MagicMock either
unpacked to empty (a, b = mock.method() -> 'not enough values to unpack
(expected 2, got 0)') or leaked into RouterModelInfo(**model_info) and
failed Pydantic str validation. Pass in isolation, fail under xdist.

The original TestModelInfoEndpoint failure (#33807 CI) was the same class
surfaced by merge skew: #33721 added a get_configured_token_limits unpack
to create_model_info_response, and CI's merge commit ran that against the
un-updated bare-mock test before the #33742 band-aid landed.

Fix (test-only, no product change):
- TestModelInfoEndpoint: mock the real seam (get_available_models_for_user),
  configure the router methods the endpoint actually calls, return a real
  Deployment, and drop the dead proxy_server.get_key_models/get_team_models/
  get_complete_model_list patches the refactor had stranded.
- TestGetModelInfoWithIdBlocked: spec the model mock so unset enterprise
  columns read as None instead of child MagicMocks.
- test_ProxyConfig_get_model_info_with_id_missing_model_id_raises: pin
  premium_user so the asserted AttributeError no longer flips with the
  ambient license global.
2026-07-20 14:28:09 -07:00
devin-ai-integration[bot]
cc45d18e9c
feat(complexity-router): add return_raw_model_name toggle for response model field (#33875)
* feat(complexity-router): optionally return raw model name

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

* test(proxy): restore asyncio import

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

* chore(tests): preserve staging asyncio import

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

* test(proxy): drop unused local asyncio import

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

* feat(dashboard): add complexity router raw model toggle

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

* refactor(complexity-router): move metadata key constant to constants.py

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

* style(proxy-tests): preserve module spacing

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-18 19:24:56 -07:00
devin-ai-integration[bot]
4a297dd611
fix(otel): restore proxy-level error.* attributes on v2 failure spans (LIT-4179) (#33664)
* fix(otel): restore proxy-level error.* attributes on v2 failure spans (LIT-4179)

* refactor(otel): narrow v2 failure hook return type to drop fastapi import (LIT-4179)

---------

Co-authored-by: yucheng-berri <yucheng@berri.ai>
2026-07-18 10:52:27 -07:00
devin-ai-integration[bot]
07e07e6e2b
fix(vertex_ai): exclude Gemini Google Search grounding tokens from input token billing (#33742)
* fix(vertex_ai): exclude Google Search grounding tokens from Gemini input token billing

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

* test(proxy): stub get_configured_token_limits on mocked routers

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-17 21:17:49 -07:00
devin-ai-integration[bot]
8a4f3808ad
fix(proxy): resolve router_settings.plugins dotted paths and load plugins from installed packages (#33644)
Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-17 11:23:18 -07:00
devin-ai-integration[bot]
637fc1f60e
fix(router): tag-aware pre-routing strategy selection for shared model_name (#33691)
* fix(router): tag-aware pre-routing strategy selection for shared model_name

Complexity/auto/adaptive/quality router registries were keyed by model_name
alone, so a second deployment sharing a model_name but carrying different tags
was rejected and every request used the first config. This made tag-based
routing to distinct provider configs behind one alias impossible, surfacing as
401 'Not allowed to access model due to tags configuration' for the second tag.

Each registry now holds a list of tag-scoped strategies and async_pre_routing_hook
selects the entry whose tags match the request before classification, falling
back to a default-tagged then first-registered entry. A repeat of the same
(model_name, tags) pair is still rejected.

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

* test(router): cover tag-scoped pre-routing strategy registry helpers

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

* chore: re-trigger CI

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-17 09:26:07 -07:00
Krrish Dholakia
b96460608d
feat(router): resolve auto-router routing plugins from proxy YAML config (#33251)
* feat(router): resolve auto-router routing plugins from proxy YAML config

Router(plugins=[...]) was Python-SDK constructor only, so proxy/YAML users
had no way to configure it, and the merged pipeline narrowed candidates
from the outer model alias rather than the auto-router's actual tier pool,
making it a no-op for auto_router deployments.

Add complexity_router_config.plugins (dotted-path strings resolved via
get_instance_fn, the same convention litellm_settings.callbacks uses) and
run the resolved plugins against ComplexityRouter's tier pool at every
model-pick site, so a policy plugin narrows what get_model_for_tier
actually returns instead of the outer alias list. adaptive=True with
plugins set now raises at config validation instead of silently ignoring
the plugins, since the bandit selector doesn't consume narrowed pools yet.

Also fixes a latent bug in Router._generate_model_id: it json.dumps every
litellm_params dict value to build a deployment hash id, which crashed
once a live plugin object could land inside complexity_router_config.

* fix(router): use stable class name, not object repr, in model-id json fallback

json.dumps(v, default=str) on a litellm_params dict containing a live
RoutingPlugin instance fell back to object.__repr__'s default
<module.Class object at 0x...>, embedding the instance's memory address.
_generate_model_id's hash (and therefore the deployment id) changed on
every process restart/hot-reload for any deployment with
complexity_router_config.plugins configured, defeating the function's own
"consistently generate the same id" contract and orphaning anything keyed
on that id across restarts (e.g. Redis-backed per-deployment state).

Use the plugin's fully-qualified class name instead, which is stable
across restarts.

* test(router): cover _json_default_stable_id for router_code_coverage gate

router_code_coverage.py's AST scanner requires every router.py function be
called by name somewhere in tests/, and flagged the new
_json_default_stable_id helper from the previous commit.

* fix(router): close two routing-plugin policy-bypass gaps flagged by Veria AI

Session-affinity pin shortcut: async_pre_routing_hook returned a session's
first-turn pinned model on every later turn without ever re-running it
through the plugin pipeline, so a policy plugin (e.g. a budget cap crossed
mid-session) was only enforced on turn one. Now the pin shortcut is
disabled whenever plugins are configured, so every turn re-runs
_classify_and_route (and therefore the plugins).

Plugin resolution validation: get_instance_fn accepts any dotted path and
returns whatever object it finds there, so a misconfigured
complexity_router_config.plugins entry passed proxy startup silently and
only surfaced as a confusing AttributeError on the first request that
reached the plugin pipeline. Extracted the resolution logic into
resolve_complexity_router_plugins() and added an isinstance(...,
RoutingPlugin) check that fails proxy startup immediately with a clear
error instead.

* fix(router): raise instead of falling back to default_model on empty plugin-narrowed tier

default_model was never checked against the configured plugins, so it
functioned as an unconditional escape hatch around whatever policy a
plugin enforces -- a tenant/budget plugin narrowing a tier to zero
candidates could still be bypassed by the fallback. Drop the fallback
entirely for this path; a plugin narrowing to zero is a policy decision,
not something to route around, matching the fail-closed behavior the
Router-level plugin pipeline already uses for the same situation.

Flagged by Veria AI on PR #33251.

* style: ruff format complexity_router.py

* style(proxy): use modern str | None instead of Optional[str] in resolve_complexity_router_plugins

* fix(router): stop default_model short-circuit from skipping plugins on no-user-message path

self.config.default_model or await self._pick_model_for_tier(...) -- Python's
`or` short-circuits on a truthy default_model, so _pick_model_for_tier (and
therefore the plugin pipeline) never ran at all for the no-user-message path
whenever default_model was configured. A tenant/budget plugin's decision was
silently bypassable this way even after the other two policy-bypass fixes,
since this call site had a different shape from the other three pick sites.

Removed the short-circuit; falls through to _pick_model_for_tier ->
get_model_for_tier, which already checks the MEDIUM tier before default_model
-- the same priority every other call site uses.

Flagged by Veria AI on PR #33251.

* fix(router): address Greptile findings on the plugin-bypass fixes

Preserve default_model-first priority in the no-user-message path when no
plugins are configured, instead of unconditionally flipping to the MEDIUM
tier -- the plugin-bypass fix must not silently change model selection for
the (much larger) population of users who don't use plugins at all. Gated
on self.config.plugins, matching the pattern already used elsewhere in
this PR, per CLAUDE.md's guidance against backwards-compat flags when a
plain conditional does the job.

Also close a gap in the plugin validation added earlier:
@runtime_checkable only checks that `run` exists as an attribute, not that
it's a coroutine function, so a synchronous `def run(self, context)`
passed isinstance(resolved_plugin, RoutingPlugin) at startup and only
failed at request time with a confusing TypeError. Added an
inspect.iscoroutinefunction check.

Both flagged by Greptile on PR #33251.
2026-07-14 21:27:14 -07:00
yucheng-berri
5862be3e79 fix(proxy): resolve os.environ/ refs universally in DB-sourced models
Root cause: PR #30867 removed request-time os.environ/ expansion in
BaseAWSLLM.get_credentials. That is only safe if config-load pre-resolves
os.environ/ refs so the value reaching get_credentials is already the real
secret. The YAML config path has always done this. The DB-load path
(ProxyConfig._resolve_db_litellm_param) only re-expanded keys in a hardcoded
whitelist (_DB_LITELLM_PARAM_ENV_REF_KEYS) plus short-circuited env-ref
resolution entirely for team-scoped rows. PR #32256 extended that whitelist
to 18 keys to unblock a customer whose Bedrock model with aws_role_name:
os.environ/BEDROCK_ASSUME_ROLE_ARN broke on v1.90+, but the whitelist is
structurally fragile: every future auth field breaks the same way until
someone remembers to add it

Fix: remove the whitelist and the team-scope short-circuit. The DB-load
resolver now expands os.environ/ on every string field, matching the YAML
path. Trust boundary stays on the write side: only PROXY_ADMIN can create
team_id=None rows, only team admins of a team can create rows scoped to
that team, and the request-body vector is still blocked by
_BANNED_REQUEST_BODY_PARAMS. Team-scoped rows now resolve env refs — this
is a deliberate LIT-3831 threat-model expansion trusting team admins for
env-var reads

Regression tests in tests/test_litellm/proxy/proxy_server/test_proxy_config.py:
- test_ProxyConfig__add_deployment_resolves_env_refs_after_db_decrypt pins
  admin-scoped rows resolve every field (previously api_base stayed literal)
- test_ProxyConfig__add_deployment_resolves_team_env_refs pins team rows
  resolve env refs (previously stayed literal)
- test_ProxyConfig__add_deployment_resolves_env_refs_on_arbitrary_field pins
  the no-whitelist invariant against a made-up field name
- test_ProxyConfig__add_deployment_resolves_env_refs_for_aws_bedrock_auth_params
  (from #32256) still passes
- Path B counterparts (decrypt_model_list_from_db) mirror the above

Left as followups (not fixed here):
- /model/info and /v2/model/info still echo resolved values for fields not
  in the current pop-list (aws_role_name, aws_sts_endpoint, api_base, etc.).
  Fix is to extend remove_sensitive_info_from_deployment; separate PR
- Master-key rotation reads DB rows via decrypt_model_list_from_db which
  now resolves universally, so rotation collapses env-refs into hardcoded
  values. Pre-existing bug for the 6 previously-whitelisted fields; wider
  surface after this PR. Separate PR
2026-07-07 18:31:44 -07:00
Sameer Kankute
42f5b0bd34
fix(proxy): wire general_settings SSRF allowlist to litellm globals (#32243)
* fix(proxy): wire general_settings SSRF allowlist to litellm globals

general_settings.user_url_allowed_hosts was documented in SSRF errors but
never applied at startup, so internal MCP/OpenAPI URLs stayed blocked.

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

* fix(proxy): regenerate dashboard types and satisfy ruff UP006 budget

Use list[str] in ConfigGeneralSettings and run gen:api so schema.d.ts
matches the new SSRF general_settings fields.

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

* fix: normalize ssrf general settings

* fix: clear ssrf allowlists from null settings

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 20:49:55 +05:30
yucheng
29f0b02a83 fix(proxy): also allow os.environ/ resolution for aws_bedrock_project_id, aws_batch_role_arn, aws_workspace_id
Round out the AWS auth-field coverage of _DB_LITELLM_PARAM_ENV_REF_KEYS
so every stringy aws_* field a deployment can pin in the DB resolves
os.environ/ refs at load time:

- aws_bedrock_project_id: Bedrock project/workspace association, banned
  from request bodies via _BANNED_REQUEST_BODY_PARAMS
- aws_batch_role_arn: Bedrock batches role ARN (analog of aws_role_name)
- aws_workspace_id: Claude Platform workspace ID

Verified against three independent sources:
- BaseAWSLLM.aws_authentication_params (all 11)
- LiteLLM_Params-declared AWS fields (all 5)
- every aws_* string read from litellm_params/kwargs/optional_params
  across litellm/ (all 14, excluding aws_bedrock_client which is a
  boto3 client object, not a string, and aws_polly which is a provider
  name)

The regression test now pins all 12 newly-allowlisted fields.
2026-07-06 10:06:42 -07:00
yucheng
59285e6720 fix(proxy): resolve os.environ/ refs for all AWS auth params in DB-sourced models
PR #30867 removed request-time os.environ/ expansion in
BaseAWSLLM.get_credentials to close LIT-3831. That relies on config-load
paths pre-resolving os.environ/ refs, but the DB-load path
(_resolve_db_litellm_param) only re-expands keys in
_DB_LITELLM_PARAM_ENV_REF_KEYS, which covered api_key,
aws_access_key_id, and aws_secret_access_key but not the other AWS auth
fields. A model stored in Postgres with e.g.
    aws_role_name: os.environ/BEDROCK_ASSUME_ROLE_ARN
lands on the router with the literal string, get_credentials no longer
expands it, and STS returns
    ValidationError: os.environ/BEDROCK_ASSUME_ROLE_ARN is invalid

Add the remaining AWS auth params to the allowlist so DB-sourced values
resolve at model-load time (trusted, server-side), matching the
YAML-config path. Team-scoped DB rows still get resolve_env_refs=False,
so the LIT-3831 defense-in-depth path is unchanged and request-body
injection is still blocked by _BANNED_REQUEST_BODY_PARAMS.

Regression tests pin every added field as an os.environ/ DB value and
assert it resolves on the router, plus a team-scoped pin that asserts
env refs remain literal.
2026-07-06 09:56:15 -07:00
Krrish Dholakia
2967bc9bef
fix: merge websearch tool params (#32162)
* fix: pass websearch tool params

* fix: load db websearch tool params

* fix: merge search tools in proxy

* fix: satisfy websearch lint budget

* fix: enforce websearch tool auth

* fix: preserve search tools on empty sync

* chore: rerun circleci
2026-07-04 19:24:35 -07:00
yucheng-berriai
89d3f2a7b8 fix: redact db environment variable debug logs 2026-07-03 13:03:45 -07:00
yucheng-berriai
64d6a15182 fix(proxy): redact secrets on the db-config and litellm_settings log paths too
Reuse the existing recursive `_redact_secret_values_in_obj` for the worker
config log instead of a hand-rolled top-level pass, so a credential nested
under general_settings is masked at any depth and depth overrun fails closed.
Route the `_update_config_from_db` param_value log (the store_model_in_db
path) and the litellm_settings apply-loop log through the same redactors, so
master_key, database_url, and secret-named settings such as api_key stop
leaking at DEBUG when the module regex scrubber is bypassed. A plain setting
like num_retries still logs its real value.

Regression tests disable _ENABLE_SECRET_REDACTION and cover the nested worker
config shape, the db-config path, and the litellm_settings loop in both
directions.
2026-07-03 13:03:45 -07:00
yucheng-berriai
680f15f1e3 fix(proxy): stop leaking master_key and database_url in startup DEBUG logs
Three startup log statements in litellm/proxy/proxy_server.py dumped
secret-bearing values in cleartext when the last-line-of-defense regex
scrubber was bypassed (LITELLM_DISABLE_REDACT_SECRETS=true, older versions
that predated the SecretRedactionFilter, or any downstream handler that
snapshots log records before the module filter runs)

ProxyConfig._load_alerting_settings logged the whole general_settings
dict under a label that only referred to the alerting callbacks; a
copy-paste bug that happened to leak master_key, database_url, and every
other secret sitting in general_settings. Now logs only the alerting
callback list

ProxyConfig.load_config logged the resolved DB URL after secret-manager
resolution. The line's stated purpose was to confirm the retrieval ran,
which does not need the value. Now logs a value-less breadcrumb

proxy_startup_event logged the raw WORKER_CONFIG blob, which docker/K8s
deployments hand the proxy as a JSON string containing master_key,
database_url, and provider API keys. Now routes through
_redact_worker_config_for_logging, which combines the segment-matching
SensitiveDataMasker (catches master_key, api_key, *_token) with an
explicit pass over _EXTRA_SECRET_GENERAL_SETTINGS_FIELDS (catches
database_url and other credential-URL fields the segment masker misses)

Regression tests disable the module-level SecretRedactionFilter so
assertions see the raw record; without the fix they would trip on the
secret substring, so a future refactor cannot silently reconstruct the
leaky string
2026-07-03 13:03:45 -07:00
yucheng-berri
8ce6b4d712
fix(proxy): tighten role gating on /get/config/callbacks response (#31745)
The handler returned decrypted callback environment values and alerting
routing values verbatim to callers who were not full PROXY_ADMIN. Gate
those on full-admin role, matching the posture used on the sibling
config-inspection endpoints. Non-sensitive routing fields (host / base
URL / port style values) stay visible so the UI can still label which
integration is wired up. Full PROXY_ADMIN sees everything unchanged so
the edit form round-trips on save.

Resolves LIT-4115.
2026-07-01 17:58:31 -07:00
Yassin Kortam
87f035b58f
perf(spend): gather independent per-scope spend-counter increments (#31578) 2026-06-30 12:07:47 -07:00
Sameer Kankute
2b496bc7f7
fix(proxy): restore wildcard expansion in /v1/model/info (#31444) 2026-06-26 08:50:58 -07:00
Sameer Kankute
7eacdd5258
chore: litellm oss staging 250626 (#31305)
* fix(anthropic): support Bearer auth for custom api_base endpoints (Fixes #30926)

* style: format common_utils.py with black

* fix(anthropic): extract api_base from litellm_params in batches/files validate_environment

* fix(anthropic): scope Bearer key check to custom api_base endpoints

* fix(streaming): reset Anthropic message_start cursor (output_tokens=1) when no message_delta arrives

The Anthropic streaming protocol emits `message_start.usage.output_tokens=1`
as a placeholder cursor; the real cumulative output count only arrives in
the final `message_delta` event. When a stream is cancelled before
`message_delta` lands (common for thinking models on long-tail prompts),
ChunkProcessor._calculate_usage_per_chunk's last-wins accumulator left
completion_tokens stuck at 1. Because 1 is truthy, the
`completion_tokens or token_counter(text=...)` fallback in
calculate_usage() never fired, and requests were billed for 1 output
token even when several thousand tokens of text had actually streamed.

Fix: track whether any chunk's completion_tokens exceeded 1
(saw_non_cursor_completion). If the only update we saw was the cursor,
reset completion_tokens to 0 so the text-based fallback estimates from
the real completion content.

Legitimate 1-token completions (model returns "Yes." etc.) are unaffected
in practice — token_counter on a 1-token completion_output also yields
~1, so billing stays approximately correct.

Tests:
- TestAnthropicCursorBug (6 cases) — pins the post-fix behavior
- TestNonAnthropicStreamingIntact (2 cases) — guards against regression on
  providers without the cursor pattern

All 8 new tests pass; 9 existing streaming_chunk_builder_utils tests
still pass.

* fix(streaming): scope cursor reset to anthropic provider + recognize message_delta arrival

Addresses both Greptile P2 threads on PR #30420:

CLASS A — Anthropic-specific heuristic was applied globally
============================================================
The `completion_tokens == 1 and not saw_non_cursor_completion` reset
lived in provider-neutral `streaming_chunk_builder_utils.py`. Any
non-Anthropic provider that legitimately reports completion_tokens=1
in a single usage chunk (perfectly normal for short OpenAI / Bedrock /
Vertex single-token replies with stream_options.include_usage=true)
would have its value silently rewritten to 0 and re-billed via
token_counter — producing a different number than what the provider
actually charged.

Fix: gate the reset on `custom_llm_provider == "anthropic"`, resolved
from the first chunk's `_hidden_params` (the same field set by
streaming_handler.py:722 on the live path). Unknown / missing provider
is treated as non-Anthropic and skips the reset, so newer providers and
custom plugins are also safe by default.

CLASS B — `saw_non_cursor_completion` missed legitimate single-token replies
============================================================
Previous condition was `usage_chunk_dict["completion_tokens"] > 1`,
which never fires for an Anthropic stream where the model legitimately
emits exactly one output token (e.g., "Yes."). Anthropic still sends
message_start (output_tokens=1, the cursor) AND message_delta
(output_tokens=1, the real value) — same value, but two distinct usage
events. The old check couldn't tell that apart from a cancelled stream
where only message_start landed.

Fix: track `completion_usage_updates` and flip `saw_non_cursor_completion`
when EITHER (1) the value exceeds 1 (definitely not a placeholder), OR
(2) we've seen >=2 completion-bearing usage events (positive evidence
that message_delta arrived). Cancelled cursor-only streams still have
exactly one event and still hit the reset; cache chunks with
completion_tokens=0 don't count toward the threshold.

Tests
============================================================
- _make_chunk now sets `_hidden_params["custom_llm_provider"]` (default
  "anthropic") so the gate is exercised by every existing test —
  none of them needed assertion changes besides the legitimate-single-
  token case, which now expects exactly 1 (was a fuzzy 0..3 range).
- New: test_anthropic_cache_only_chunks_after_message_start_still_resets
- New: test_non_anthropic_provider_completion_tokens_one_not_reset
- New: test_unknown_provider_completion_tokens_one_not_reset

11/11 tests pass.

* chore: add Co-authored-by trailer for attribution

Co-authored-by: songkuan-zheng <songkuan-zheng@users.noreply.github.com>

* fix(anthropic): preserve messages cache usage

* style(anthropic): format messages cache usage helper

* fix(anthropic): accept integral float cache token counts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix(anthropic): accept integral float cache token counts

* test(anthropic): cover cache usage edge cases

* fix(gemini): preserve thoughtSignature for server-side tool responses

When Gemini API returns toolCall and toolResponse parts, they might have
different thoughtSignatures. Previously, LiteLLM merged them into a single
dict, overwriting the response's thoughtSignature with the call's.
This fix extracts them separately and re-injects them correctly.

TAG=agy
CONV=755b21d0-3200-40bc-bd1a-bb58a378a9a6

* fix(gemini): address PR comments on thoughtSignature handling

- Fix orphan-response thoughtSignature regression by copying thought_signature to response_thought_signature
- Add missing assertions in existing tests
- Add new unit tests for orphan-response signature handling

TAG=agy
CONV=755b21d0-3200-40bc-bd1a-bb58a378a9a6

* feat(mcp): include server alias and server_id in mcp_info response

- Add alias and server_id fields to mcp_info object in /mcp-rest/tools/list endpoint
- Update rest_endpoints.py to surface alias from server config
- Add test coverage in test_mcp_server.py and test_rest_endpoints.py

Fixes #31015

* fix(proxy): reject non-finite spend via validate_finite_spend

A NaN/-inf spend would bypass spend >= max_budget enforcement. Add a
shared finite-value guard, defined above the litellm.proxy.* imports to
avoid the module-level cyclic-import warning.

* fix(proxy): require admin for any /key/update spend, reject non-finite

Gate the admin check on the presence of `spend` (not a value diff): the
DB spend lags the live cross-pod counter, so an "unchanged" spend on the
non-admin path let a key owner / team member overwrite the live counter
below real usage. Also reject NaN/+-inf spend before the DB write.

* fix(proxy): invalidate spend counter on /user/update spend change

A direct spend change on /user/update wrote the DB row but left the warm
cross-pod counter at the stale value, so enforcement kept reading the old
spend. Invalidate spend:user:{user_id} after the write (reseed-from-DB),
and reject non-finite spend before the write.

* fix(cache): route Bedrock semantic-cache sync embedding through the Router (#28244)

The semantic cache's embedding model is a proxy Router alias whose AWS
credentials (aws_role_name, aws_session_name) live only in the Router
deployment's litellm_params. The sync embedding paths called litellm.embedding()
directly, bypassing the Router, so they could neither resolve the alias nor
assume the configured role; cross-account Bedrock semantic caching failed with
"bedrock:InvokeModel is not authorized". On Redis this surfaced at proxy startup
because redisvl's CustomTextVectorizer eagerly fires a dimension-probe embedding
during cache construction, while llm_router is still None.

Fix A: make the sync paths mirror the already-correct async paths. A shared,
dependency-injected helper (litellm/caching/_embedding_router.py) decides whether
to route through llm_router.embedding(...) when the model is a Router deployment,
else fall back to direct litellm.embedding(...). Redis and qdrant sync
set_cache/get_cache now precompute the embedding and pass vector= to the backend,
exactly as the async astore/acheck already do. Both async _get_async_embedding
methods are unified onto the same helper and now forward the caller's full
metadata instead of a hand-picked subset.

Fix B (Redis only): defer redisvl index construction from __init__ into a lazy,
memoized llmcache property, so the dimension-probe embedding fires on first cache
use, after llm_router is wired. A failed build is not memoized, so a transient
outage recovers on the next request.

Known limitation: resolve_embedding_router gates on an exact model-name match
(same as the shipped async path); wildcard/alias/team-public routes still fall
back to direct embedding. Tracked as a follow-up.

* fix(cache): harden embedding-router and shrink Any surface (review)

Address review feedback on the semantic-cache aws-role fix (#28244):

- resolve_embedding_router now skips deployment entries missing model_name
  instead of raising KeyError on a malformed model_list (Greptile P2);
  add a regression test that fails on the old direct-key access.
- Replace the `**kwargs: Any` passthrough on the four cache _get_embedding /
  _get_async_embedding helpers with an explicit, typed
  `metadata: Optional[Dict[str, Any]] = None` parameter. The helpers only
  ever consumed kwargs["metadata"], so this is behavior-preserving, makes the
  forwarded field obvious at the call site, and removes three bare-Any
  annotations (keeps the strict-rule ANN401 budget within ceiling).
- Note in _build_llmcache that redisvl's dimension-probe embedding adds one
  extra billable embedding on the first cache request (Greptile P2).

* fix(bedrock_mantle): correct responses routing for openai.gpt-5.x models

Dashboard Test Connection for bedrock_mantle/openai.gpt-5.4 and openai.gpt-5.5 was failing with maximum recursion depth errors and "model does not exist"

Route detection in the bedrock provider matched route tokens by plain substring, so the bedrock_mantle/ prefix was mistaken for the mantle/ invoke route and the body model was rewritten to bedrock_openai.gpt-5.5; route tokens now only match at a path-segment boundary so the bare model name is preserved

A responses-mode model whose provider has no responses config bounced forever between the responses API and chat completions; the responses to completion fallback now tags its call so completion() does not bridge back, breaking the loop

The Test Connection endpoint hardcoded the test mode to chat, which disabled mode auto-detection for responses-only models; the default is now None so the mode is detected from model capabilities

acompletion() now drops a duplicate acompletion kwarg before building the partial and treats model_info=None as an empty dict to avoid a NoneType crash

* test(bedrock_mantle): cover route guard and bridge flag; fix reportArgumentType regression

Adds the regression coverage codecov flagged on the two responses to completion
bridge guard lines and the bedrock route-prefix helper. The handler tests drive
both the sync and async fallback paths with litellm.completion and
litellm.acompletion mocked, and assert the forwarded kwargs carry
_skip_responses_api_bridge=True, so dropping either flag line fails the suite.
The common_utils tests assert that bedrock_mantle/openai.gpt-5.x no longer
resolves to the mantle route while the genuine mantle/ and bedrock/mantle/ ids
still do, exercising both branches of _model_has_route_prefix.

Also aligns update_messages_with_model_file_ids model_id to Optional[str],
matching its Responses API sibling, so the defensive model_info fallback no
longer introduces a new reportArgumentType in completion(); the file-id lookup
narrows model_id before the dict get

* chore(ui): sync generated OpenAPI types for optional test_connection mode

The test_model_connection mode body param default changed from chat to None so
the mode is auto-detected from model capabilities, which makes the field
optional in the proxy OpenAPI spec. Regenerate the committed schema so the
dashboard types match: mode becomes optional and the description and default
JSDoc follow the spec, keeping the Check UI API Types Sync gate green

* refactor(bedrock): match all explicit route prefixes at path-segment boundary

Migrates the remaining substring route checks to the existing
_model_has_route_prefix helper so every explicit route token matches only as a
leading path segment, consistent with get_bedrock_route and the mantle route.
Covers _explicit_converse_route, _explicit_claude_platform_route,
_explicit_invoke_route, _explicit_agent_route, _explicit_agentcore_route,
_explicit_converse_like_route, _explicit_async_invoke_route and
_explicit_openai_route. This also stops invoke/ from substring-matching
async_invoke/. Route precedence and order are unchanged, and a note on the
segment invariant is added to the helper docstring

* test(bedrock): cover explicit route prefix segment matching

Exercises all eight migrated _explicit_*_route helpers (converse, converse_like,
invoke, async_invoke, agent, agentcore, claude_platform, openai) directly: each
matches its token as a leading path segment and rejects the token glued to a
preceding segment, so reverting any method to the old substring check fails the
suite. Also asserts invoke/ no longer matches async_invoke/ models, the concrete
improvement of the segment-boundary migration

* test(proxy): assert negative spend is allowed (one-time grant use-case)

Negative spend is intentionally permitted so admins can grant extra
allowance for the current budget period only, without raising the
recurring budget ceiling. Cover it explicitly in validate_finite_spend
and via the /user/update invalidation test.

* fix(google_genai): forward native generateContent top-level fields

Google's native generateContent REST body carries safetySettings, toolConfig,
cachedContent and labels at the top level as siblings of generationConfig. The
proxy's :generateContent endpoint spread them into agenerate_content as loose
kwargs and then dropped them, so callers had to wrap them in extra_body for them
to take effect; safetySettings, for instance, was silently ignored

The provider config now exposes the native top-level field names and
setup_generate_content_call collects whichever are present, merging them into the
outgoing request body through the existing extra_body merge so they reach Google
verbatim. An explicit extra_body still wins on conflict. The sync
generate_content_stream path now also forwards systemInstruction, matching the
other three entry points

Fixes #12671

Claude-Session: https://claude.ai/code/session_016MFtMXokCjT8u6mvyASudK

* fix(proxy): resolve env refs for DB-stored models

* fix(proxy): restrict DB env ref resolution

* fix(proxy): block team DB env ref resolution

* fix(lint): resolve ANN401/UP045/C901 strict-gate violations

- Replace Optional[X] with X | None (UP045) in 8 files
- Replace Any return/param types with concrete types or object (ANN401)
- Extract _make_api_key_auth_header helper to reduce get_anthropic_headers complexity below C901 threshold (17 → 14)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(anthropic): preserve x-api-key for custom endpoints; opt-in Bearer via prefix

Users who pass a key already prefixed with "Bearer " get Authorization: Bearer.
All other keys continue to use x-api-key, preserving backward compatibility with
custom api_base endpoints that expect x-api-key rather than Authorization.

Also consolidates get_auth_header to reuse _make_api_key_auth_header helper,
eliminating the duplicated custom-endpoint routing logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* revert(anthropic): restore Bearer routing for non-sk-ant- keys on custom api_base

The backwards-compat change broke existing tests that verify the intentional
Bearer-for-custom-base behavior (Fixes #30926). Restore original logic while
keeping the _make_api_key_auth_header helper for code deduplication.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(anthropic): gate Bearer-for-custom-base behind use_bearer_for_custom_base flag

Previously the auth-header switch from x-api-key to Authorization: Bearer
applied unconditionally for non-sk-ant- keys on a custom api_base, silently
breaking existing deployments that proxied to gateways expecting x-api-key.

Introduce use_bearer_for_custom_base: bool = False on _make_api_key_auth_header,
get_anthropic_headers, and get_auth_header. validate_environment reads it from
litellm_params so callers can opt in per-model without any API surface change.

Tests updated to pass use_bearer_for_custom_base=True where Bearer behavior is asserted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(redis): apply namespace prefix in delete_cache and async_delete_cache (#29981)

DEL was the only Redis cache operation that skipped check_and_fix_namespace,
so it targeted the raw SHA256 hash (e.g. 3997c4...) rather than the
namespaced key (litellm:3997c4...). This caused two problems: a Redis NOPERM
error on deployments with an ACL restricting DEL to the litellm:* pattern,
and a silent no-op on all other deployments since the un-prefixed key was
never stored.

* style(anthropic): reformat common_utils.py with Black (--target-version py312)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: preserve cache metadata and spend counters

* style: apply ruff format to streaming_iterator.py

* refactor: reduce complexity of usage/spend helpers to satisfy strict ruff gate

Extract Anthropic message_start cursor reset into
_reset_anthropic_cursor_completion_tokens and the cross-pod spend-counter
invalidation into _invalidate_user_spend_counter_if_changed, keeping both
_calculate_usage_per_chunk and _update_single_user_helper under the
max-complexity ceiling. Use builtin generics in the new signatures so no
new UP006 violations are introduced. Behavior unchanged.

---------

Co-authored-by: rupak-eng <rupakji99@gmail.com>
Co-authored-by: songkuan-zheng <252822057+songkuan-zheng@users.noreply.github.com>
Co-authored-by: songkuan-zheng <songkuan-zheng@users.noreply.github.com>
Co-authored-by: Kannan Priyadharshan <kpd2204@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Marco Georgaklis <mgeorgaklis@google.com>
Co-authored-by: Anjaiah Methuku <anjaiahspr@gmail.com>
Co-authored-by: Andrii Butko <booandrew23@gmail.com>
Co-authored-by: Kent <kingdooo@gmail.com>
Co-authored-by: kunal2002 <k.nayyar2002@gmail.com>
Co-authored-by: Ali Khan <alirazakhan.offi@gmail.com>
Co-authored-by: jesco-absolut <team@srswti.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Matt Hill <mhill@dataminr.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-06-25 21:00:28 -07:00
mubashir1osmani
e0c8a6b483
fix(proxy): expand all-proxy-models sentinel in direct access lookup (#31153)
A user provisioned with "All Proxy Models" stores the literal
"all-proxy-models" sentinel in user.models. get_direct_access_models looked
that string up as a real model_name via get_model_list, which matched no
deployment, so /v2/model/info marked every model direct_access=false and the
Models + Endpoints page rendered empty for such users when they have no teams.
The model dropdown / Playground worked because get_key_models already expands
the sentinel to the full proxy model list, hence the inconsistency in the
report.

Expand the sentinel to all non-team deployment ids via
get_model_ids(exclude_team_models=True), the same call the PROXY_ADMIN branch
in the caller already uses. This fixes both /v1/model/info and /v2/model/info
since they share _populate_team_access_on_models. Empty user.models stays "no
direct access" to match get_key_models semantics.

Fixes #22791
2026-06-23 22:23:14 -07:00
yucheng-berri
c2e06890ad
fix: tighten role-based visibility of config and MCP fields (#30587)
* fix: redact config and MCP secrets in read-only admin views

GET /config/field/info and the MCP server list/detail endpoints returned
secret-bearing fields to any caller with an admin view, including
read-only admins. They now return those fields in full only to a full
PROXY_ADMIN; every other caller gets the reduced, non-admin view, while
non-sensitive fields remain readable. Regression tests cover the
role-based visibility on both endpoints, including that a full admin
still sees everything needed to populate the edit form.

* fix: redact nested secrets in config field info for non-admins

/config/field/info returned structured general_settings fields verbatim to
any admin-view caller, so a view-only admin reading database_args received
the nested aws_web_identity_token (a DynamoDB role-assumption credential) in
plaintext. Recurse into dict/list field values and redact secret leaves for
non-PROXY_ADMIN callers, leaving non-secret siblings and full-admin reads
unchanged

* fix: redact secret config values in /config/list for non-admins

/config/list shared the same _user_has_admin_view gate as /config/field/info
but returned each field value unredacted, so a view-only admin reading the
list received pass_through_endpoints upstream Authorization headers verbatim.
Route every general_settings value through a shared role-aware redactor
(extracted from /config/field/info) covering the top-level and nested field
paths, so non-PROXY_ADMIN callers get secret-bearing fields redacted while
full-admin reads stay unchanged

* chore(ci): allowlist _redact_secret_values_in_obj in recursive_detector

The config secret redactor recurses over JsonValue, which is acyclic, and
its depth is bounded by the operator-authored general_settings schema. Add
it to the recursive_detector ignore list alongside the other bounded
nested-redaction helpers (mask_dict, _redact_sensitive_litellm_params)

* proxy: cap recursive secret redaction depth at 10

Match the cap on _redact_sensitive_litellm_params (the closest analog
in the proxy, also recursive, key-name driven, returns a sentinel).

The previous justification — bounded by operator-authored schema depth,
JsonValue acyclic — is true today but is a property of the threat model,
not an enforced invariant of the function. If a code path is ever added
that pipes external input into general_settings (config import,
migration tooling, JWT-driven settings, …) the assumption silently
breaks. A local cap makes the invariant local.

The cap branch fails closed: at _REDACT_SECRET_MAX_DEPTH the whole
subtree is replaced with 'REDACTED' rather than returned verbatim. A
future refactor that flips this to fail-open would let a deeply nested
credential leak; the new regression test test_redact_secret_values_in_obj_fails_closed_at_max_depth
guards against that.

Updates the recursive_detector ignore-list rationale to point at the
numeric cap rather than the structural argument.

* test: actually exercise the depth cap in fails-closed test

The previous fixture stored the leaf under the secret-named key
'aws_web_identity_token', which the recursor's key-name short-circuit
redacts regardless of the cap — so the test passed both with and
without the cap in place. Empirically confirmed: under an uncapped
mutant the old fixture still hides the secret (key-name catches it),
the new fixture leaks it (only the cap can stop it). Swap the leaf
key to a non-secret name so the cap is the only redaction path
exercised, making the test fail on mutation as advertised.
2026-06-23 15:25:30 -07:00
Sameer Kankute
80c5a84871
chore: litellm oss staging (#30968)
* fix: correct amazon.titan-embed-text-v2 input price to $0.02/1M tokens (#29693)

* fix: correct amazon.titan-embed-text-v2 input price to $0.02/1M tokens

* test: scope local cost map env var with monkeypatch to avoid test pollution

* fix(sensitive_data_masker): fully mask secrets at or below the reveal threshold (#30764)

* fix(sensitive_data_masker): fully mask secrets at or below the reveal threshold

_mask_value did partial reveal by showing the first visible_prefix and last
visible_suffix characters, but for a value whose length was at or below
visible_prefix + visible_suffix (8 by default) it returned the value verbatim.
A value of exactly 8 chars fell through the length guard and computed
masked_length == 0, reconstructing the original string with no mask characters;
anything shorter hit the early return. Either way short credentials were emitted
in plaintext.

mask_dict routes real secrets through this path, so an 8-char-or-shorter redis
password, api key, or token could be written to logs and the UI unmasked. The
sibling helper mask_sensitive_keys already guards this case; _mask_value now does
the same by fully masking any value at or below the threshold.

* fix(sensitive_data_masker): add mask_short_values opt-out for truncation callers

Fully masking short values is the right default for secret masking, but
CooldownCache reuses the masker purely to truncate exception messages to the
first 50 characters, and it relies on short messages being returned readable.
Masking those blanked out short exception text and broke its tests.

Add a mask_short_values flag (default True, secure) and have CooldownCache pass
False so it keeps the truncation behavior, while every secret-masking caller
still gets short values fully masked.

* fix(mcp_debug): opt out of short-value masking to keep diagnostic token preview

MCPDebug uses the masker to preview auth tokens in debug headers and documents
that values of 10 chars or fewer are shown unchanged so token types stay
distinguishable. Pass mask_short_values=False so that diagnostic behavior is
preserved while secret maskers keep masking short values.

* fix(mcp_debug): mask short auth values in debug headers instead of echoing them

Earlier this masker opted out of short-value masking to keep a token preview, but
that echoes short authorization and token values verbatim in debug response
headers, which is the same leak this change is meant to close. Auth material
should never be emitted in full, so mask short values here too; the first/last
character preview still applies to longer tokens. Only CooldownCache keeps the
opt-out, since it truncates exception text rather than masking secrets.

* test(mcp_debug): assert masked short value preserves length

* refactor(fireworks_ai): remove deprecated audio transcriptions endpoint (#30917)

Fireworks AI deprecated audio inference on 2026-06-10
(https://docs.fireworks.ai/updates/changelog#audio-inference-and-image-generation-deprecation).
Live API testing confirms the endpoint is already non-functional: a valid
Fireworks API key receives HTTP 401 "Unauthorized" from
api.fireworks.ai/inference/v1/audio/transcriptions for every request,
regardless of payload. The audio-prod.api.fireworks.ai host referenced in
the test suite returns 401 for every path; the entire host is decommissioned.

Remove the dead FireworksAIAudioTranscriptionConfig class and every
reference to it across the codebase:

- Delete litellm/llms/fireworks_ai/audio_transcription/ directory (17-line
  config class that inherited from OpenAIWhisperAudioTranscriptionConfig)
- Remove the Fireworks branch from
  ProviderConfigManager.get_provider_audio_transcription_config() in
  litellm/utils.py; update the stale comment in
  get_optional_params_transcription that referenced fireworks ai
- Remove the FireworksAIAudioTranscriptionConfig entries from
  LLM_CONFIG_NAMES and _LLM_CONFIGS_IMPORT_MAP in
  litellm/_lazy_imports_registry.py
- Remove the TYPE_CHECKING re-export in litellm/__init__.py
- Remove the transcription branch in the fireworks_ai case of
  get_supported_openai_params() in
  litellm/litellm_core_utils/get_supported_openai_params.py
- Remove the whisper-v3 and whisper-v3-turbo entries from
  model_prices_and_context_window.json and
  litellm/model_prices_and_context_window_backup.json (both had
  mode: audio_transcription and zero-cost pricing)
- Remove the TestFireworksAIAudioTranscription test class and its
  imports from tests/llm_translation/test_fireworks_ai_translation.py

No other provider is affected. The openai_compatible_providers list,
FireworksAIMixin, and the OpenAI Whisper transcription handler all stay
because they are shared with other Fireworks endpoints and other
providers. The provider_endpoints_support.json registry already had
audio_transcriptions set to false for fireworks_ai.

* feat: add darkbloom provider (#30876)

* feat: add darkbloom provider

* fix: document darkbloom provider endpoints

* fix: address darkbloom review feedback

* fix: update darkbloom tool metadata

* fix: fail fast for non-Postgres database URLs (#30883)

* fix(proxy): fail fast on non-PostgreSQL DATABASE_URL instead of hanging on startup

LiteLLM's Prisma datasource is pinned to provider = 'postgresql', so a sqlite:// or mysql:// DATABASE_URL can never connect.

Today that surfaces as an opaque startup stall where the port never binds, and a separate 'DB not connected' 500 on /key/generate when no DATABASE_URL is set at all leaves operators guessing what to configure.

Validate the DATABASE_URL / DIRECT_URL scheme in run_server before any Prisma call and exit with an actionable message naming the unsupported scheme.

Also reword CommonProxyErrors.db_not_connected_error to tell the operator to set DATABASE_URL to a postgresql:// connection string.

Add regression tests covering postgres acceptance and sqlite/mysql/mssql rejection.

* fix: resolve CI failures and proxy DB URL typing issue

* fix(proxy): fail fast on non-PostgreSQL DATABASE_URLs with clear startup errors instead of hanging

* Validate DIRECT_URL alongside DATABASE_URL startup guards

* fix(bedrock): surface modeled HTTP status for mid-stream error events so 5xx is retryable (#24608) (#30946)

* fix(bedrock): surface modeled HTTP status for mid-stream error events (#24608)

* test(bedrock): mid-stream server errors trigger streaming fallback (#24608)

* style(bedrock): black-format stream-error helper (#24608)

* fix(mcp): re-land native tool preservation with typed annotations (#30645)

* fix(mcp): preserve native tools in semantic filter hook with typed annotations

* fix(mcp): tighten _is_mcp_tool Chat Completions shape check

* fix(sambanova): return embeddings supported params instead of dropping them (#30937)

* fix(router): send fallback metadata when streaming (#30914)

When a streaming request triggers a fallback, there was previously no way to
know it happened. This commit addresses this in a few ways:

1. The response now correctly populates the fallback headers
    (`x-litellm-attempted-fallbacks`) so callers know a fallback happened.
2. The correct model ID is passed in the streaming chunks.
3. A streaming chunk with the fallback error can be optionally sent back
    to the client (opt-in) by passing `include_fallback_errors: true` in
    the request.

The format of the fallback errors while streaming is intentionally OpenAI
compatible to not break existing libraries that parse these events. It was
tested with Vercel's AI SDK (ai-sdk.dev). It is also opt-in, so it is not
delieved unexpectedly to callers by default.

* fix(mistral): drop output-only reasoning fields from input messages (#30884)

LiteLLM attaches reasoning_content and thinking_blocks to assistant
responses. Replaying those assistant turns verbatim forwarded the fields
back to Mistral, whose input schema forbids unknown keys, so the whole
request failed with a 422 extra_forbidden and reasoning models became
unusable across multiple turns.

Strip both fields from assistant messages before the request is built, in
a spot that runs ahead of the image/file branch so it applies on every
path. Fixes #30835

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

* fix(perplexity): bill search queries at the per-request price, not 1/1000 of it (#30652)

* fix(perplexity): bill search queries at the per-request price, not 1/1000

The fallback cost calculator divided search_context_cost_per_query by
1000, but that field stores the per-request price in USD: sonar is
{low: 0.005, medium: 0.008, high: 0.012}, matching Perplexity's published
$5/$8/$12 per 1,000 requests expressed per request. The gemini cost
calculator reads the same field per request with no division (its
docstring calls it "the per-request cost").

The division understated search cost by 1000x on every Perplexity call
that falls back to manual calculation (i.e. when the API does not return
a pre-computed usage.cost). Use the value directly.

Update the tests that had encoded the /1000 factor in their expectations,
and drop an unused import flagged by ruff in the touched test file.

* test(perplexity): update integration test search-cost expectations to per-request

The integration tests still encoded the old /1000 search-cost factor, so
they failed once the fallback calculator was corrected to bill
search_context_cost_per_query per request. Update the four expected-cost
computations (and the high-volume dollar-value comments) to match.

* test(perplexity): drop unused mock imports flagged by ruff

* fix: include model_access_groups when expanding all-team-models in get_team_models (#30622)

* fix(fireworks_ai): return None for transcription in get_supported_openai_params

Fireworks AI deprecated audio inference on 2026-06-10; the endpoint is
decommissioned. Without an explicit transcription branch, requests with
request_type='transcription' fell through to the else and returned
FireworksAIConfig chat-completion params. Return None instead to signal
the provider does not support transcription.

* fix(proxy): gate include_fallback_errors behind expose_fallback_errors_to_caller setting

Without an operator gate, any authenticated caller could set include_fallback_errors=True,
trigger a fallback, and read raw upstream exception messages from the
x-litellm-fallback-errors header and the litellm-fallback-metadata SSE event.

Strip include_fallback_errors from request data in common_processing_pre_call_logic
when expose_fallback_errors_to_caller is not set, so the router never builds the
error list. Also gate _should_include_fallback_errors on the same setting as a
secondary check for the streaming SSE injection path.

* test(proxy): opt in to expose_fallback_errors_to_caller in streaming SSE test

The operator gate added in e7ff3e1 means include_fallback_errors is only
honoured when general_settings.expose_fallback_errors_to_caller is True.
Set that flag via monkeypatch in the test that exercises the emit path.

* test(prompt_templates): make test_convert_url hermetic instead of hitting picsum.photos

test_convert_url called convert_url_to_base64 against a live picsum.photos
URL and asserted nothing, so it added no real signal and broke CI whenever
the host was unreachable (it was returning 522 and blocking this branch).
Replace the live call with a mocked HTTP client and assert the produced
base64 data URL, so the conversion path is exercised deterministically with
no network dependency. This suite runs under VCR, which is why a transport
level mock (respx) does not reliably intercept; mocking the client object
itself is robust regardless.

* fix(interactions): drop role from Interaction response to match Google spec

Google removed the output-only role field from the Interaction schema (it
now lives only on Turn), so the live OpenAPI compliance canary started
failing with 'role' not in spec. Reconcile our generated types by removing
role from Interaction, CreateModelInteractionParams, CreateAgentInteractionParams
and from the LiteLLM InteractionsAPIResponse/InteractionsAPIStreamingResponse,
stop stamping role=model in the responses-to-interactions transformation, and
update the compliance and integration tests accordingly. Turn.role is kept
since the spec still defines it.

* fix: align all-team-models sentinel access

* fix(router): forward include_fallback_errors through multi-hop fallbacks

run_async_fallback received include_fallback_errors as an explicit named
parameter, so it was bound out of **kwargs and never reached the nested
async_function_with_fallbacks call. Multi-hop fallback chains (a fallback
group that itself fails over) therefore stopped collecting fallback errors
beyond the first hop when a caller opted in. Re-inject the flag into kwargs
before the nested call so inner hops keep accumulating errors, which
add_fallback_headers_to_response already merges across levels.

---------

Co-authored-by: Srivatsa Kamballa <skamb10@uic.edu>
Co-authored-by: Ahmad Shahzad <107808273+shzdehmd@users.noreply.github.com>
Co-authored-by: Jeremy Chapeau <113923302+jychp@users.noreply.github.com>
Co-authored-by: KRISH SONI <67964054+krishvsoni@users.noreply.github.com>
Co-authored-by: Kent <72616338+kingdoooo@users.noreply.github.com>
Co-authored-by: Ayush Shekhar <106994833+ayushh0110@users.noreply.github.com>
Co-authored-by: dav nguyxn <hoangson091104@gmail.com>
Co-authored-by: Tal Marian <tal.marian@island.io>
Co-authored-by: Hemant K <51333870+hemant1026@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Yash Raj Pandey <55940078+devYRPauli@users.noreply.github.com>
Co-authored-by: Zang Peiyu <166481866+factnn@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-06-23 07:31:44 -07:00
ryan-crabbe-berri
ce4111b800
fix(proxy): scope team BYOK models by key team_id in /model/info (#31009)
GET /model/info returned an empty list for a team key whose team only has team-scoped BYOK deployments, even though /v1/models and a master key both returned them. _get_caller_byok_team_scope resolved the caller's allowed teams only from user_api_key_dict.user_id and the bound user's team memberships. A team or service key has user_id=None, so the helper returned an empty set and _byok_row_outside_caller_teams then dropped every team BYOK row

This includes the key's own team_id in the allowed-team set across every non-admin branch, since a team key is authoritatively scoped to its team regardless of whether a bound user is resolvable or a formal member of that team

Completes the work in #30025, which aligned /v1/model/info with router deployments but missed the team-key case in the scope helper it introduced
2026-06-22 14:57:16 -07:00