Commit graph

11927 commits

Author SHA1 Message Date
mubashir1osmani
4725cb4661
test(e2e): cover google-native generateContent framing and prometheus queue time (#34650)
* test(e2e): cover google-native generateContent framing and prometheus queue time

Adds live coverage for three shipped regressions that had none, all reached
through surfaces a customer drives from Google SDKs and operator dashboards.

The managed google-native route (`/v1beta/models/{model}:generateContent`) had
no harness support at all, so EndpointsClient gains generate_content and
stream_generate_content plus the request body models, and a new suite asserts
the two contracts that broke there: the response carries
x-litellm-response-cost so SDK traffic reconciles against spend (LIT-4076), and
the stream relays single-prefixed SSE frames with no OpenAI [DONE] terminator.
A doubled `data:` prefix, a leaked bytes literal, or the [DONE] sentinel each
fail the stream test; [DONE] absence is only asserted once real content has
arrived, because a first-chunk upstream error legitimately falls back to the
OpenAI error shape and does emit it.

The prometheus test pins litellm_request_queue_time_seconds to an actual
observation on our own key's series rather than to the family merely existing,
which is the distinction the original regression turned on: the histogram stayed
registered while nothing was ever written to it (LIT-2034).

Each assertion was mutation-checked against the live proxy; inverting the
[DONE] expectation, the cost-header expectation, or the metric name fails the
corresponding test.

* refactor(e2e): simplify google native coverage
2026-08-12 01:28:26 +00:00
mubashir1osmani
67643606ab
test(e2e): add reproducers for passthrough and model budget gaps (#34657)
* test(e2e): add failing reproducers for two open gateway bugs

Both tests assert the behavior a customer expects and both are red today. They
are reproducers, not regressions: the product is wrong, not the tests.

Native passthrough returns almost none of the operational headers the managed
route does. A /gemini/ generateContent call comes back with three x-litellm-*
headers and no x-ratelimit-* at all, against sixteen and four on
/v1beta/models/{m}:generateContent for the same prompt, and critically it omits
x-litellm-response-cost. Customers front provider-native traffic through this
route and read those headers to reconcile spend and pace themselves, so native
traffic is currently invisible to the tooling that covers every other route.

/budget/update rejects any model_max_budget with a 500. The reported symptom was
model ids containing dots, and that reproduces (prisma raises "Unexpected
`-5.2[FloatValue]` Expected `:`" because the key is interpolated into a GraphQL
query unquoted, so glm-5.2 lexes as an identifier followed by a float), but the
plain name gpt4o fails too, on a separate "model_max_budget should be of any of
the following types: Json" type mismatch at budget_management_endpoints.py:173.
Omitting the field returns 200. The test drives both names so the failure says
whether per-model budgets are broken outright or only for punctuated ids; today
it stops on the plain name, which is the wider bug.

* test(e2e): add reproducer for unenforced end-user per-model rate limits

model_max_budget accepts an rpm_limit alongside the spend cap, and /budget/new
stores it: the create response echoes {"gemini-2.5-flash": {"rpm_limit": 1,
"max_budget": 100.0, "budget_duration": "1d"}}. Attach that budget to an end
user, drive three calls as that user, and all three return 200. The limit is
accepted, persisted, and then ignored.

The same shape already works when the budget hangs off a key, which is what
makes this quietly dangerous: the API gives every indication the cap is in
force. A customer using it to hold one end user to a slow rate on a shared key
gets no throttling at all.

Harness additions this needs: ModelBudgetEntry carries the rpm_limit/tpm_limit
the route already accepts, BudgetNewBody and create_budget carry
model_max_budget, and create_customer can attach an existing budget_id rather
than only an inline max_budget.

Red today, for the reason in the assertion message.

* test(e2e): tighten model_max_budget reproducers and drop in-loop closure

Trim the reproducer docstrings to the contract they assert, keeping the
failure messages that document each red-by-design bug. Replace the nested
per-model closure in the /budget/update test with a module-level predicate
and a per-model helper so nothing closes over a loop variable, and fix the
import order the merge left unsorted.

* test(e2e): skip the three reproducers while their gateway bugs stay open

The passthrough header contract, /budget/update model_max_budget, and
end-user per-model rpm enforcement reproducers all still fail against
staging by design. Skip each with the product gap named so the combined
suite can gate merges on green while the collector keeps reporting the
cells as uncovered.

* test(e2e): validate model budget response contracts

* refactor(e2e): unify model budget schema

* refactor(e2e): reuse shared model budget type
2026-08-11 18:15:34 -07:00
mubashir1osmani
ec8088f064
test(e2e): vendor API testing coverage (#34557)
* test(e2e): cover vendor strategy gaps for chat contract, image edits, auth, team activity

Resolves the first slice of LIT-4778 (vendor API testing strategy): image edits happy path, chat multi-turn + validation + sanitization, LLM-route auth header matrix, and /team/daily/activity structure

* test(e2e): expand vendor API strategy coverage across endpoints

Adds validation cases on existing endpoint suites, plus vector stores, search,
bedrock native, realtime HTTP secrets/calls, responses retrieve, files/batches
contract, and chat stream SSE. Registers coverage cells for LIT-4778

* test(e2e): finish vendor strategy open items

Audio transcription negatives, vector-store file attach/poll/search,
OpenAI moderation category matrix across chat/messages/responses, and
smoke model matrix for chat (LIT-4778)

* test(e2e): harden vendor strategy suite against live env edges

Fix stream [DONE] tracking, XSS no-crash contract, realtime model routing,
vector store list/search models, responses validation, and provider-denied
Bedrock paths so the suite is stable against a live proxy

* test(e2e): rename suites, drop vendor_contract, fix greptile gaps

Move shared status helpers into e2e_http, rename chat auth headers and
chat security suites, remove vendor_contract and dev_config files_settings,
and tighten transcription validation plus vector-store search assertions

* test(e2e): route bedrock stream disconnects through e2e_http

Catch mid-stream RequestException in the shared harness so bedrock native
tests do not import requests directly
2026-08-12 01:07:52 +00:00
Yassin Kortam
a0d499e131
fix(e2e): assert on the gen-AI span that served the stream, not the span count (#36582)
The otel trace tests asserted that a streamed call produces exactly one gen-AI
span. The proxy opens one gen-AI span per upstream attempt, so a call the
router retried carries an error span for every failed attempt beside the one
that answered, and the assertion fails on a request that succeeded.

Select the served attempt instead: drop spans whose otel.status_code is ERROR,
require exactly one survivor, and run the TTFT and streaming-flag assertions
against it. That keeps what these assertions exist for, a split trace or a
stream logged as two served spans, while tolerating a retry.

Only the failed attempt lacks TTFT, so the old code also had a second failure
mode: when the first span happened to be the error one, the test reported the
attribute as missing rather than as belonging to a different attempt.

test_span_selection.py covers the selection itself against Jaeger-shaped
payloads and carries no e2e marker, since reproducing a first-attempt failure
live is not something a test can arrange.
2026-08-11 17:49:44 -07:00
yucheng-berri
417c70589f
fix(bedrock_guardrails): skip ApplyGuardrail when there is no content to scan (#36441) 2026-08-11 17:30:54 -07:00
mubashir1osmani
19c914066b
fix(deps): ship boto3 with the base SDK so bedrock works out of the box (#36568)
* fix(deps): ship boto3 with the base SDK so bedrock works out of the box

* keep boto3 listed in the proxy extra as well

* scrub ambient AWS env vars in the base SDK bedrock smoke check
2026-08-11 14:39:10 -07:00
Mateo Wang
422d925334
Merge pull request #36499 from BerriAI/litellm_bedrock_converse_pdf_text_block_fix
fix(bedrock): add text block to converse user messages carrying documents
2026-08-11 12:59:00 -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
Mateo Wang
7b34a6c632
Merge pull request #36507 from BerriAI/litellm_bedrock_converse_thinking_stream_fix
fix(bedrock): preserve adaptive thinking effort through the /v1/messages bridge
2026-08-11 12:23:29 -07:00
Mateo Wang
5c704055c6
Merge pull request #36502 from BerriAI/litellm_bedrock_invoke_tool_search_fix
fix(bedrock): send tool-search beta header for Haiku 4.5 on Invoke /v1/messages
2026-08-11 12:21:46 -07:00
ryan-crabbe-berri
cbf85a015f
feat(proxy): per-key prompt caching toggle via enable_prompt_caching (#36466)
* feat(proxy): per-key prompt caching auto-injection via enable_prompt_caching

Adds a key-level enable_prompt_caching toggle that auto-injects Anthropic
cache_control breakpoints on requests made with that key, without requiring
the gateway-wide enable_anthropic_prompt_caching flag. The flag lives in key
metadata, is stamped onto the request root by add_key_level_controls, rides
kwargs into both the /chat/completions seeding path and the native
/v1/messages path, and reuses every existing gate (anthropic/bedrock only,
supports_prompt_caching, client markers win). Client-supplied body values are
stripped as an untrusted root control field. Includes the Admin UI switch on
key create and key edit plus a read-only settings row, and dedupes the key
edit view's drifted initial-values objects.

* fix(proxy): drop section comment and suppress LIT011 on key-level prompt caching stamp
2026-08-11 11:53:11 -07:00
Deepanshu Lulla
84d6666a59
feat(router): add required-AND (&) tag prefix and allow_fail_open flag (#36193)
* feat(router): add required-AND (&) tag prefix and allow_fail_open flag

Tag routing supported inclusion-OR and independent "!" negation, but had no way
to express a hard "must match all of these" constraint per request, and no way
for a model group to opt into degrading gracefully instead of raising when a
constraint eliminates every deployment.

Adds a "&tag" prefix for required-AND inclusion, composing with existing plain
(OR) and "!" (negate) tags: negation still applies first, then required tags
narrow the survivors, then plain tags apply today's OR/AND preference logic
unchanged. Adds model_info.allow_fail_open (default false) so a chain can opt
into falling back to the default-tagged pool instead of raising
no_deployments_with_tag_routing when "!" or "&" empties the candidate set;
existing chains without the flag keep today's fail-closed behavior exactly.

* fix(router): gate mixed negation on allow_fail_open and stop diluting required-only requests

Two gaps in the initial required-AND/allow_fail_open change: a "!" exclusion
combined with a plain positive tag that emptied the candidate set raised
unconditionally, bypassing allow_fail_open entirely, since the fail-open check
only looked at required-AND exhaustion. And a request using only "&" tags
could get narrowed down to just the deployment matching an incidental
tag_regex/User-Agent preference, silently dropping other deployments that
satisfied the required tags but had no tag_regex at all.

Fixes both: the fail-open check now fires whenever either "!" or "&" leaves
the candidate set empty, not just "&". And regex/header preference no longer
counts as a positive filter when a required-AND ask is present, so a
required-only request returns every deployment satisfying the required tags
regardless of regex/header matching.

Also regenerates ui/litellm-dashboard/src/lib/http/schema.d.ts for the new
model_info.allow_fail_open field, and removes source comments explaining
the router logic per repository convention.

* fix(router): let allow_fail_open cover a non-empty !/& survivor set that fails the plain-tag preference

The unconditional raise inside the has_positive_filter loop was the one
remaining path a chain could hit despite setting allow_fail_open: when "!"
or "&" leaves a non-empty candidate set but none of the survivors match the
request's plain preference tag or carry "default", the request still raised
instead of degrading. Routes that raise through the same allow_fail_open
check used everywhere else, so it now falls back to the default-tagged pool
for opted-in chains and keeps raising unconditionally for everyone else.
This also let the now-redundant pre-loop empty-candidates shortcut be
removed, since the loop reaches the same outcome on its own.

* fix(router): deny allow_fail_open when an unrecognized required tag is masking a satisfiable answer

A caller could add a single "&" tag no deployment in the group has ever
carried to force an empty required-AND set on demand. On a chain with
allow_fail_open, that emptied set fell back to the default-tagged pool
unconditionally, discarding every other constraint merged into the same
request, including ones inherited from key/team policy, even when the rest
of those constraints were still individually satisfiable.

Before falling back, drop any required tag not carried by any deployment in
the group and recompute: if a specific, non-empty answer exists using only
the recognized tags, the unrecognized tag was the actual cause of the
exhaustion, and fail-open must not paper over it. If every required tag is
already recognized, or none are, there's nothing hidden behind an invented
tag, and fail-open proceeds exactly as before; this keeps a single opted-in
deployment's legitimate catch-all behavior working when a caller's tag
simply doesn't exist anywhere in that group.

Ratchets ANN401 and LIT001 budgets down to reflect fixes already earned in
this branch.

* test(router): cover required-AND, allow_fail_open, and unknown-tag denial across fallback chains and model groups

Extends coverage beyond single-hop scenarios: & exhausting a primary group
falls through to a fallback group exactly like ! already does; !, &, and
allow_fail_open composed together across three chained model groups each
raise or fall back independently per-hop; and the unknown-tag denial from
the previous commit is evaluated fresh per hop rather than leaking state
across groups in a fallback chain.

* feat(router): add model_info.enable_tag_filtering per-model-group override

enable_tag_filtering was router-wide only: an operator turning it on for one
model group that needs tag-driven routing exposed every other model group on
the same proxy to the same tag evaluation, even ones that never use tags.
Adds model_info.enable_tag_filtering, checked against any deployment sharing
a model_name, so a chain can flip the router-wide default in either
direction for itself alone: opt a specific group into filtering while the
rest of the proxy stays off, or opt a group out (e.g. an incident-response
catch-all) while the rest of the proxy enforces it.

Precedence, low to high: router-wide default, then the chain override if
set, then the existing request-level escalation (from key/team settings),
which still only ever turns filtering on, never off, over whatever the
router and chain already decided.

Also regenerates ui/litellm-dashboard/src/lib/http/schema.d.ts for the new
field.

* fix(router): gate plain-tag exhaustion on allow_fail_open when the tag is known to the group

A model group where every deployment is tagged "default" (a legitimate
cross-cutting safety-net pattern) never has an empty default_deployments
list, so the existing exhaustion check (len(new)==0 and len(default)==0)
never fired for a plain positive tag that matched nothing among the
currently healthy candidates. The request silently fell through to whatever
"default"-tagged deployment happened to survive, even when allow_fail_open
was never set and the caller's intent (e.g. quality:high) was never honored.

Adds a check for whether the requested tag is part of the group's real
vocabulary at all: if some deployment configured under this model_name
(regardless of current health) genuinely carries the tag, and nothing
healthy currently matches it, the request now raises by default or falls
back per allow_fail_open, through the same _resolve_or_fail_open gate every
other exhaustion path already uses. A tag that's foreign to the group
entirely (e.g. one meant for an unrelated mechanism sharing the same
request-tags list) keeps falling back to the default pool unconditionally,
unchanged, since there's nothing this group's own routing intent could be
violating.

* fix(router): preserve inherited tag constraints when allow_fail_open discards a caller-caused exhaustion

Adds metadata.caller_tags in litellm_pre_call_utils.py, populated only from
what the request itself supplied (header, body tags, body metadata.tags),
never from key/team metadata merged into the same metadata.tags list.

get_deployments_for_tag now uses it to compute a trusted-only pool before
falling open: a required/excluded tag attributable to the caller can be
discarded on fail-open, one inherited from key/team policy cannot. If the
trusted-only pool is itself empty, allow_fail_open raises instead of
silently routing around an unsatisfiable inherited constraint. When
caller_tags carries no information at all (direct SDK Router usage,
bypassing the proxy layer), behavior is unchanged: unconditional fall-open
to the default pool, exactly as before this fix.

* feat(router): add opt-in tag_routing_prefix for collision-proof tag disambiguation

router_settings.tag_routing_prefix lets a caller explicitly mark which
x-litellm-tags/metadata.tags values are routing directives, exempting
them from the known-tag-vocabulary heuristic used to guard fail-open
against caller-invented "&"/"!" tags. Unprefixed tags keep going
through today's existing handling unchanged (hybrid, no migration
required); default "" is a full no-op.

Fixes a bug caught during live-proxy verification: the prefix-stripped
"confirmed" set kept the "&"/"!" marker character, so it never matched
required_set/excluded_set (which _split_tags always strips bare) -- the
entire trusted-required/excluded-tag mechanism silently no-opped for
its primary use case. Adds regression tests for the bare-value mismatch
and updates existing _chain_allows_fail_open/_tag_known_to_group/
_caller_constraint_sets call sites for the new routing_confirmed/
routing_prefix parameters.

* fix(router): resolve model_info.enable_tag_filtering override from the full model group, not just healthy deployments

Cooldown filtering runs before get_deployments_for_tag, so
_chain_tag_filtering_override only saw the survivors of that filter.
A model group whose only enable_tag_filtering-carrying deployment goes
into cooldown lost the override entirely, silently falling back to
the router-wide default and letting any !/&/tag constraint on that
chain be bypassed by driving the one overriding deployment into
cooldown. Resolve the override from every deployment configured for
the model instead, mirroring _tag_known_to_group's existing pattern.

Verified live: with a bad-key deployment carrying the override forced
into real cooldown via allowed_fails=1, an explicit "!provider:openai"
ban on the remaining deployment reproducibly returned 200 via OpenAI
before this fix and 401 (tag filtering still enforced) after it.

* fix(router): avoid Final-reassignment lint error and a MagicMock router fixture gap from tag_routing_prefix

_chain_tag_filtering_override's try/except reassigned a Final-annotated
name across branches, which basedpyright flags as illegal; extracted
the lookup-with-fallback into its own helper so the binding is assigned
once. Also sets tag_routing_prefix on the bare MagicMock router used by
test_router_tag_regex_routing.py's fixture, which otherwise returns an
auto-generated MagicMock (truthy, non-string) for the new attribute and
crashes _strip_routing_prefix's removeprefix() call.

* fix(router): key inherited-tag protection off provenance, not value subtraction

allow_fail_open's trusted-only pool computed "not caller-attributable"
as required_set - caller_required_set. A caller who resubmits the
exact value of an inherited "&"/"!" tag (e.g. an inherited "&region:eu"
alongside a caller-supplied "&region:eu" plus a conflicting
"!region:eu") collapses both origins to the same set value, so the
subtraction zeroes out the inherited requirement's protection too,
letting fail-open route outside a key/team-enforced constraint.

Adds metadata.inherited_tags in litellm_pre_call_utils.py: a snapshot
of "tags" taken after key/team/project policy is merged in but before
this request's own caller-supplied tags are merged on top. A required
or excluded tag is now protected from fail-open discard if it has ANY
inherited backing (set intersection with inherited_tags), regardless
of whether the caller also happens to submit the identical value --
this is what set membership alone could never tell apart under the
old subtraction-based approach. caller_tags is kept (documented as the
complementary record) but no longer consulted for this decision.

Verified live: a virtual key with metadata.tags=["&region:eu"] hit
with header x-litellm-tags: &region:eu,!region:eu (the exact
value-collision attack) reproducibly routed to the OpenAI/us
deployment before this fix and stayed on the Anthropic/eu deployment
after it.

* fix(lint): re-ratchet budget ceilings after rebasing onto litellm_internal_staging

Regenerated ruff-strict-budget.json and type-discipline-budget.json
via make lint-ruff-budget-update / lint-type-discipline-budget-update
against the post-rebase merge-base.

* fix(proxy): compute inherited_tags from key/team/project sources directly, not a tags-list snapshot

apply_client_tag_policy_pre_auth (run from user_api_key_auth, for
_tag_max_budget_check) merges the caller's x-litellm-tags header into
the same metadata.tags list before add_litellm_data_to_request ever
runs. The previous inherited_tags snapshot ("whatever's in tags before
this function's own caller-tag merge") therefore misattributed that
caller-controlled value as policy-backed whenever a request arrived
with the header set -- Greptile flagged this as a P1 security finding.

inherited_tags is now built directly from key_metadata/team_metadata/
project_metadata's own "tags" fields, independent of the shared,
pipeline-position-dependent "tags" list's mutation history. Verified
with a direct reproduction mirroring the real pipeline (calling
apply_client_tag_policy_pre_auth on the same data dict before
add_litellm_data_to_request, as user_api_key_auth actually does): the
caller's header tag no longer appears in inherited_tags. Added a
regression test exercising that same call order; confirmed it fails
against the pre-fix snapshot approach and passes against this fix.

* fix(router): make tag_routing_prefix configurable through update_settings/get_settings and UpdateRouterConfig

router_settings.tag_routing_prefix was only ever applied via the
Router() constructor. Router.update_settings's _allowed_settings
(used directly by proxy_server.py's _add_router_settings_from_db_config
for the DB-backed router_settings path) and get_settings's
vars_to_include both omitted it, so an operator relying on that path
had the value silently ignored -- flagged by veria-ai. Also adds it to
UpdateRouterConfig (the pydantic schema behind POST /config/update),
the same bug shape LIT-3152 previously fixed for retry_policy: a field
missing from that schema gets silently dropped by
model_dump(exclude_none=True) before update_settings is ever called.

* chore(ui): regenerate schema.d.ts for UpdateRouterConfig.tag_routing_prefix

Adding tag_routing_prefix to UpdateRouterConfig changed the proxy's
OpenAPI spec; regenerate the dashboard's generated API types to match.

* fix(lint): re-ratchet budget ceilings after rebasing onto litellm_internal_staging

Regenerated ruff-strict-budget.json and type-discipline-budget.json
against the post-rebase merge-base. LIT002/LIT011 ceilings reflect
this branch's true current counts (confirmed unchanged across the
rebase by diffing against the pre-rebase commit); the base's own
counts moved independently.

* fix(lint): replace mutable-collection fallbacks with immutable ones in inherited_tags computation

key_metadata/team_metadata/project_metadata's "tags" fallbacks used
`or {}` / `or []` literals, each a LIT002 mutable-collection-construction
violation that pushed the branch 4 over its ratchet ceiling relative to
a moved base. Swapped to MappingProxyType({}) / () to match the
immutable idiom the rest of tag_based_routing.py already uses; no
behavior change, since both are falsy and only ever read via .get()/
unpacking. Tightens type-discipline-budget.json's LIT002 ceiling back
down to match, fully closing that gap (LIT011 keeps a genuine 1-count
gap from pre-existing, untouched lines in this file, non-gating).

* fix(lint): suppress LIT011 on the two new data[...] mutation sites

Both new lines follow this file's established data[...] mutation
idiom for add_litellm_data_to_request, matching the existing
suppression already on the inherited_tags line.

* test(router): lock in fallback + tag-filtering interaction

Cover the router-level fallbacks mechanism composing with tag-based
routing: a plain negation exhausting a group correctly advances to
the fallback group, the same exclusion tag exhausting every hop
correctly raises, and allow_fail_open resolving locally must not
spuriously trigger an unrelated external fallback.

* chore: retrigger CI now that litellm-docs#814 is merged

---------

Co-authored-by: Deepanshu <deepanshu.lulla@alpha-sense.com>
2026-08-11 11:49:18 -07:00
devin-ai-integration[bot]
1d90432975
Merge pull request #36500 from BerriAI/litellm_feature_request_user_flow
docs: require a user flow and a stuck-at proof in feature requests
2026-08-11 11:28:41 -07:00
ryan-crabbe-berri
b144b15d48
fix(proxy): add config_updated_at audit timestamp for virtual keys (#36488)
* fix(proxy): add config_updated_at audit timestamp for virtual keys

updated_at carries Prisma's @updatedAt, so every batched spend flush
rewrites it and it cannot distinguish config changes from usage. Add an
additive config_updated_at column stamped only by key management writes
(update, bulk update, regenerate, block, unblock) via a shared helper,
expose it on key responses, and switch the key page's Last Updated to it
with a created_at fallback.

* test(proxy): assert config_updated_at survives key archival

* refactor(proxy): rename config_updated_at to settings_updated_at
2026-08-11 11:02:57 -07:00
mateo-berri
812abcc7f5 fix(triage): track and credit feature dead-end evidence in the verdict 2026-08-11 10:57:46 -07:00
mateo-berri
e59da40750 fix(triage): ask for dead-end evidence in feature request recovery comments 2026-08-11 10:43:36 -07:00
mateo
38a1bec5c2 fix(triage): require evidence of the dead-end in feature requests
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-11 10:40:57 -07:00
devin-ai-integration[bot]
657c4c2b1e
Merge pull request #36498 from BerriAI/litellm_issue_template_user_flow_proof
docs: require a user flow and live-proxy proof in bug reports
2026-08-11 10:40:52 -07:00
Mateo Wang
02b0ee7608
Merge pull request #36503 from BerriAI/litellm_fix_openai_passthrough_stream_cost
fix(proxy): inject streaming usage cost on openai passthrough streams
2026-08-11 10:06:12 -07:00
mateo-berri
df5425675e fix(schema): declare supports_tool_search in the model prices schemas 2026-08-11 15:16:31 +00:00
mateo-berri
7d4488d2e8 refactor(bedrock): read tool search support from the model map
Record supports_tool_search on the Bedrock Claude entries in both cost
map files and have _supports_tool_search_on_bedrock read it first via
the provider-resolved capability lookup, keeping the name patterns as a
fallback for ARNs and ids the map cannot resolve. Threads the flag
through ModelInfoBase and drops a dated remark from the pattern list
2026-08-11 10:06:04 +00:00
mateo-berri
0f41365c34 fix(bedrock): forward output_config effort for application inference profile ARNs 2026-08-11 09:11:22 +00:00
mateo-berri
e7c8cff3b7 fix(proxy): preserve crlf line endings when injecting streamed usage cost 2026-08-11 00:45:32 -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
Mateo Wang
1a8cd8a078
Merge pull request #34290 from eugene-yao-zocdoc/litellm_fix_anthropic_midturn_interjections
fix(anthropic): preserve midturn system corrections
2026-08-10 22:55:09 -07:00
mateo-berri
1b488f7c2f fix(proxy): ban caller-supplied aws identity selectors in request bodies 2026-08-10 22:05:49 -07:00
mateo-berri
b9b200b348 fix(anthropic): keep tool exchanges intact around midturn system write-back 2026-08-10 21:43:42 -07:00
mateo-berri
938396ef90 fix(proxy): recognize crlf sse frame boundaries in passthrough reassembly 2026-08-10 21:37:07 -07:00
mateo-berri
929ee52b87 fix(bedrock): preserve adaptive thinking effort through the /v1/messages bridge
Claude Code drives Opus 4.7 with thinking {"type": "adaptive"} plus
output_config {"effort": "max"}. The anthropic-to-openai adapter
forwarded thinking verbatim for Claude models but dropped output_config,
and Bedrock Converse streams zero reasoningContent blocks for adaptive
thinking without an effort tier. Forward the effort subset of
output_config for Bedrock targets, accept it in the converse supported
params, and map it with the model's effort ceiling applied. Re-enable
the skipped e2e compat cell that catches this
2026-08-11 03:23:16 +00:00
mateo-berri
b5eed5e526 test(bedrock): cover the aws request param merge guard in the unit suite 2026-08-10 20:08:20 -07:00
mateo-berri
46fb1cd514 fix(proxy): reassemble fragmented SSE frames and inject logging dependency 2026-08-10 20:07:57 -07:00
mateo-berri
426b909447 fix(proxy): inject streaming usage cost on openai passthrough streams 2026-08-10 19:56:34 -07:00
mateo
7a17735473 fix(triage): treat an unfilled bug report scaffold as missing proof
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-11 02:56:13 +00:00
mateo-berri
b63ba63655 fix(router): preserve aws session token and role params in deployment credential resolution 2026-08-10 19:46:41 -07:00
mateo-berri
bc98c67028 fix(bedrock): send tool-search beta header for Haiku 4.5 on Invoke /v1/messages
Bedrock InvokeModel rejects tool_search_tool_* tool types unless the
request body carries the tool-search-tool-2025-10-19 beta. The model
allowlist gating that beta omitted Haiku 4.5 (and Opus 4.7, supported
since launch per live verification), so every tool-search request on
those models got a Bedrock 400. Add both to the allowlist and re-enable
the e2e compat cell that caught it.
2026-08-11 02:46:27 +00:00
mateo-berri
78dd81c014 Merge origin/litellm_internal_staging into litellm_fix_anthropic_midturn_interjections 2026-08-10 19:36:35 -07:00
mateo-berri
79accda090 fix(bedrock): add text block to converse user messages carrying documents 2026-08-11 02:35:49 +00:00
mateo-berri
f72ddedf39 fix(bedrock): keep s3_region_name authoritative over merged deployment region 2026-08-10 19:28:05 -07:00
mateo-berri
3a10df7c04 Merge branch 'litellm_internal_staging' into codex/bedrock-s3-credentials-36155 2026-08-10 19:24:04 -07:00
tin-berri
79d412efc2
fix: net prompt-caching savings against the cache-write premium (#36452)
* fix: net prompt-caching savings against the cache-write premium

Prompt-caching savings priced only the cache-read discount and ignored what
the provider charges to create the cache entry. Anthropic bills cache writes
at 1.25x the input rate, so a request that writes a large cache and reads
little from it is a net loss that the dashboard reported as a gain -- or, on
a pure cold write, as a flat zero.

The counterfactual the number answers is "what would this have cost with
caching off", where every token is billed at the input rate. Since
prompt_tokens partitions disjointly into text + reads + writes, that gives

    savings = reads * (input - read_rate) - writes * (write_rate - input)

The write term is the premium over the input rate, not the full write cost:
the tokens would have been paid for at the input rate anyway, so only the
markup is attributable to caching.

The premium stays signed rather than clamped. Three models in the pricing map
price writes below input, and clamping would silently drop that saving.
A model with no cache_creation_input_token_cost falls open to the input cost,
yielding a zero premium -- this is why the change is a no-op for the implicit
caching providers (OpenAI, Gemini), which publish no write price, and bites
exactly on Anthropic and Bedrock.

Verified live through the proxy on a mock Anthropic rig across four cases
(cold pure-write, warm pure-read, write-heavy, read-heavy). Reported total
matched the derived net to the cent, including the negatives; the read-only
case is unchanged.

Pre-existing rows are not backfilled, so a range spanning the deploy mixes
gross and net.

* fix: read a zero cache-write price as unpublished, not free

deepseek-chat carries a literal 0.0 cache_creation_input_token_cost. The
fall-open only caught None, so the zero was taken at face value and the
premium became 0 - input_cost -- reporting a fabricated saving of
writes * input_cost on traffic that cached nothing.

No provider gives cache writes away, so a falsy price means the same thing
an absent one does.

* test: pin that the read leg keeps a literal zero price

The two zero prices mean opposite things and the asymmetry was unpinned.
A free cache write is unpublished pricing; a free cache read is real, and
15 models charge for input while serving reads for nothing. Copying the
write leg's falsy fall-open onto the read leg would zero out their savings.

* refactor: resolve caching rates through the established pricing helpers

Addresses Greptile's P1 and P2, and replaces hand-rolled pricing lookup with
the patterns this file and the cost calculator already own:

- Deployment pricing first: rates now resolve through _effective_model_info
  (Router.get_deployment_model_info), the same helper the autorouter driver
  uses, falling back to _model_info public rates. A deployment with negotiated
  cache rates previously priced at the public map -- a 3x error on the repro.
- Individual prices read via _get_cost_per_unit, the cost calculator's
  accessor, which also coerces string prices from config.yaml and resolves
  service-tier suffixes; the previous raw .get() handled neither.
- Pricing tests no longer monkeypatch litellm.get_model_info; each case now
  pins a real pricing-map entry with a fixture-drift assertion, and the
  deployment-rate case follows the existing Router-fixture test pattern.

Behaviour on public rates is unchanged: 101 tests pass, including the exact
same live-verified formula.

* fix(cost-optimization): computeCacheLeakage divides net savings by all cached tokens, not reads alone

prompt_caching_savings_spend is net of the cache-write premium since PR #36452.
computeCacheLeakage was still dividing by cache_read_tokens alone, which:

1. Overstates the per-token rate on traffic that writes and reads cache equally:
   a 1:1 read:write key shows rate = 0.002, not 0.001, if net savings is /bin/zsh.002

2. Flips the sign on write-heavy traffic: when writes cost more than reads save
   (common on Anthropic and Bedrock), the aggregate net can go negative, but
   dividing by reads alone would show a positive 'potential savings' for keys
   that don't cache yet — recommending they start caching when it's currently
   losing money overall

Fix: divide realizedCachingSavings by (cacheReadTokens + cacheCreationTokens),
matching the semantic that a key starting to cache pays those write premiums too.

When the rate is non-positive, price nothing (potentialSavings stays null, renders
as '—'), reusing the existing no-data fallback path. The card can't meaningfully
estimate savings from a losing rate.

Rename discountPerToken → netSavingsPerCachedToken to surface the semantics and
prevent this drift in future.

Update Usage tab and Cache Leakage card tooltips to describe net-of-premium cost.

Add tests for 1:1 read:write traffic and write-heavy negative-net traffic.
2026-08-10 18:52:03 -07:00
Yassin Kortam
d8762bf4db
fix(router): warn when a deployment's credentials contradict its provider (#36486)
A deployment that carries one provider's credentials while resolving to
another is silently broken: litellm ignores the credentials and sends the
request to the resolved provider. The common shape is a Bedrock model
group where one entry lost its route prefix, so `model: claude-sonnet-5`
with aws_region_name set resolves to the first-party Anthropic API and
returns "x-api-key header is required". Because the router load balances
across the group, only the fraction of requests routed to that entry
fails, which reads as an intermittent provider outage rather than a
config error, and nothing at startup says otherwise.

Warn at deployment registration when provider-scoped credential params
(aws_*, vertex_*) sit on a model that resolves elsewhere, naming the
params, the resolved provider, and the likely missing prefix. Warn only:
an operator may be overriding a route deliberately, so this must not
block startup. Deployments litellm cannot classify are left alone.

Resolves LIT-5391
2026-08-10 18:41:19 -07:00
Yassin Kortam
1d3b64c66f
test(e2e): cover the Anthropic web_search server tool on Bedrock (#36443)
The existing web_search cells drive Claude Code's client-side WebSearch
tool, which the CLI executes itself and feeds back as a tool_result. The
CLI never emits a web_search_20250305 definition, so those cells stayed
green while the Anthropic-managed server tool 400'd on Bedrock.

Add a cell that posts the server tool to a Bedrock deployment over
/v1/messages and asserts a web_search_tool_result block comes back, and
reword the compat row so it no longer reads as coverage of the server
tool. Model the server tool as a composed base shared with tool_search.

Resolves LIT-5391
2026-08-10 17:38:08 -07:00
Yassin Kortam
d0c65f83f1
fix(websearch): stop leaking interception control fields to providers (#36480)
The web-search interception hooks stamp
_websearch_interception_emit_native_blocks and
_websearch_interception_converted_stream onto kwargs to carry state across
the agentic loop, but neither was registered in all_litellm_params. The
param builder sweeps anything it does not recognize into the outbound
request, so a provider that validates its body rejects the whole call:
Bedrock Converse answers "_websearch_interception_emit_native_blocks:
Extra inputs are not permitted" with a 400, which breaks every request
interception touches on that route.

Register both alongside their code-interpreter counterparts, which were
already listed for exactly this reason.

Resolves LIT-5391
2026-08-10 17:22:37 -07:00
Yassin Kortam
be5e9000b2
perf(spend): write each daily spend batch in one upsert statement (#36448)
The daily spend flush emitted one INSERT ... ON CONFLICT per aggregated key,
so every replica put hundreds of statements on the database each interval,
all contending for the same handful of hot rows and each holding its row
locks for the rest of the enclosing batch transaction. LiteLLM_DailyTagSpend
felt it worst because a request writes one row per tag, and litellm adds two
user-agent tags of its own by default.

A batch now goes out as a single multi-row statement. Rows are folded by the
conflict tuple first, and every nullable member of that tuple is normalized
to '': a NULL can never match itself in a unique index, so such a row was
re-inserted on every flush rather than aggregating, and a NULL model made
prisma reject the whole batch.
2026-08-10 17:06:00 -07:00
Deepanshu Lulla
05943b47a3
fix(router): cool down failed fallback deployments and correct cooldown TTL after Redis backfill (#35104)
* fix(router): cool down failed fallback deployments and correct cooldown TTL after Redis backfill

A deployment that failed partway through a fallback chain (any attempt after
the first) was silently exempt from cooldown, because the has_logged_async_failure
dedup flag blocks the normal failure callback for every attempt past the first.
_trigger_cooldown_for_failed_deployment now explicitly evaluates cooldown for
that deployment when the dedup flag is set, using the same deployment-config >
response-header > router-default precedence as the primary failure path, and
skips advisor-orchestration failures. Deployment-ID resolution prefers the
exception's stamped failed_deployment_id, now also set from the generic-API-call
fallback path (rerank, embeddings, /v1/messages, etc.), falling back to metadata
inspection for call paths that don't stamp it yet.

CooldownCache also recomputes the remaining TTL when DualCache promotes a Redis
entry into the in-memory layer: before this, a cooldown entry restored from Redis
kept the in-memory layer's default 600s TTL regardless of the deployment's real
cooldown_time, so a deployment could stay excluded from routing for up to 10
minutes after a much shorter cooldown had already expired.

* fix(router): address Greptile review on the fallback-cooldown trigger

Two P1 findings on PR #35104:

- _trigger_cooldown_for_failed_deployment never incremented the deployment's
  per-minute failure counter before evaluating cooldown, so a fallback
  deployment's repeated retryable failures never accumulated toward the
  default percent-fail-rate threshold that _should_cooldown_deployment checks.

- The metadata-bucket fallback (checking "metadata" before "litellm_metadata"
  for a deployment_model_name marker) could be fooled by a caller with
  permission to set metadata, since neither bucket's authorship can be
  determined without knowing the call's function_name. Removed it entirely;
  cooldown now requires the server-stamped failed_deployment_id, matching
  what the primary chat-completions path and the generic-API-call path
  (rerank, embeddings, /v1/messages, etc.) already set unconditionally.

* fix(router): freeze the litellm_params fallback mapping to satisfy the type-discipline gate

* fix(router): defer f-string interpolation in fallback-cooldown debug logs

* fix(router): annotate cooldown-path locals with Final to satisfy the LIT010 budget

* fix(router): don't cool down deployments for request-scoped 404s on generic API fallbacks

* fix(router): stamp the dynamic client-side-credential deployment id, not the shared static one

* fix(router): don't cool down deployments for a caller-supplied x-litellm-timeout

* fix(router): stamp dynamic client-side-credential id in completion fallback paths too

The generic-API-call helper already stamped the effective (dynamic-if-client-side-credential)
deployment id on exceptions, but the regular _completion/_acompletion exception handlers still
stamped the static shared deployment's id. A tenant using invalid forwarded credentials could
generate repeated failures attributed to, and eventually cooling down, the shared deployment
other tenants rely on. Extracted the stamping logic into one shared helper used by all three
call sites (generic API, sync completion, async completion) so the fix and future changes to it
stay in one place.

* test(router): add direct-reference unit tests for the new stamping helper

router_code_coverage.py's coverage gate flags _stamp_failed_deployment_id_with_effective_model_info
as untested because it only sees the function invoked indirectly through _completion/_acompletion's
exception handlers. Added two tests that call it directly, covering both the dynamic-id-present and
static-fallback branches.

* test(router): cover the timeout stamping branch and async active-cooldown append

_acompletion's litellm.Timeout handler and async_get_active_cooldowns' happy
path both lacked direct coverage despite their sibling branches (the generic
Exception handler, the sync get_active_cooldowns) being tested.

* test(router): remove duplicate cooldown-trigger and fallback-helper tests

#34416 landed its own TestTriggerCooldownForFailedDeployment/
TestRunAsyncFallbackTriggersCooldown classes and
test_ageneric_api_call_with_fallbacks_helper_stamps_failed_deployment_id
covering the exact same scenarios as this branch's earlier flat-function
tests, once its version of fallback_event_handlers.py was taken as-is
during the last merge. Dropping the redundant copies.

---------

Co-authored-by: Deepanshu <deepanshu.lulla@alpha-sense.com>
2026-08-10 16:51:55 -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
5c1623888e
fix(arize): trace MCP tool calls instead of crashing on CallToolResult (#36453)
* fix(arize): stop MCP CallToolResult from aborting span attribute setting

`call_mcp_tool` logs the MCP SDK's `CallToolResult`, a Pydantic model with
no `.get`. `_coerce_response_obj_for_attrs` left it untouched and
`_set_request_attributes` then raised AttributeError, which aborted the rest
of the attribute block, so MCP tool spans lost their invocation params,
input messages, and outputs.

Dump Pydantic models that lack `.get` to a dict, and guard the response
id/model reads the same way `_set_response_attributes` already does so any
other uncoercible response object degrades instead of crashing.

* feat(arize): render MCP tool calls as OpenInference TOOL spans

`call_mcp_tool` spans carry neither `messages` nor `choices`, so every
generic extraction path left Input and Output blank and the span showed only
provider/model metadata.

Emit `tool.name` from `metadata.mcp_tool_call_metadata`, `input.value` from
the tool arguments, and `output.value` from the `CallToolResult` content
(text parts when present, JSON otherwise). Arguments and results are user
content, so the input/output emit is gated on the same
`should_redact_message_logging` check the passthrough normalizer uses.

Reuse `_to_plain_dict` for the Pydantic coercion instead of the local
BaseModel branch added in the previous commit.

* fix(arize): annotate the new MCP helper parameters

The strict-rule gate flagged three new ANN001 violations. Type the payload
as StandardLoggingPayload | None and the coerced response as object, which
the isinstance guards already narrow.

* fix(arize): annotate the MCP helper against the type-discipline gate

LIT001 bans mutable collections in annotations, so the kwargs parameter
becomes Mapping[str, object]. should_redact_message_logging still declares a
dict it only ever reads, and widening it would cascade into core_helpers, so
the call carries a scoped ignore instead. Narrow the payload by None rather
than isinstance now that it is typed, and annotate the values read out of the
untyped logging payload.

* fix(arize): record empty MCP arguments and results instead of dropping them

Zero-argument tools record arguments={} and successful calls can return
content=[]; both were skipped by truthiness, leaving the generic placeholder
on Input and nothing on Output. Read structuredContent when content yields
no text, and cover the list_mcp_tools response shape.

* fix(arize): keep media parts in mixed MCP results

A result mixing text and media returned the text alone, so Arize showed
text/plain and dropped the image or resource parts.

---------

Co-authored-by: Sean Lee <yihsean@gmail.com>
2026-08-10 16:37:12 -07:00
ryan-crabbe-berri
20354bfcdc
fix(bedrock): reject Anthropic server-side web_search tool with actionable error (#36473)
* fix(bedrock): reject Anthropic server-side web_search tool with actionable error

Bedrock's Anthropic Messages endpoints cannot execute Anthropic's
server-side web_search tool, so forwarding it returns an opaque
"The provided request is not valid" 400 from Bedrock. Fail fast in the
invoke transform with an error that names the unsupported tool, the
model, and links the web search interception docs as the fix.

* refactor(bedrock): address review nits on web_search guard typing
2026-08-10 15:59:18 -07:00
fancybear-dev
f1ed4690bb
fix(proxy): treat SAML as configured in UI SSO detection (#36196)
* fix(proxy): treat SAML as configured in UI SSO detection

_has_user_setup_sso only checked OAuth client IDs, so SAML-only setups
left /.well-known/litellm-ui-config sso_configured=false and the login
button gray even when SAML IdP metadata was set. Include
SAML_IDP_METADATA_URL / SAML_IDP_METADATA_XML so UI discovery matches
the login redirect path.

* chore: adhere to comment policy

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

* fix: apply suggestion from @greptile-apps[bot]

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

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-08-10 15:41:27 -07:00
ryan-crabbe-berri
76ad1c319d
feat(proxy): add GET /v1/indexes to list vector store indexes (#36289)
* fix(scripts): stop type-discipline checker reading Literal strings as forward refs

The checker re-parsed every string constant inside an annotation as a
forward reference, so Literal["list"] was counted as the mutable list
type. Skip Literal subtrees and ratchet the LIT001 ceiling down to the
corrected count.

* fix(proxy): keep lazy openapi snapshot fragments for transitively imported features

generate_snapshot skipped register_fn for any feature module already in
sys.modules, so a module pulled in transitively by an earlier feature
never mounted its routes and its fragment silently vanished on regen
(vector_store_management). Route collection also matched path_prefixes
only, dropping suffix-matched routes from fragments. Register every
feature and collect routes with feat.matches, mirroring the runtime
loader.

* feat(proxy): add GET /v1/indexes to list vector store indexes

/v1/indexes was POST-only, so indexes created through it could never be
viewed again. Add an admin-only list endpoint returning the stored index
rows newest first, fix the stale index_create docstring curl, and
regenerate the lazy openapi snapshot and dashboard schema types.

* chore(proxy): defer lazy openapi snapshot catch-up regen to a follow-up

Reverts _lazy_openapi_snapshot.json and schema.d.ts to the staging
versions. The snapshot was months stale, so regenerating it here buried
the actual change under ten thousand generated lines. A follow-up will
land the regen together with CI enforcement that keeps the snapshot
current. Until then GET /v1/indexes is served but absent from the
dashboard's generated types, which the UI step needs anyway.

* fix(proxy): use Annotated dependency to avoid new B008 violation
2026-08-10 15:09:59 -07:00