Commit graph

10044 commits

Author SHA1 Message Date
yucheng-berriai
02f4fd1610 test: cover JWT multi-issuer claim normalization edge cases 2026-06-17 17:40:42 -07:00
yucheng-berriai
bf87f7a8f0 fix: allowlist JWT claims in multi-issuer normalization
Build the normalized token from standard registered claims plus the claims each
issuer explicitly maps, instead of passing through all non-internal claims. The
single-issuer path is unchanged
2026-06-17 17:40:42 -07:00
Mateo Wang
556e8f89c8
ci: run a local fake OpenAI endpoint instead of the shared Railway mock (#30695)
Several CI jobs run the proxy against a model whose api_base is a shared
"fake OpenAI endpoint" hosted on Railway
(exampleopenaiendpoint-production.up.railway.app) so the E2E runs return
canned responses without paying for or depending on a live provider. When
that single deployment is down, every one of those jobs fails with
"404 Application not found" even though nothing in the PR is broken; the
whole repo is coupled to the uptime of one free external service.

This adds tests/_fake_openai_endpoint_server.py, a small canned-response
OpenAI-shaped server (chat, text, embeddings, streaming with usage, and the
"429" rate-limit special case), and a reusable start_fake_openai_endpoint
CircleCI command that runs it on host port 8190 and waits until healthy. The
affected jobs now inject FAKE_OPENAI_API_BASE pointing at the local server,
and the example configs they mount resolve api_base from that env var. The
intentionally bad fallback URL in proxy_server_config.yaml is left untouched
so the fallback test still exercises a failing upstream.

Wired into build_and_test, litellm_router_testing,
db_migration_disable_update_check, proxy_logging_guardrails_model_info_tests,
proxy_spend_accuracy_tests, proxy_multi_instance_tests,
proxy_store_model_in_db_tests, and proxy_build_from_pip_tests.
2026-06-17 17:01:13 -07:00
Yassin Kortam
187b205b34
fix(pod_lock): release cron lock by matching async_set_cache JSON encoding (#30600)
acquire_lock stores the pod_id through async_set_cache, which JSON-encodes
the value, so Redis holds the quoted string "<pod_id>". release_lock's Lua
compare-and-delete compared the raw pod_id, so the equality check never
matched and the lock was never deleted; it only cleared on TTL expiry. That
stalled the spend-update drain whenever the leader pod restarted, letting the
litellm_daily_*_spend_update_buffer lists grow unbounded in Redis.

Compare against json.dumps(self.pod_id) so the release matches the stored
value. The GET+DEL fallback already round-trips through async_get_cache and is
unaffected.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-17 17:01:04 -07:00
tin-berri
ba29657d09
feat(proxy): warn at startup when custom_auth skips common_checks enforcement (#30665)
When general_settings.custom_auth is configured but custom_auth_run_common_checks
is not set, project/team/org enforcement (budgets, model-level rate limits, and
model-access lists) silently does nothing for custom-auth requests, since the
centralized common_checks gate returns early for custom auth. Emit a startup
warning pointing operators at the flag so the misconfiguration is visible instead
of failing silently.
2026-06-17 16:28:14 -07:00
Mateo Wang
43dadc5138
fix(cost): stop non-string service_tier from silently dropping cost tracking (#30690)
completion_cost read service_tier straight from the request optional_params
and called service_tier.lower() on it, so a non-string value (dict/int/list,
reachable via allowed_openai_params/drop_params) raised AttributeError.
_response_cost_calculator swallowed that and returned response_cost=None, so
the request's cost was silently lost.

The isinstance guard alone is not enough: a surviving dict would crash again
downstream in _get_service_tier_cost_key, which also calls .lower(). A
request-level service_tier is only meaningful for pricing when it is a concrete
billable tier string, so coerce any non-string value to None and defer to the
tier the provider reports on the response usage, the same way "auto" already
does.

Adds a regression test driving a dict service_tier through completion_cost; it
raises AttributeError before the fix and prices at the served tier after.
2026-06-17 15:36:23 -07:00
Mateo Wang
4ccc32312d
test(pass_through): harden vertex spendlog poll against transient empty reads (#30683)
test_basic_vertex_ai_pass_through_with_spendlog failed intermittently on
litellm_internal_staging (pipelines 82155, 82196, 82209, 82230) with "Spend
should be greater than before after 120s". Spend logging is async and batched,
so the pass-through call's cost sometimes had not landed within the 120s poll
window; one run ended on spend_after 0.0 because the final /global/spend/logs
read returned nothing and "or 0.0" recorded that as zero spend.

Widen the poll window to 240s and skip a transient empty read instead of
treating it as 0.0, so a momentary endpoint hiccup on the last poll no longer
fails an otherwise-billed call. The spend_after > spend_before assertion is
unchanged, so a genuinely unbilled call still fails the test
2026-06-17 15:11:44 -07:00
Mateo Wang
654e354ebd
test: harden remaining pass-through CI flakes (image-gen spend poll, ruby assistants timeout) (#30685)
* test(proxy): poll for image-gen spend instead of a fixed 5s sleep

test_key_info_spend_values_image_generation failed once on litellm_internal_staging
(pipeline 82282) with "spend did not increase on an identical repeat image call"
(assert 0.24966 > 0.24966). The test made the second image call, slept 5s, then
read the key's spend once. Response caching is commented out in
proxy_server_config.yaml and no sibling test enables it, so the likely cause is
async/batched spend logging not having flushed the repeat call's cost within 5s,
which the build_and_test job aggravates by running every tests/test_*.py against
one shared proxy under pytest -n 4.

Poll the key's spend for up to 60s and break as soon as it grows. This removes
the timing flake while preserving the canary: if the repeat were genuinely
unbilled (for example the proxy response cache being on), spend never grows, the
poll times out, and the assertion still fails.

* test(pass_through): raise ruby assistants client request_timeout to 600s

The streaming assistants example in openai_assistants_passthrough_spec.rb hit
Net::ReadTimeout on litellm_internal_staging (pipeline 82280), failing at roughly
125s which is ruby-openai's default request_timeout of 120s. An assistants run
with the code_interpreter tool can occasionally take longer than that to stream
its first content back through the pass-through.

Raise the client's request_timeout to 600s, matching the 600s timeout the Python
pass-through e2e tests already use, so a slow-but-healthy streaming run no longer
trips the default read timeout.
2026-06-17 14:35:47 -07:00
Mateo Wang
c51ba34294
fix(health): correct bedrock embedding health checks (#30583)
* fix(health): correct bedrock embedding health checks

Health checks for Bedrock embedding deployments failed in two ways. A
deployment configured without an explicit model_info.mode was probed as
chat, so max_tokens was injected and Bedrock embeddings rejected it with
400 "extraneous key [max_tokens]". Separately, stripping the bedrock/
routing prefix dropped the provider, so a cross-region inference-profile
id like us.cohere.embed-v4:0 failed downstream with "LLM Provider NOT
provided".

Resolve the deployment mode from the model cost map (which understands
the bedrock/ and us./eu./apac. prefixes) before deciding whether to
inject max_tokens, and pin custom_llm_provider to bedrock when stripping
the prefix so the bare model id still resolves. ahealth_check now accepts
any string mode so the resolved embedding mode routes the probe to the
embedding handler.

* fix(health): preserve explicit custom_llm_provider on bedrock probe

The bedrock prefix-strip pinned custom_llm_provider to bedrock
unconditionally, so a deployment that set custom_llm_provider:
bedrock_converse had it overwritten at health-check time and the probe
hit the Invoke endpoint instead of Converse, a different request format
that can report a spurious failure. Only fill in bedrock when the
deployment left the provider blank, which still resolves bare
cross-region ids like us.cohere.embed-v4:0 while leaving an explicit
provider untouched.

* test(health): assert resolved mode reaches the ahealth_check probe

The existing tests check _resolve_health_check_mode and the params builder
in isolation, but nothing verified that _run_model_health_check actually
threads the resolved mode into litellm.ahealth_check. Without that, a
refactor that probed with model_info.get("mode") again would reintroduce
the chat fallback for embedding deployments while every test stayed green.
This drives _run_model_health_check with a bedrock embedding deployment and
asserts the probe is called with mode=embedding and the embedding params.

* fix(health): resolve probe mode once for reasoning_effort and audio_speech

The reasoning_effort and audio_speech branches read model_info.mode
directly, so an embedding deployment declared without an explicit mode (the
case this PR targets) was still treated as chat-like: a configured
health_check_reasoning_effort got injected into the embedding probe, which
embeddings reject as an unknown field, and an auto-detected audio_speech
deployment never had its voice set. Resolve the effective mode once from the
cost map and reuse it for the max_tokens, reasoning_effort, and audio_speech
decisions so they all agree with the mode threaded into ahealth_check.
2026-06-17 14:34:09 -07:00
Yassin Kortam
39ab43c10a
feat(proxy): add --max_requests_before_restart_jitter to stagger worker restarts (#30601)
Setting --max_requests_before_restart alone recycles every worker at almost the
same time once they have served a similar number of requests, which under
sustained load can drop a whole pod's capacity at once roughly every 7-10 days.

This exposes a jitter knob that adds a random amount in [0, jitter] to the
restart threshold per worker so restarts are staggered. It maps to uvicorn's
limit_max_requests_jitter and gunicorn's max_requests_jitter. uvicorn only
gained limit_max_requests_jitter in 0.41.0 while litellm still allows
uvicorn>=0.33.0, so the uvicorn path feature-detects the parameter via the
Config signature and warns instead of crashing on older versions. The flag has
no effect without --max_requests_before_restart, so the kwarg is not forwarded
in that case and a warning is printed on both the uvicorn and gunicorn paths.

Resolves LIT-3774
2026-06-17 11:28:48 -07:00
Shivam Rawat
6c8b60d50d
fix(proxy): resolve list files credentials from team BYOK deployments (#30495)
* fix(proxy): resolve list files credentials from team BYOK deployments

GET /v1/files without target_model_names now prefers the team's own
deployment (model_info.team_id) over shared global provider keys, so JWT
team auth lists files against the correct upstream account.

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

* fix(proxy): scope list files credential lookup to team allowlist

Remove the unrestricted deployment scan that could leak global provider
keys to teams without access, normalize all-proxy-models to the team-scoped
model list, and fix TID251 violations by using dict instead of Dict/Any.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 10:32:52 -07:00
Yassin Kortam
78a7d0b210
feat(guardrails): surface OpenAI moderation violation_categories on guardrail traces (#30659)
The OpenAI moderation guardrail (and the ai-platform-moderation guardrail
built on it) stamped the whole moderation model response into the guardrail
trace as guardrail_response. That blob carries the full category_scores map
plus categories and category_applied_input_types, which on OTEL backends that
index span attributes (for example ELK, which caps indexed attribute values at
1024 chars) overflows the limit and gets truncated, so the violated categories
cannot be reliably searched.

Extract the flagged category names from the moderation response and pass them
through tracing_detail to add_standard_logging_guardrail_information_to_request_data,
mirroring the Bedrock hook. Both the legacy and v2 OTEL integrations already
read violation_categories off the standard logging guardrail information and
emit it as a short, queryable guardrail_violation_categories attribute, so
dashboards can group and filter by violation category without parsing the large
guardrail_response blob.

Resolves LIT-3801
2026-06-17 09:44:19 -07:00
Mateo Wang
b8d79d1e0c
ci: drop mypy entirely, standardize type checking on basedpyright (#30648)
* ci: drop redundant mypy type-check gate, standardize on basedpyright

Type checking ran both mypy (via the pydantic.mypy plugin) and basedpyright.
pydantic v2 emits dataclass_transform, so basedpyright understands models
natively with no plugin, and its gated rules already cover what the mypy pass
caught (no-untyped-def, no-any-return, valid-type, import-not-found all map to
basedpyright equivalents). Running both meant two checkers, two budgets, and a
plugin only mypy could load.

This removes the mypy type-check gate: the lint-mypy/lint-mypy-budget-update
Makefile targets, the CI MyPy step, mypy-code-budget.json, the budget-ratchet
entry, and the vestigial [tool.mypy] pydantic plugin block (the gating pass used
litellm/mypy.ini, which never loaded the plugin). type_check_gate.py is
specialized to basedpyright since the mypy parsing path is now unused.

mypy stays a dev dependency because the Any-discipline gate
(scripts/check_any_discipline.py) imports it as a library to detect Any-typed
values; it is no longer run as a type checker.

* ci: remove the Any-discipline gate, rely on basedpyright's reportAny

The Any-discipline gate (scripts/check_any_discipline.py) was the last consumer
of mypy: it imported mypy as a library to detect values whose inferred type
contains Any, gated per-file against any-discipline-budget.json. basedpyright
already reports the same class of finding through reportAny/reportExplicitAny,
which are gated tree-wide in basedpyright-code-budget.json, so the separate gate
(and the mypy dependency behind it) is redundant.

Removes the gate end to end: check_any_discipline.py and its test, the
any-discipline CI job, the lint-any/lint-any-budget-update Makefile targets,
any-discipline-budget.json, litellm/mypy.ini, the .mypy_cache_any references,
and mypy from the dev dependencies. budget_ratchet_check.py drops the
any-discipline entry and the now-unused zero-floor mechanism (rewritten as a
comprehension). check_type_discipline.py drops the any-ok suppression token,
since # any-ok suppressed only the deleted gate; the 134 now-orphaned
# any-ok comments across 14 files are stripped (they never affected
basedpyright, which uses # pyright: ignore).

uv.lock is intentionally left untouched: uv still considers it consistent with
the mypy-removed pyproject (uv lock --check and uv sync --frozen both pass), and
a relock bumps 30+ unrelated packages because of the moving exclude-newer window.
A future intentional relock will prune the now-unreferenced mypy entry.

* build: relock to drop mypy from uv.lock

CI's uv 0.10.9 honors the repo's exclude-newer window and correctly flags the
lockfile as out of sync once mypy leaves pyproject; my earlier local uv 0.8.17
could not parse exclude-newer and silently passed --check. Relocking with the
pinned CI version removes only mypy and its transitive librt, with no other
version changes.
2026-06-17 09:42:00 -07:00
ishaan-berri
60f4c01b74
fix(proxy): list public team model name in /v1/models (#30588)
* fix(proxy): optionally surface public team model name in /v1/models

Behind general_settings.use_team_public_model_name (default False). When
enabled, /v1/models and /models surface the public team_public_model_name
for team-scoped (BYOK) models instead of the internal routing key
model_name_{team_id}_{uuid} -- consistent with /v1/model/info and
OpenAI-compatible. Off by default so the listing's model ids stay
backward-compatible for callers that scripted against the internal name;
routing by the internal name is unchanged regardless of the flag.

Presentation-layer only: access-group, auth, and routing semantics are
unchanged; non-team models are pass-through.

* fix(proxy): default team model listings to public names

* test(proxy): cover team model listing metadata

* test(proxy): cover empty team listing deployments

* refactor(proxy): simplify team model listing translation

* fix(proxy): resolve public team model name on GET /v1/models/{id}

The listing endpoints advertise team_public_model_name, but the retrieve
endpoint validated and looked up by the raw id, so a public name 404'd.
Resolve the public name back to the internal routing key (scoped to the
caller's accessible models so colliding names never cross teams), look up
by it, and echo the public name back as the response id.

* test(proxy): cover public-name resolution on model retrieve

* refactor(proxy): extract team model-name translation into TeamModelNameTranslator

Move the team-scoped (BYOK) listing/retrieve name translation out of
proxy_server.py into a dedicated common_utils module. Static methods with
general_settings injected so the logic is unit-testable without globals and
proxy_server.py stays thin.

* refactor(proxy): use TeamModelNameTranslator in model_list and model_info

* test(proxy): target TeamModelNameTranslator for model-name translation

* fix(proxy): type create_model_info_response return as dict[str, object]

* fix(proxy): keep internal routing key for team model listing metadata lookup

Add listing_entries returning (public response id, internal lookup id) so
include_metadata=true resolves fallbacks against the routing key the router
indexes by, instead of the translated public name (which never matches).

* fix(proxy): build /v1/models metadata from internal key, show public id

* test(proxy): cover team listing fallback metadata via internal key

* fix(proxy): use builtin dict generics in create_model_info_response (UP006)

---------

Co-authored-by: Tushar More <tusharmore8408@gmail.com>
Co-authored-by: Ishaan Jaffer <ishaanjaffer0324@gmail.com>
2026-06-17 09:17:22 -07:00
Mateo Wang
b638bc2248
fix(anthropic): price and surface response service_tier in cost tracking (#30558) 2026-06-17 06:33:51 -07:00
Mateo Wang
cf2db415b8
fix(audio): don't override explicit response_format with verbose_json (#30599)
* fix(audio): don't override explicit response_format with verbose_json

* fix(audio): handle plain-text response body for response_format=text

* fix(audio): only swallow non-JSON transcription body when not declared JSON

Guard the plain-text fallback in transform_audio_transcription_response with
the response Content-Type: a body that fails json() but is labelled
application/json is a genuine upstream error and is re-raised, while
text/plain bodies (response_format=text) are still returned as-is. Prevents
a malformed JSON 2xx from silently becoming a transcription of garbled bytes.

* fix: normalize content-type header case in whisper transcription fallback

* test(audio): lock in case-insensitive content-type guard for transcription fallback

Adds a regression test that a mixed-case 'Application/JSON' content-type still
re-raises a malformed JSON body, covering the case-insensitivity fix in 72982e4
(removing the .lower() normalization fails this test).

---------

Co-authored-by: cohml <62400541+cohml@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-06-17 11:13:10 +05:30
Mateo Wang
17b88719a2
ci(lint): grandfather any-discipline with a per-file ratchet budget (50% headroom) (#30582)
* ci(lint): grandfather any-discipline with a per-file ratchet budget (50% headroom)

The any-discipline gate previously failed on any Any-typed value touched on a
changed line, which tripped on merely editing a legacy `X | Any` line. Switch it
to a per-file budget: `any-discipline-budget.json` records each file's current
Any count and a changed file fails only when its count exceeds `baseline + slack`
(50% headroom, rounded up). New/unbudgeted files have baseline 0, so they stay
airtight, while editing legacy files no longer forces cleaning pre-existing debt.

Only changed files are re-type-checked (per-PR cost unchanged); the whole-tree
scan to recapture the budget runs under `--update` (`make lint-any-budget-update`).
The budget is a one-way ratchet guarded by `budget_ratchet_check.py`, matching the
ruff/mypy/basedpyright budgets, and folds into `make lint-budget-update`.

Also fixes a RecursionError in `contains_any` (recursive type aliases yield fresh
objects per unfold, defeating the id() cycle guard) by walking iteratively with a
depth cap, exposed by the whole-tree scan.

* chore: make CLAUDE.md more concise

* chore: rearrange Makefile

* ci(lint): make any-budget --update git-failure-safe; clarify over-budget message

all_litellm_py_files now returns None when git is unavailable (mirroring
changed_line_map) instead of letting CalledProcessError/FileNotFoundError escape
as a raw traceback, and update_budget reports a clean setup error (exit 2) for
that case. The list-files dependency is injected so the path is unit-testable
without monkeypatching. The over-budget diagnostic now reads "N value(s) total,
over budget" so the count isn't misread as the excess over the ceiling.

* ci(lint): exempt the file-keyed any-discipline budget from the ratchet's dropped-entry rule

budget_ratchet_check treats a vanished budget entry as a loosening (an untracked
rule whose ceiling is now unbounded). That holds for the rule-keyed budgets, but
the any-discipline budget is keyed by file and its gate treats an absent file as
ceiling 0 (the file must be Any-free). Cleaning a file to zero drops its entry on
the next --update, so the generic rule flagged that as a regression: a false-
positive red on exactly the cleanup the ratchet exists to encourage. Exempt the
file-keyed budget from the dropped-entry rule while still catching a raised
ceiling.
2026-06-16 19:23:20 -07:00
ryan-crabbe-berri
b5fcd859be
fix(guardrails): return 400 not 500 when AIM blocks a request (#30573)
* fix(guardrails): return 400 not 500 when AIM blocks a request

AIM guardrail blocks raised a bare HTTPException whose type and param
serialized as the literal string "None", which broke OpenAI-SDK error
parsing for downstream consumers. Switching AIM to raise a ProxyException
surfaced a second bug: the shared error funnel re-derived the HTTP status
from a nonexistent status_code attribute and downgraded the 400 to a 500.
The funnel now honors an already-normalized ProxyException rather than
rebuilding it, and ProxyException is excluded from llm_exceptions alerting
so a content-policy block no longer pages on-call as an LLM API failure

Resolves LIT-3751

* fix(guardrails): route all AIM rejection paths through ProxyException

The block-action fix left two AIM rejection paths raising a bare
HTTPException: the multimodal anonymize rejection and the output-side
block. Both serialized type and param as the literal string "None", the
same malformed shape the block fix removed. Funnel all three through a
shared _rejection helper so they return a conformant OpenAI error body.
The output block carries content_policy_violation; the multimodal
rejection stays a plain invalid_request_error because it is a usage
error, not a policy violation

Resolves LIT-3751

* fix(guardrails): record AIM ProxyException blocks in failure logs

Switching AIM blocks from HTTPException to ProxyException made
_is_proxy_only_llm_api_error return False for them, so
_handle_logging_proxy_only_error was skipped and the blocked prompt was
dropped from the configured failure loggers. Classify ProxyException as a
proxy-only error alongside HTTPException so guardrail blocks are recorded
again, matching the prior behavior. The llm_exceptions alert suppression
is a separate check and stays in place

Resolves LIT-3751

* style(guardrails): use str | None over Optional[str] in AIM _rejection

* style(guardrails): collapse AIM _rejection signature per black
2026-06-16 18:56:14 -07:00
Sameer Kankute
1ccc1e5b23
chore: litellm oss staging160626 (#30527)
* feat(ui): gate "Default Credentials" hint on /ui/login behind env flag (#30234)

Adds LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT (and an equivalent
general_settings.hide_default_credentials_hint) that suppresses the
"By default, Username is admin and Password is your set LiteLLM Proxy
MASTER_KEY" info card rendered on /ui/login and /fallback/login.

Motivation: in production deployments operators set UI_USERNAME /
UI_PASSWORD (or SSO), and the hardcoded hint becomes factually
incorrect and is flagged by security scanners (Tenable WAS plugin
114625) as information disclosure. There is currently no way to
suppress it without forking the dashboard.

Behaviour:
- Default is unchanged (hint shown), so existing deployments are
  unaffected.
- New field hide_default_credentials_hint on the well-known UI config
  endpoint, populated from the env var or general_settings.
- LoginPage.tsx conditionally renders the Alert based on the flag.

Refs: BerriAI/litellm#30232

* fix(router): clean pattern_router state on upsert/delete (#29601)

* fix(router): clean pattern_router state on upsert/delete

PatternMatchRouter.add_pattern was append-only, and neither Router.upsert_deployment nor Router.delete_deployment removed the existing entry. Rotated-out api_keys stayed in the routing rotation for wildcard deployments (model_name with `*`) until proxy restart, silently defeating key rotation as an admin operation. The same leak applied to provider_default_deployment_ids and per-team pattern routers, and the patterns list grew unboundedly on every edit

* test(router): direct unit tests for _remove_deployment_from_wildcard_state

router_code_coverage.py greps test files for AST Call nodes and flagged
the helper as untested because the existing coverage only exercised it
transitively through upsert/delete. Adds two direct tests that pin the
helper's contract (cleans across global pattern router, per-team
routers with empty-router pop, and provider_default_deployment_ids;
noop on falsy model_id)

* fix(router): address Greptile review on pattern_router cleanup

Widen PatternMatchRouter.remove_deployment annotation to Optional[str];
the implementation already handles None via the falsy guard and the
unit test exercises it directly.

Move _remove_deployment_from_wildcard_state up one level in
upsert_deployment so it runs whenever the prior deployment is on the
router, not only when the model_id is present in the fast-mapping
index. The scenario is currently unreachable (get_deployment shares
the same index), but the cleanup is idempotent so this is defensive
against any future divergence between those code paths.

* fix(router): widen _remove_deployment_from_wildcard_state to Optional[str]

Moving the call out of the inner `deployment_id in deployment_fast_mapping`
block in the previous commit lost mypy's narrowing of `deployment_id`
from Optional[str] to str, tripping the lint CI. The helper already
handles None via its falsy guard, so widening the annotation matches
the actual contract.

* fix(router): make delete_deployment wildcard cleanup symmetric with upsert

After the previous commit moved _remove_deployment_from_wildcard_state out
of the inner index-map guard in upsert_deployment, delete_deployment was
still calling it only inside `if deployment_idx is not None`. Greptile
flagged the asymmetry: under a desynced index_map, delete would silently
leave the stale wildcard credential in pattern_router.

Moves the cleanup call to the top of the try block, mirroring the upsert
path. Cleanup is idempotent so the change is a no-op on the happy path.
Adds a regression test that simulates the desync by removing the entry
from model_id_to_deployment_index_map and asserts delete still clears
pattern_router.

* fix(pricing): add 1h cache-write cost for Anthropic Sonnet 4.5/4.6 (#30474)

The native anthropic claude-sonnet-4-5/4-6 price-map entries were missing
cache_creation_input_token_cost_above_1hr (and the >200K long-context
sub-tier for 4.5), so 1-hour-TTL cache writes were costed at the 5-minute
rate. Adds 6e-06 regular (and 1.2e-05 long-context) = 2x base input,
matching the vertex_ai/azure_ai/bedrock siblings and the older
claude-sonnet-4-20250514 entry. Adds a regression test.

* fix(proxy): cancel upstream gemini request and release httpx connection on client disconnect (#30075)

* fix(proxy): cancel upstream gemini request and release httpx connection on client disconnect

- add _check_request_disconnection to common_request_processing; wrap llm_call
  as asyncio.Task so it can be cancelled; catch CancelledError and raise
  HTTPException(499) when client disconnects before LLM responds (non-streaming path)

- pass raw httpx.Response into ModelResponseIterator in make_call/make_sync_call
  so the iterator holds a reference to the underlying connection

- implement ModelResponseIterator.aclose() and .close(): close the line iterator
  then explicitly call response.aclose()/response.close() to release the httpx
  connection when the client drops mid-stream; errors are debug-logged, not raised

- add tests for _check_request_disconnection (cancels task, graceful on exception,
  does not cancel when client stays connected) and base_process_llm_request 499
  behavior; add TestModelResponseIteratorCleanup verifying aclose/close propagation
  through CustomStreamWrapper

* fix(proxy): record 499 on streaming disconnect and cancel orphaned gather tasks

Wire streaming generator cleanup to log client_disconnected with error_code 499
in spend logs, cancel pending during_call_hook tasks when the LLM call is
cancelled on disconnect, and align the 600s poll limit comment with proxy_server.

* fix: extract client disconnect logging helper to satisfy PLR0915

* fix: resolve mypy and code-quality CI failures for client disconnect logging

Cast client disconnect error_information for mypy, only await pending gather tasks to avoid masking LLM errors, and add tests for the new logging helper and gather cleanup.

* fix(proxy): harden gather cleanup so finally cannot mask LLM errors

* fix(proxy): shield streaming disconnect logging and strip spoofable metadata

Move streaming disconnect recording into a shielded cancel scope, add gather cleanup regression coverage for guardrail-converted cancels, and strip client_disconnected/error_information from user metadata at the proxy boundary.

* fix(proxy): only map CancelledError to 499 for client disconnect

Track when the disconnect poller cancels the LLM task and re-raise other CancelledError paths so graceful shutdown is not reported as HTTP 499.

* fix(proxy): remove dead _check_request_disconnection helper

Non-streaming client disconnect is handled by staging's cancel_on_disconnect path via _await_llm_call_cancelling_on_disconnect. Drop the unused is_disconnected poller and its unit tests; rename the remaining integration tests to TestDisconnectGatherCleanup.

* feat(mistral): add mistral-medium-3-5 to model_prices_and_context_wind.. (#29303)

* feat(mistral): add mistral-medium-3-5 to
  model_prices_and_context_window.json

Mistral's docs page lists mistral-medium-3-5 as a new model offering.

Pricing/specs sourced from Mistral's published model metadata:
- input: $1.50 / 1M tokens
- output: $7.50 / 1M tokens
- context: 262,144 tokens
- capabilities: vision, function calling, structured outputs, assistant
  prefill

Adds entry: `mistral/mistral-medium-3-5`, mirroring the pattern used for
the rest of the Mistral family.

test(mistral): add model_info test for mistral-medium-3-5 + sync backup
cost map
- Mirror mistral/mistral-medium-3-5 entries into
  litellm/model_prices_and_context_window_backup.json so the bundled
  model cost map matches the canonical
  model_prices_and_context_window.json.
- Add tests/test_litellm/test_mistral_medium_3_5_model_metadata.py
  covering pricing tiers, capability flags, context window, provider
  routing, and parity between the main and backup cost maps.
- Point 'source' at the live Mistral models documentation page.

* fix(ui): three small UI fixes — Gemini api_base + credential form reset + Mode badge (#30419)

* fix(ui): three small UI fixes — Gemini api_base field + credential form reset + Mode badge

Three independent fixes; bundled because they all touch the
credential-form / logging-callbacks area.

1. expose api_base field on Google AI Studio credential form
   The runtime gemini provider supports custom api_base via
   `vertex_llm_base._check_custom_proxy`; the UI just needs to expose
   the field. Adds api_base to the Google_AI_Studio credential form
   ordered before api_key (matching OpenAI/Anthropic conventions).
   Default value matches the canonical Google AI Studio endpoint that
   LiteLLM's gemini provider talks to when api_base is unset, so
   leaving the default in the form behaves identically to leaving it
   blank.

2. reset credential form state when switching providers
   Switching the Provider select in AddCredentialModal / EditCredentialModal
   left the previous provider's field values populated. The form then
   submitted a mixed payload (e.g. Azure deployment fields under an
   OpenAI credential), producing confusing failures.

   Extract `getProviderFieldDefaults` helper and reset the form to it
   on provider change. Unit-tested via the extracted helper because
   Antd Select's portal/dropdown behaviour is unreliable in jsdom.

3. logging callbacks table reads backend `type` for Mode badge (#35)
   The `/get_callbacks` proxy endpoint returns each callback as
   `{name, type, variables}` where `type` is `"success"` or
   `"failure"`. The same callback name can appear twice (one per event
   class) and the two entries fire on disjoint events.

   `LoggingCallbacksTable` ignored `type` and read `record.mode`
   (always undefined), so every row fell back to the "Success" badge.
   A `generic_api` callback registered for both classes showed up as
   two identical "Success" rows + React duplicate-key warning.

   Read `record.type` first (fall back to `record.mode` for newly-
   added not-yet-server-acknowledged rows). Composite rowKey
   `${name}-${type ?? mode ?? 'success'}`. Removed leftover debug
   `console.log`.

* fix(ui): drop api_base default_value to preserve Gemini v1alpha auto-routing

Greptile P2 (PR #30419, threads on lines 1255-1256 of
provider_create_fields.json): the api_base field's `default_value` was
hard-coded to "https://generativelanguage.googleapis.com/v1beta". This:

1. Bakes v1beta into every credential record saved through the form,
   even when the user never touched the field. If LiteLLM's internal
   gemini default URL ever changes, those persisted credentials keep
   hitting the stale path.

2. Bypasses `_get_gemini_url`'s automatic version routing for Gemini 3+
   models. That helper picks v1alpha for Gemini 3+ and v1beta for older
   models when api_base is unset. With the default pre-filled (and
   `_check_custom_proxy` then taking over because api_base is non-empty),
   Gemini 3+ requests get pinned to v1beta and may fail or behave
   unexpectedly — purely because the user accepted the visible default.

Fix: set `default_value` to `null` and move the canonical URL guidance
into the `placeholder` (visible to the user, never persisted) and an
expanded tooltip. UX is unchanged — the URL is still shown in the
greyed-out input — but the auto-version-routing path stays default.

Updated test_google_ai_studio_provider_fields_expose_api_base to assert
the new contract (`default_value is None`, `placeholder` carries the
canonical URL), with a comment pointing at the Greptile threads as the
rationale so future contributors don't accidentally re-introduce the
default.

26/26 tests in the file pass. JSON validates (`json.load` clean).

* feat(azure_ai): add gpt-5.5 to model cost map (#30428)

* feat(azure_ai): add gpt-5.5 to model cost map

Adds azure_ai/gpt-5.5 and its dated snapshot azure_ai/gpt-5.5-2026-04-23 to
both the canonical and bundled cost maps. gpt-5.5 is generally available on
Azure AI Foundry; pricing mirrors the openai gpt-5.5 entry, matching the
established azure_ai convention (verified identical for gpt-5.4), in the
azure tier structure (base / above-272k / priority). supports_minimal_
reasoning_effort is false, the capability that changed from gpt-5.4.

Fixes #30306

* Update tests/test_litellm/test_gpt_5_5_model_metadata.py

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>

* fix: guard check_and_fix_namespace against None key (#30435)

* fix: guard check_and_fix_namespace against None key

When user_id is None, the cache key can be None, causing
AttributeError: 'NoneType' object has no attribute 'startswith'
in check_and_fix_namespace.

Add an early return for None key to prevent the error and the
ERROR-level log noise it produces on every unauthenticated request.

Fixes #30424

* fix: update type annotations for check_and_fix_namespace

- key: str -> Optional[str] (now handles None input)
- return: str -> Optional[str] (returns None when input is None)

Addresses Greptile review concern about type signature mismatch.

* fix: revert check_and_fix_namespace type signature to str to fix MyPy downstream errors

* fix: update type annotations for check_and_fix_namespace

- Change signature from str -> str to Optional[str] -> Optional[str]
- Remove type: ignore comment on None return
- Add None guard in async_set_cache_sadd before passing to helper

Addresses review feedback from Sameerlite on type mismatch.

* Revert "fix: update type annotations for check_and_fix_namespace"

This reverts commit 5272920fa0.

---------

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

* fix(cost): apply service_tier suffix to above-threshold cache rates and expose priority+threshold keys in ModelInfo (#30450)

* fix(cost): apply service_tier suffix to above-threshold cache rates and expose priority+threshold keys in ModelInfo

Models that publish both a service_tier (e.g. priority) rate and an above-threshold tier (e.g. _above_200k_tokens) currently bill cached tokens at the standard above-threshold rate rather than the priority above-threshold rate. Affected entries in the live pricing JSON include gemini-3-pro-preview, gemini-3.1-pro-preview and their vertex_ai/ and gemini/ variants, plus azure/gpt-5.4 and azure_ai/gpt-5.4. For a 250K-token priority request with 200K cached tokens against gemini-3-pro-preview, the leak is about 44 percent of the prompt cost.

Two stacked defects caused this. First, ModelInfoBase (and the ModelInfo pydantic class) and the get_model_info construction in litellm/utils.py omit the priority+above-threshold cost keys, so even if the calculator asked for them they would never reach it. Second, in _get_token_base_cost the cache_creation/cache_read tiered keys never get wrapped with _get_service_tier_cost_key, while the input/output tiered keys above and below do. The change here surfaces six new keys (input, output and cache_read at both 200k and 272k priority variants) and wraps the three cache tiered keys in _get_token_base_cost the same way input/output already are. _get_cost_per_unit's existing service_tier-to-base fallback covers models that ship the standard above-threshold rate without a priority variant.

Adds one regression test in tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py that drives the actual generic_cost_per_token path for gemini-3-pro-preview at 200K cached + 50K text under priority and asserts the priority above_200k rates are picked. Verified the test fails on litellm_internal_staging without these changes and passes with them.

* fix(cost): drop guard on cache tiered keys so service_tier fallback can reach standard above-threshold rate

Addresses Greptile P1 on PR 30450. The previous commit wrapped cache_creation_tiered_key, cache_creation_1hr_tiered_key, and cache_read_tiered_key with _get_service_tier_cost_key (matching how the sibling input and output tiered keys are wrapped) but kept the surrounding 'if key in model_info' guards. For models that publish a standard above-threshold cache rate but no priority variant (gpt-5.4-pro, gpt-5.5-pro and their dated siblings, plus vertex_ai/claude-sonnet-4-5 for cache_creation), the guard short-circuits before _get_cost_per_unit's existing service_tier-to-base fallback can strip _priority and find the standard above-threshold key. The result on priority requests over the threshold was that those models silently dropped from the above-threshold rate back to the priority-base rate. Dropping the guard and calling _get_cost_per_unit unconditionally (mirroring how tiered_input_key and tiered_output_key are already handled) restores correct billing for that class of models while keeping the new priority+above-threshold behaviour for gemini-3-pro-preview and friends.

Adds a second regression test that pins generic_cost_per_token for vertex_ai/claude-sonnet-4-5 priority + above_200k with cached and cache_creation tokens to the expected standard above-threshold rates, so the guard cannot be silently reintroduced for either the cache_read or cache_creation path.

* fix(presidio): skip pre-call masking when guardrail is logging_only (#30461)

The Presidio pre-call hook masked the live request unconditionally, ignoring
the configured event hook. With mode: logging_only the masked request reached
the model, so its response echoed anonymization tokens (e.g. <PERSON>) instead
of the real output. Gate async_pre_call_hook on should_run_guardrail, matching
every other guardrail; logging_only masking still happens via async_logging_hook.

* fix(router): resolve list unhashable crash on model alias (#30464)

* fix(router): resolve list unhashable crash on model alias

Fixes the fallback parsing logic that mistakenly categorized standard array fallback definitions as override dictionaries when a deployment alias matches the literal string 'model'.

Closes https://github.com/BerriAI/litellm/issues/30459

* fix(router): address greptile review for fallback parsing edge cases

- Resolves ambiguity in standard vs override fallback dictionaries by iterating over all items and validating that no mapped litellm param resolves to a non-list type.
- Adds regression tests in test_router_order_fallback.py to prevent unhashable type crash from silently re-entering the codebase.

* chore(router): format code with black to pass CI

* fix(hosted_vllm): remove thinking_blocks and convert list content to strings (#30475)

* fix: hosted_vllm remove thinking_blocks and convert list content to strings

vLLM endpoints reject assistant messages with thinking_blocks converted
to content list blocks. This change removes thinking_blocks entirely
and converts any list content back to strings.

This fixes BadRequestError when using Claude Code with hosted_vllm
models that pass thinking_blocks in messages.

* fix(hosted_vllm): address Greptile review feedback

- Join multiple text blocks with newline instead of empty string
- Always set content to string (never None) to avoid vLLM validation errors

* fix(hosted_vllm): update chat transformation to clean assistant messages

* fix: re-raise exception instead of silently dropping MCP team permissions (#30477)

* fix: re-raise exception instead of silently
  dropping MCP team permissions

  When MCPRequestHandler.get_allowed_mcp_servers raises, the
  broad
  except was swallowing the error and returning only
  allow_all_server_ids,
  silently discarding all team-level object_permission grants.

  Fixes #30476

* fix: log full traceback when MCP permission lookup fails

Uses verbose_logger.exception() instead of warning() so operators
can see the full traceback when team-level object_permission grants
are dropped due to an internal error in get_allowed_mcp_servers.

Fixes #30476

* fix: remove timezone date expansion in daily-activity aggregation (#29569)

* fix: remove timezone date expansion in daily-activity aggregation

Single-day spend queries from non-UTC timezones over-counted by ~2x
because the previous implementation widened the SQL date range by a
full UTC day on whichever side the offset pointed. Spend is bucketed
in whole-UTC-day rows in LiteLLM_DailyUserSpend, so the expansion
pulled an extra 24h of unrelated bucket data per boundary.

Concretely on IST (UTC+5:30, offset -330): a single-day query for
2026-05-29 was rewritten to date >= 2026-05-28 AND date <= 2026-05-29
and returned spend across both UTC days. Sums of single-day queries
across a 5-day window then exceeded the equivalent multi-day aggregate
by ~50%, which is mathematically impossible.

Treat the local date range as the UTC date range. The aggregation
table has no hour-level granularity, so any conversion using only
date arithmetic must round to whole UTC days; the previous fix turned
that boundary slop into systematic over-counting. Pass-through trades
a small one-time slop at each end of the range for correct, monotonic,
additive results across single-day and multi-day queries.

Repro from production: bedrock/global.anthropic.claude-opus-4-8 over
2026-05-29 to 2026-06-02, IST timezone:
- 5-day aggregate: $701.39 / 1,831 reqs
- Sum of 5 single-day queries: $1,070.94 / 2,755 reqs
- Excess (was 1.527x): now matches within boundary slop

Adds regression tests in TestAdjustDatesForTimezone and
TestBuildAggregatedSqlQuery that pin the pass-through behavior and
the additivity invariant for any future implementation.

* ci: rerun checks on litellm_oss_branch base

---------

Co-authored-by: Sameer Kankute <sameer@berri.ai>

* fix: buffer native gemini sse frames (#30225)

* fix: buffer native gemini sse frames

* fix: scope native gemini sse buffering

* fix: check raw sse residual buffer size

* feat: updated openrouter provider to map max level to xhigh (#28881)

* feat(proxy): allow use_redis_transaction_buffer without redis cache (#28764)

* feat(proxy): allow use_redis_transaction_buffer without redis cache

* fix(proxy): require host or url for standalone buffer redis

* fix(mcp): fail closed when scope filter resolves to no servers (#30353)

`_get_allowed_mcp_servers_from_mcp_server_names` returned the caller's full
allowed-server set when the requested `mcp_servers` list (path- or
header-derived) resolved to nothing. URL/header namespacing therefore
appeared to work even when the requested name was unknown or the caller had
no grant — `/mcp/<typo>/` silently exposed every server the key could reach.

Fail closed instead: when `mcp_servers` is explicitly provided but nothing
resolves, return an empty list. The `mcp_servers=None` path (no scope
requested) keeps its existing behavior.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* fix(token-counter): handle Anthropic tool_reference blocks to stop dropped spend logs (#30302)

* fix(token-counter): handle Anthropic tool_reference blocks to stop dropped spend logs

`token_counter` did not know about Anthropic tool-search `tool_reference`
content blocks, a lightweight pointer to a deferred tool that shows up as
`{"type": "tool_reference", "tool_name": ...}`. When such a block appeared in
message content, `_count_content_list` fell through to its catch-all branch and
raised `Invalid content item type: tool_reference`.

On the streaming `anthropic_messages` proxy path that exception nulls
`response_cost`, which makes the proxy drop the entire SpendLogs row. The result
is a silent cost undercount on any tool-search traffic; the request succeeds for
the caller but the spend is never recorded.

This adds a `tool_reference` branch that counts the referenced `tool_name` (the
full tool definition is already counted via the `tools` param, so only the name
is added here) and handles an empty/missing name gracefully. The catch-all error
message is updated to list `tool_reference` among the expected types.

A regression test asserts that a message containing a `tool_reference` block no
longer raises and returns a positive token count, and that an empty `tool_name`
is handled without error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(token-counter): collapse explicit None tool_name to empty string

In _count_content_list, c.get("tool_name", "") returns None when the
key is present with an explicit None value, and str(None) == "None"
which is truthy, causing a spurious token to be counted. Use
c.get("tool_name") or "" so both a missing key and an explicit None
collapse to an empty string and are skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(token-counter): cover catch-all for unknown content block type

Adds a regression test that calls `_count_content_list` with an unrecognized
content block type and asserts it raises `ValueError` whose message names the
offending type and lists `tool_reference` among the supported types. This
exercises the previously uncovered catch-all branch (codecov patch gap) and
pins the error contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(token-counter): cover tool_reference on the spend/cost and streaming paths

Adds end-to-end regression tests that exercise the real public entry points
(`completion_cost` and `stream_chunk_builder`), not just the private
`_count_content_list` helper, for Anthropic tool-search `tool_reference`
content blocks.

These pin the actual bug the fix addresses: before the fix the `tool_reference`
block raised out of `completion_cost` -> the proxy logging layer nulled
`response_cost` and the spend callback dropped the SpendLogs row (silent cost
undercount on all tool-search traffic); and `stream_chunk_builder` swallowed the
same raise and collapsed prompt_tokens to 0. With the fix, cost is positive and
prompt_tokens are counted. Verified: 3 fail without the fix, 3 pass with it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cost): add cost mapping for deepseek-v4-flash and deepseek-v4-pro (#27056)

* feat(cost): add cost mapping for deepseek-v4-flash and deepseek-v4-pro

Adds pricing entries for the two new DeepSeek V4 models released on
2026-04-24, for both bare model names and the deepseek/ provider prefix.

Prices sourced from https://api-docs.deepseek.com/quick_start/pricing:
- deepseek-v4-flash: $0.14/M input, $0.28/M output
- deepseek-v4-pro:   $1.74/M input, $3.48/M output

Cache hit price set to 1/10 of input (per DeepSeek docs).
Context window: 1M tokens for both models.

Closes #26709

* fix(cost): update backup registry for deepseek-v4

* style: remove print statement from deepseek-v4 test

* feat(cost): add cost mapping for deepseek-v4-flash and deepseek-v4-pro

Adds pricing entries for the two new DeepSeek V4 models released on
2026-04-24, for both bare model names and the deepseek/ provider prefix.

Prices sourced from https://api-docs.deepseek.com/quick_start/pricing:
- deepseek-v4-flash: $0.14/M input, $0.28/M output
- deepseek-v4-pro:   $1.74/M input, $3.48/M output

Cache hit price set to 1/10 of input (per DeepSeek docs).
Context window: 1M tokens for both models.

Closes #26709

* fix: update deepseek-v4 prices to active discounted rates

* test: update deepseek-v4 prices in tests to match active discounted rates

* fix(deepseek): remove duplicate entries and update backup registry to active discounted rates

* fix: update max_output_tokens to 384K for deepseek-v4

* fix: correctly restore upstream models accidentally dropped during merge

* fix(tests): resolve failing claude-fable-5 and reasoning tests by safely updating cost map

- Pulled the latest cost map from upstream staging
- Safely appended deepseek-v4 mapping without deleting duplicate keys or formatting via json.dump

* fix(tests): correct deepseek model cache prices and update JSON schema

- Appended both prefixed and bare deepseek-v4 models to satisfy test assertions
- Corrected deepseek-v4-pro expected cache hit and token prices based on latest review updates
- Added missing realtime endpoint to test_utils.py INTENDED_SCHEMA

* fix: remove accidental azure/gpt-realtime-whisper addition

---------

Co-authored-by: Dushyant Acharya <dushyantacharya@Dushyants-MacBook-Pro.local>

* feat(key/info): expose per-model budget usage in /key/info response (#30394)

* feat(key/info): expose per-model budget usage in /key/info response

Add model_max_budget_usage to /key/info and /v2/key/info responses.
For each model in model_max_budget, reads current-period spend from
the same DualCache used by the budget enforcer and returns it alongside
the limit and time period so callers can see how much of each model
budget has been consumed in the active window.

* test(key/info): add coverage for model_max_budget_usage in v1 and v2 endpoints

Add tests for the model_max_budget_usage enrichment in both info_key_fn
and info_key_fn_v2, covering the budget-present path, the empty-budget
path, and the v2 batch endpoint.

* fix(key/info): source model_max_budget current_spend from SpendLogs instead of DualCache

The DualCache used for enforcement is ephemeral and only populated when budget metadata
is present at request time. Fall back to a direct LiteLLM_SpendLogs DB aggregation
using the budget period window (budget_reset_at - budget_duration) for accurate reporting.
Also fall back to litellm_budget_table.model_max_budget when the key's top-level field
is empty, and round current_spend to 4 decimal places.

* test(key/info): cover remaining branches in model_max_budget_usage helpers

Add unit tests for: prisma_client=None early return, DB query exception swallowing,
invalid budget_duration handled by _compute_budget_period_start, budget_reset_at
received as a datetime object (Prisma native type), max_seconds=0 early return, and
skipping models that lack a budget_duration. Also remove an unreachable except branch
where fromisoformat would fail after _compute_budget_period_start already validated the
same value.

* test(key/info): cover except path for unparseable per-model budget_duration

* fix(key/info): compute per-model rolling windows in model_max_budget_usage

Each model in model_max_budget now gets its own time window derived from
its own budget_duration, rather than sharing a single window computed as
the max (or the budget table's reset_at). This matches what the DualCache
enforcer actually tracks and prevents current_spend from being inflated
for models with shorter windows.

_query_model_spend_for_period is refactored to accept a model filter
(handling provider-prefix variants in SQL) and return a float directly.
_compute_budget_period_start and the budget_table window path are removed
as they are no longer needed.

* refactor(model_max_budget_limiter): remove dead get_current_period_spend method

* refactor(key/info): strip synthetic formatter noise from PR diff

Restore key_management_endpoints.py and test_key_management_endpoints.py
to origin/litellm_internal_staging, then re-apply only the intentional
additions: _query_model_spend_for_period, _build_model_max_budget_usage,
the two endpoint patches (info_key_fn / info_key_fn_v2), and the new
test suite. The previous commits had reformatted ~300 pre-existing lines
across both files, making the functional diff unreadable.

* test(key/info): cover empty-rows path in _query_model_spend_for_period

* fix(model_max_budget_limiter): guard BudgetConfig construction inside try/except

A malformed model entry in the DB (e.g. non-numeric max_budget from a
manually edited or migrated row) caused BudgetConfig(**budget_info) to
raise a Pydantic ValidationError outside any exception guard, surfacing
as a 500 for the entire /key/info or /v2/key/info call. Merging both
try/except blocks into one ensures bad entries are silently skipped,
consistent with the existing duration_in_seconds guard.

* fix: don't stack provider prefix on wildcard models with a custom prefix (#30360)

* fix: don't stack provider prefix on wildcard models with a custom prefix

get_known_models_from_wildcard expanded provider-prefixed model ids (e.g.
"ollama/gemma3:1b" from get_provider_models) by prepending the wildcard's
prefix whenever the id did not already start with it. With a custom wildcard
prefix such as "ollama_server1/*" (used to distinguish multiple Ollama
instances), this produced "ollama_server1/ollama/gemma3:1b", which is
uncallable and breaks /v1/models.

When the expanded id already carries a provider prefix, replace it with the
wildcard's prefix instead of stacking both. Matching-prefix and bare-model
cases are unchanged.

Fixes #30358

* fix: only strip a known provider prefix when expanding custom wildcard prefixes

The wildcard expansion replaced the leading slash segment of every expanded id with the wildcard prefix whenever the id did not already start with it. For ids whose first segment is an org rather than a litellm provider (for example a provider returning "meta-llama/Llama-3-8B" with no outer provider prefix), that dropped the org and produced an uncallable id

Only strip the leading segment when it is a recognized provider (membership in LlmProviders); otherwise keep it and just prepend the wildcard prefix. Provider-prefixed ids like "ollama/gemma3:1b" still have their prefix replaced, so the original fix is unchanged for known providers

* address greptile review feedback: log dropped non-text vLLM assistant content blocks (greploop iteration 1)

* fix(ci): format credential_form_helpers test + regenerate dashboard schema.d.ts

* fix(proxy): raise litellm.BadRequestError for missing model param

When no model is passed, route_request now raises a litellm.BadRequestError
('Missing model parameter') instead of falling through to ProxyModelNotFoundError.
This keeps the missing-param error clear and independent of router wildcard
state. Unknown (non-empty) model names still raise ProxyModelNotFoundError.

* Revert "fix(proxy): raise litellm.BadRequestError for missing model param"

This reverts commit 9240da403c.

* Revert "fix(router): clean pattern_router state on upsert/delete (#29601)"

This reverts commit ad4e6e2395.

* fix: correct streaming and key budget usage reporting

* fix(hosted_vllm): type assistant tool_calls to satisfy mypy

* feat: aws secret manager cross region replication (#30368)

* feat(aws-secret-manager): add replica_regions cross-region replication after CreateSecret

When store_virtual_keys is enabled, async_write_secret() only wrote secrets
to the primary AWS region. Multi-region proxy deployments had no built-in
way to synchronize virtual key secrets across regions through LiteLLM,
requiring external replication mechanisms.

Add replica_regions support to AWSSecretsManagerV2:
- New replica_regions field in KeyManagementSettings (types/secret_managers/main.py)
- New async_replicate_secret() method that calls ReplicateSecretToRegions API
- async_write_secret() calls replication after successful CreateSecret
- Replication failure is logged as a warning but does NOT fail key creation
- load_aws_secret_manager() forwards replica_regions from key_management_settings

Configuration example:
  key_management_settings:
    store_virtual_keys: true
    replica_regions:
      - us-west-2
      - eu-west-1

When replica_regions is omitted or empty, behavior is unchanged.

* test(aws-secret-manager): restore litellm.secret_manager_client after test to prevent state pollution

* test(aws-secret-manager): add coverage for HTTP error and replication exception paths

* fix: restore litellm.secret_manager_client global state in test; add replication log proof

- Global state in test_load_aws_secret_manager_passes_replica_regions was
  already guarded with try/finally (committed in previous pass); no further
  change needed for Fix 1.
- Fix 2: add verbose_logger.info("ReplicateSecretToRegions called …") inside
  async_replicate_secret so callers get an observable INFO log line whenever
  replication fires.
- Add test_replication_fires_on_create: calls async_replicate_secret directly
  with caplog.at_level(INFO, logger="LiteLLM") and asserts "ReplicateSecretToRegions"
  appears in the captured log output, proving the code path executes.

* fix: pass request to streaming generators

* fix(hosted-vllm): preserve assistant structured content

* fix(hosted_vllm): satisfy mypy on preserved structured content assignment

* chore: resolve litellm_internal_staging merge conflicts for #30527 (#30554)

* chore(codecov): add Batches, Videos, and Realtime components (#30517)

* chore(codecov): add Batches, Videos, and Realtime components

Define per-feature Codecov components so PR comments track coverage
for batch API, video generation, and realtime streaming paths.

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

* chore(codecov): use wildcard path for Batches proxy component

Align batches_endpoints glob with Videos, Realtime, and Proxy_Authentication.

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

---------

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

* test(batches): move orphan tests into tests/test_litellm for CI coverage (#30510)

Four batch-related tests lived under tests/litellm/ and were never picked
up by GitHub Actions. Relocate them and fix gemini multimodal e2e to use
the batchEmbedContents path expected for gemini/ provider.

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

* fix(guardrails): run pre_call hook once for model-level guardrails (#30543)

* fix(guardrails): run pre_call hook once for model-level guardrails

A CustomGuardrail attached to a deployment via litellm_params.guardrails
gets its async_pre_call_hook invoked twice per request: once by the proxy
pre-call loop and again by async_pre_call_deployment_hook after the router
spreads the model-level guardrails into the top-level request kwargs.

Record in request metadata that the proxy pre-call loop already ran a given
guardrail, and have the deployment hook skip it when the marker is present.
Direct-SDK usage never runs the proxy loop, so the deployment hook stays the
sole invocation there and still fires exactly once.

The marker key is stripped from untrusted caller metadata so a request body
cannot suppress a model-only guardrail by pre-seeding it.

* fix(guardrails): mark pre_call dedup on the post-hook request data

Record the exactly-once marker after async_pre_call_hook runs, on the data
object that flows downstream, rather than before it. A guardrail whose hook
returns a brand-new request dict (instead of mutating or spreading the one it
received) would otherwise discard the marker, letting the deployment hook
re-run the guardrail a second time.

* fix(guardrails): stop re-initializing DB guardrails on every poll (#30542)

* fix(guardrails): stop re-initializing DB guardrails on every poll

InMemoryGuardrailHandler._has_guardrail_params_changed compared the
in-memory LitellmParams against the raw dict loaded from the DB. The
in-memory side carries every field default and coerces enums via
model_dump(), while the DB side only holds the keys originally stored,
so the two shapes never compared equal and the guardrail was rebuilt on
every poll cycle.

Each rebuild created a fresh instance, but delete_in_memory_guardrail
only removed the old callback from litellm.callbacks. Request handling
promotes guardrail callbacks into the success/failure/async lists, so
the previous instance stayed referenced there and instances accumulated.

Normalize both sides through LitellmParams(...).model_dump() before
diffing, and purge the callback from every callback list on delete.

* refactor(guardrails): narrow params-normalization fallback to ValidationError

The comparison normalizer caught a bare Exception and silently fell back
to the raw dict, which hid the cause and quietly degraded the affected
guardrail back to re-initializing on every poll. Catch only the
ValidationError that LitellmParams construction can raise, log a warning
so the offending row is diagnosable, and let any other error surface
instead of being swallowed.

* refactor(callbacks): add remove_callback_from_all_lists helper to manager

Move the knowledge of which callback lists a callback can be promoted
into out of the guardrail registry and into LoggingCallbackManager, where
the rest of the callback-list bookkeeping already lives. delete_in_memory_guardrail
now delegates to the new helper instead of iterating the lists itself.

* chore(oss): litellm oss staging 150626 (#30463)

* fix(pricing): add GitHub Copilot MAI Code Flash pricing (#30415)

* fix(pricing): add GitHub Copilot MAI Code Flash pricing

Add GitHub Copilot pricing entries for MAI-Code-1-Flash and the internal Copilot CLI model name so cost calculation can price input, cached input, and output tokens.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(pricing): cover GitHub Copilot MAI Code Flash pricing

Add regression coverage for both GitHub Copilot MAI-Code-1-Flash model names, including cached input pricing, chat endpoint metadata, and cost_per_token arithmetic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(router/proxy): propagate completed_response through FallbackResponsesStreamWrapper for streaming /v1/responses container ownership (#30210) (#30213)

* fix(router/proxy): propagate completed_response through FallbackResponsesStreamWrapper for streaming /v1/responses container ownership (#30210)

#28990 added ownership recording for streaming /v1/responses via
_wrap_responses_stream_for_container_ownership, which reads
`getattr(stream_response, 'completed_response', None)` to extract the
ResponsesAPIResponse. The unit test bypassed the Router, so it never
exercised the production wrapping path.

Through the Router (every proxy deployment), the stream is wrapped by
FallbackResponsesStreamWrapper (router.py:2527). Its __init__ set
`self.completed_response = None` and __anext__ only forwarded chunks
— the inner source iterator's terminal event never bubbled up to the
attribute the ownership hook reads, so the hook silently recorded
nothing and every follow-up /v1/containers/<id>/files call returned
403 for non-admin keys.

This commit:

- router.py: pre-resolves the responses-API terminal event tuple
  (response.completed / .incomplete / .failed) once per
  _aresponses_streaming_iterator call, and has the wrapper's __anext__
  sniff each forwarded chunk's .type. First terminal event hit gets
  stored on the wrapper's completed_response. Iterator-agnostic — works
  for source_iterator AND any future wrapper.

- common_request_processing.py: when _extract_completed_responses_response
  returns None we now warn instead of silently skipping. Reporter on
  #30210 lost a day to this exact silent skip; the warning surfaces
  future regressions of the same shape directly in operator logs.

Fixes #30210

* fix(router): type-ignore wrapper getattr-defaults; broaden ownership-skip warning

CI lint (mypy) flagged the three pre-existing getattr(..., None) assignments
in FallbackResponsesStreamWrapper.__init__:

  router.py:2564 self.response = getattr(source_iterator, 'response', None)
  router.py:2565 self.model    = getattr(source_iterator, 'model', None)
  router.py:2566 self.logging_obj = getattr(..., None)

Those lines also exist on litellm_internal_staging and pass mypy there.
Adding the typed terminal-event tuple above the class made the function
body more narrowable, which surfaced the pre-existing mismatch — base
class declares non-Optional types but the bridge path
(LiteLLMCompletionStreamingIterator) legitimately omits these. Keep
the None fallback and silence with type: ignore[assignment].

Greptile 4/5 note: the ownership-skip warning hard-named code_interpreter
which misleads operators when a non-code_interpreter stream aborts.
Generalize to 'any tool container (e.g. code_interpreter)'.

* fix(register_model): drop synthesized zero costs to preserve sparse entries (#30198) (#30201)

* fix(register_model): drop synthesized zero costs to preserve sparse entries (#30198)

get_model_info synthesizes input_cost_per_token / output_cost_per_token = 0
when they are absent from the raw entry (the price-unknown and free cases
share the same representation). register_model then merges that result back
into litellm.model_cost, which flips a sparse entry from 'no cost keys'
(priced via model name) to 'cost keys = 0' (free).

That defeats _is_cost_explicitly_configured (#24949) on re-registration:
_is_model_cost_zero returns True, common_checks skips every tag / key /
team / user / org budget check for the group, and over-budget traffic
keeps returning 200. Spend keeps recording because cost calc still resolves
by model name, so the symptom is silent and only triggers on the second
register_model pass (router rebuild, /model/update, config sync).

Mirror the existing litellm_provider-None guard one block above and pop
the cost fields from the synthesized result when they are absent from the
raw entry and not in the caller's value. Caller-provided zeros (genuinely
free models, BYOK overrides) are preserved.

Fixes #30198

* fix(register_model): switch _raw_entry to is-None checks + drop dead test assertion

Greptile #30201 review notes:
- the `or`-chain in the raw-entry lookup treated an empty dict (a key
  with no fields) as falsy and fell through to the second arm — replace
  with explicit `is None` checks so a present-but-empty entry is still
  taken at face value.
- the first assertion in `test_router_double_init_keeps_db_model_entry_sparse`
  used `in (None, 0)` which passes under the bug condition (cost = 0
  matches the tuple); the strong follow-up assertion already covers
  every shape, so drop the dead branch.

* fix(bedrock mantle): use unique function-call id for responses->chat tool calls (#30426)

* fix(bedrock mantle): use unique function-call id for responses->chat tool calls

...

* fix(bedrock mantle): scope unique tool-call id fallback to degenerate call_id

The previous revision preferred the Responses item id for every tool call, which broke providers (and existing tests) where call_id is a unique, canonical correlation key. Restrict the fallback to the degenerate index-based call_id that Bedrock Mantle returns (call_0, call_1, ... resetting per response) and keep call_id otherwise. Revert the change to the OUTPUT_ITEM_DONE streaming handler, whose tool_call_chunk is never emitted (dead code, per review). Extend the regression tests to assert a normal call_id is preserved.

* fix(router): preserve azure_ad_token through CredentialLiteLLMParams for /v1/files + batches (#30235) (#30241)

* fix(router): preserve azure_ad_token through CredentialLiteLLMParams for /v1/files + batches (#30235)

Router.get_deployment_credentials_with_provider re-validates a
deployment's litellm_params through CredentialLiteLLMParams before
handing them to file/batch/passthrough callers:

    return CredentialLiteLLMParams(
        **deployment.litellm_params.model_dump(exclude_none=True)
    ).model_dump(exclude_none=True)

Any field NOT declared on CredentialLiteLLMParams gets silently dropped
on the way through. azure_ad_token was undeclared, so Azure deployments
using OAuth/M2M (azure_ad_token instead of a static api_key) silently
lost their token at the files endpoint and the proxy returned:

    Missing credentials. Please pass one of api_key, azure_ad_token,
    azure_ad_token_provider, ...

Declare azure_ad_token on CredentialLiteLLMParams alongside api_key /
api_base / api_version so it rides through the round-trip. Static-key
deployments stay unaffected (Optional, default None, dropped by
exclude_none=True). Provider-callable (azure_ad_token_provider) is a
separate concern and out of scope here.

Fixes #30235

* fix(ui-types): regenerate schema.d.ts for new azure_ad_token field

CI's 'Verify schema.d.ts matches the proxy OpenAPI spec' check
auto-detected the new field and emitted the exact diff to apply.
Two schemas had `aws_secret_access_key` from CredentialLiteLLMParams,
both get the new azure_ad_token marker next to it.

* fix(proxy): org_admin with own user_id now sees all org teams on /v2/team/list (#30247)

When the UI sends the callers own user_id (as it does for non-Admin
global roles), _enforce_list_team_v2_access now nulls it out for org
admins so _build_team_list_where_conditions scopes by organization_id
only -- matching the legacy /team/list behavior and the documented intent.

Fixes #30215

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* test(vertex_ai): multi-region regression coverage for cachedContents host (#29571) (#29707)

litellm_internal_staging already routes the cachedContents URL through
get_vertex_base_url, fixing the multi-region 404 reported in #29571 —
but carries no test coverage for the actual regression scenario (eu/us
must resolve to the REP host aiplatform.{geo}.rep.googleapis.com).

Add TestContextCachingMultiRegionUrls: parametrized eu/us REP-host
assertions (including absence of the old broken {geo}-aiplatform host),
plus regional (us-central1) and global no-regression checks.

* fix(proxy): close upstream LLM stream when client disconnects mid-stream (#30245)

* fix(proxy): close upstream LLM stream when client disconnects mid-stream

When a streaming client disconnects, Starlette abandons the response
body iterator without calling aclose(), so the proxy's connection to
the upstream backend stays open until garbage collection, which may
never come. The backend (e.g. vLLM) keeps generating into a dead pipe:
small responses drain invisibly into TCP buffers while large ones block
the backend on a full send buffer indefinitely (observed via lsof as an
ESTABLISHED proxy->backend connection minutes after the client left)

create_response now returns a StreamingResponse subclass that closes
both its body iterator and the wrapped upstream-facing generator in a
shielded finally. The upstream generator is closed directly rather than
through a cascade because aclose() on a never-started generator skips
its body, which would make the cascade a no-op when the client
disconnects before the first chunk is sent.
async_streaming_data_generator also gains the same shielded
finally-aclose that async_data_generator in proxy_server.py already
had, covering the Anthropic and Google SSE paths

With this, killing a streaming client causes the backend to observe the
abort within about a second and free its slot, while completed streams
are unaffected. No flag is needed, unlike the non-streaming opt-in
cancel in #30223: this only releases resources after the client is
already gone and does not change any response a client can observe

Fixes #30244

* fix(proxy): close upstream even when body iterator aclose raises BaseException

Addresses the Greptile finding on #30245: the cleanup loop caught only
Exception while the generator-level cleanup catches BaseException, so a
CancelledError or GeneratorExit escaping body_iterator.aclose() would
skip closing the upstream generator. Both sites now use the same scope
and a regression test pins that the upstream is closed even when the
body iterator explodes with a BaseException

* fix(llms): expose aclose on BaseModelResponseIterator so stream close reaches the provider connection

The response-level close added for #30244 only worked for SDK-based
providers (e.g. openai), whose streams expose aclose all the way down.
Providers served by base_llm_http_handler (hosted_vllm and most modern
transformation-based providers) wrap a bare response.aiter_lines()
generator in BaseModelResponseIterator, which had no aclose or close at
all, and nothing retained the httpx response object; so
CustomStreamWrapper.aclose() silently did nothing and the upstream
connection stayed open. Verified with a vLLM-style mock: with
hosted_vllm/ the backend streamed all 100 chunks to completion after
the client disconnected, while openai/ aborted at chunk 6

BaseModelResponseIterator now carries an optional http_response and an
aclose() that closes it; make_async_call_stream_helper attaches the
response after building the iterator. With this, hosted_vllm aborts the
backend within ~1.6s of the client dropping, and completed streams are
unaffected

---------

Co-authored-by: kursad <kursad.lacin@brado.net>

* feat(anthropic): surface compaction usage iterations data (#27065)

* feat(anthropic): surface compaction usage iterations data

* style: apply black formatting to fix lint checks

* fix(usage): correct calculate usage with cached tokens when use ChatCompletionUsageBlock (#30422)

* fix(usage): correct calculate usage with cached tokens when use ChatCompletionUsageBlock

* fix(usage): optimize test imports

* feat: add fastCRW search provider (#30434)

* feat(provider): add LibertAI as a JSON-configured OpenAI-compatible provider (#30203)

* feat(provider): add LibertAI as a JSON-configured OpenAI-compatible provider

* libertai: update served endpoints backup + add mode/matrix tests

Addresses review feedback:
- Add libertai to litellm/provider_endpoints_support_backup.json, the file
  actually served by GET /public/supported_endpoints (the root
  provider_endpoints_support.json already had it).
- Add tests asserting bge-m3 normalizes to mode='embedding' and that the
  served matrix lists libertai. embeddings stays false: the JSON-configured
  provider path only wires chat routing (OpenAILike embedding handler is
  reached only for literal openai_like/llamafile/lm_studio), matching the
  llamagate precedent; bge-m3 remains in the cost map for metadata.

---------

Co-authored-by: Moshe Malawach <moshemalawach@users.noreply.github.com>

* feat(provider): add ModelScope as an OpenAI-compatible provider (#28460)

* add ModelScope API support

* add modelscope api support

* update modelscope model list

* add image-genetation support

* update test and multimodal

* fix: address PR review feedback for modelscope provider

* update README

* fix(customer_endpoints): restrict /customer/daily/activity to admin-only (#28849)

* fix(customer_endpoints): restrict /customer/daily/activity to admin-only

* fix(customer_endpoints): check role before prisma_client guard

* fix(custom_guardrail): key disable_global_guardrails takes precedence over team guardrail list (#28563)

* fix(fallbacks): preserve fallback model in SDK fallback responses (#28260)

* fix(fallbacks): preserve fallback model in response when using SDK-level fallbacks

* fix(fallbacks): gate x-litellm-* passthrough to trusted callers only

The previous patch unconditionally let `x-litellm-*` keys bypass the
`llm_provider-` prefix in `process_response_headers`. That function is
also called on raw upstream-provider response headers (e.g. from
`llm_http_handler.py`), so a malicious provider could return
`x-litellm-attempted-fallbacks` and spoof a LiteLLM-internal marker,
bypassing the proxy model-override guard.

Add a `preserve_litellm_internal_headers` flag (default False). Only
`response_metadata.py`, which re-processes the already-built
`_hidden_params["additional_headers"]` dict (LiteLLM-owned), passes
True. Raw provider header callsites keep the default False, so upstream
`x-litellm-*` still gets the `llm_provider-` prefix.

Adds a regression test for the spoofing case and renames the existing
preserve test to make the trusted-path semantics explicit.

* fix(fallbacks): ignore preserve_litellm_internal_headers for raw httpx.Headers inputs

* style(core_helpers): apply black formatting

* fix(lint): remove banned typing.List/Dict/Any imports and suppress PLR0913 on interface overrides

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

* fix(lint): apply black formatting to modelscope chat transformation

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

* fix(lint): replace noqa with proper fixes — use **kwargs and Awaitable instead of Any/List

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

* fix(lint): remove unused AllMessageValues import

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

* revert: restore base_model_iterator.py to original PR state

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

* fix(lint): restore full method signatures for MyPy compatibility; bump PLR0913 budget for new provider files

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

* fix(lint): use @override to suppress PLR0913 on inherited signatures instead of bumping budget

The overrides keep their full base-class signatures for MyPy compatibility, but those signatures carry more than five parameters, which tripped PLR0913 on each subclass redeclaration. Since the arity is dictated by the base class and cannot be reduced, decorate the overrides with typing_extensions.override; ruff treats that as the intended signal that the parameter count is not under the author's control and skips PLR0913. This restores the PLR0913 baseline to 1813.

* fix(lint): add @override to modelscope image generation overrides

Apply the same typing_extensions.override treatment to the image generation config so its inherited-signature overrides do not count against PLR0913.

---------

Co-authored-by: Joel Tony <github@jaytau.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: hcl <chenglunhu@gmail.com>
Co-authored-by: ztko <96878659+koztkozt@users.noreply.github.com>
Co-authored-by: Nahrin <nahrin@nahrinoda.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Humphrey <a739376838@gmail.com>
Co-authored-by: kursadlacin <kursadlacin@gmail.com>
Co-authored-by: kursad <kursad.lacin@brado.net>
Co-authored-by: Dushyant Acharya <dushyantacharya873@gmail.com>
Co-authored-by: Yuriy <yuriy.shuyskiy@gmail.com>
Co-authored-by: Recep S <22618852+us@users.noreply.github.com>
Co-authored-by: Moshe Malawach <moshe.malawach@protonmail.com>
Co-authored-by: Moshe Malawach <moshemalawach@users.noreply.github.com>
Co-authored-by: Rongkun Yan <2493404415@qq.com>
Co-authored-by: Varshith <kvarshithgowda@gmail.com>
Co-authored-by: Mateo Wang <277851410+mateo-berri@users.noreply.github.com>

* ci(lint): add blanket-noqa, dataclass-default, and unused-noqa Ruff rules (#30516)

* ci(lint): enforce blanket-noqa, dataclass-default, and unused-noqa rules

Enable PGH004 (blanket-noqa), RUF008 (mutable-dataclass-default),
RUF009 (function-call-in-dataclass-default-argument), and RUF100
(unused-noqa) in ruff.toml, and clean up every resulting violation.

RUF008/RUF009 were already clean. PGH004/RUF100 surfaced ~335 stale or
blanket noqas: blanket `# noqa` are now scoped to the rule they actually
suppress (mostly T201), dead directives are removed, and inapplicable
codes are trimmed (e.g. F401 dropped from `import *`).

lint.external lists rules enforced outside this config (the strict-rule
gate via ruff-strict.toml and upstream litellm's own ruff config) so
RUF100 keeps the noqa directives that protect them instead of stripping
coverage this config can't see.

* ci(lint): trim RUF100 external list to load-bearing codes only

Drop the 9 precautionary strict-gate codes (ANN001/002/003/401, B006,
PLR0913, PLW0603, RUF012, TID251) that have zero `# noqa` references in
the gated source. Keep only the 11 codes with live suppressions so
RUF100 doesn't flag them as unused. Future strict-gate suppressions can
re-add codes here (or fix the underlying issue) as needed.

* ci: ratchet lint and type-check gates (ruff preview, ANN, mypy, basedpyright) (#30379)

* ci: enable ruff preview rules under the budgeted strict gate

Turn on ruff preview in the strict-budget lane (ruff-strict.toml) only,
leaving the clean gate (ruff.toml) untouched so make lint-ruff stays at
zero. Enumerate the 118 firing codes explicitly with
explicit-preview-rules so the gate is deterministic and stable across
ruff upgrades rather than depending on preview auto-selecting the broad
catalog.

Grandfather the existing 58438 violations into ruff-strict-budget.json
as per-rule baselines with headroom, so only net-new violations fail CI.
The existing ten rules keep their hand-tuned slack; the new rules get
slack 10 when the baseline is 50 or more and 3 otherwise.

* ci: add ANN return-type rules to the budgeted strict gate

Add ANN201/202/204/205/206 (missing return annotations) to the strict
lane and grandfather the existing counts into ruff-strict-budget.json so
the codebase ratchets toward explicit return types without breaking CI.

* ci: add mypy (disallow_untyped_defs) and basedpyright strict gates with baselines

Add two type-check gates, each grandfathering the current tree so only
net-new violations fail CI, matching the ruff strict-budget ratchet.

mypy gains disallow_untyped_defs in litellm/mypy.ini (the config the CI
invocation actually reads; the root [tool.mypy] is not picked up from the
litellm/ working dir). The 4885 existing missing-annotation errors are
captured in litellm/.mypy-baseline.txt and the run is piped through
mypy-baseline filter so new untyped defs are rejected.

basedpyright runs in strict mode over litellm/, with
enableTypeIgnoreComments disabled so it only honors '# pyright: ignore'
and never polices mypy's '# type: ignore'. The existing strict diagnostics
are grandfathered into .basedpyright/baseline.json.

Both tools are pinned in the dev group and uv.lock; the lint workflow and
Makefile run them filtered through their baselines, with
lint-mypy-baseline-update and lint-basedpyright-baseline-update to ratchet.

* ci: raise lint job timeout to 15m for the basedpyright strict pass

* ci: pin pythonVersion 3.12 and regenerate baselines against merged base

Merge litellm_internal_staging so the baselines cover code the CI merge
includes (e.g. the cisco_ai_defense guardrail), which otherwise tripped
the mypy gate with 3 ungrandfathered no-untyped-def errors. Pin
pythonVersion 3.12 in pyrightconfig so basedpyright's strict analysis is
reproducible across interpreter versions (CI runs 3.12).

* ci: regenerate basedpyright baseline against the frozen lint env

The previous baseline was generated with optional provider deps (azure,
google, anthropic, mcp, numpydoc, google-genai) installed locally, so CI's
dev-only env surfaced ~3500 reportUnknown*/reportMissingTypeStubs errors
not in the baseline. Regenerate after uv sync --frozen so the baseline
reflects the same dependency set the lint job sees.

* ci: regenerate basedpyright baseline on python 3.12 frozen env

The prior baseline still carried proxy-dev packages (e.g. prisma) that the
lint job's dev-only, python 3.12 env lacks, leaving 2 unresolved-import
errors ungrandfathered. Regenerate in a python 3.12 venv synced to the
frozen lock with default groups only, so the baseline matches exactly what
CI sees.

* ci: replace type-check baselines with per-file count budgets

The mypy and basedpyright baselines were position-sensitive (and the
basedpyright one was a 27MB file), so ordinary line shifts churned them.
Replace both with a per-file count gate: scripts/type_check_gate.py reduces
each tool's output to errors-per-file and checks it against a committed
{file: max} budget, ignoring line and column numbers. A file fails only
when it gains more errors than its ceiling; debt can't be shuffled between
files because each file has its own cap and new files default to zero.

Budgets (mypy-file-budget.json 48K, basedpyright-file-budget.json 96K) are
generated in the python 3.12 frozen lint env so they match CI. Drops the
mypy-baseline dependency; basedpyright runs without its native baseline.
ratchet via make lint-mypy-budget-update / lint-basedpyright-budget-update.

* ci: add a small per-file slack to the type-check gate

Allow each file to drift PER_FILE_SLACK (5) errors past its recorded count
before failing, so a basedpyright inference ripple in an unrelated file
doesn't break the build over a couple of errors. Budgets still record exact
counts; the tolerance is applied at check time.

* ci: move type-check slack into the budget json and trim lint timeout

Make slack declarative: the budget is now {"slack": N, "files": {path: count}}
so the tolerance is tuned in JSON without editing the script, mirroring how
ruff-strict-budget.json carries its slack. --update preserves the existing
slack. Also drop the lint job timeout from 15m to 10m; the mypy and
basedpyright passes add ~2m, leaving the job around 4-5m, so 10m is a
comfortable margin.

* ci: collapse fully-adopted ruff categories and drop inert preview flag

ANN (all nine non-removed rules) and BLE (its only rule) were spelled out
code-by-code; replace each with its category selector, which is exactly
equivalent in 0.15.3 (the removed ANN101/ANN102 are skipped by a category
selector and error when named explicitly). explicit-preview-rules was inert:
every selected rule is stable and nothing is selected by category, so the flag
had nothing to gate. Verified the strict-rule counts are identical before and
after (62379 each, zero per-rule drift), so no budget change.

* ci: drop redundant pyright dev dependency

Nothing invokes bare pyright in the Makefile, the linting workflow, or
scripts; the basedpyright gate added on this branch is the only type
checker that runs. basedpyright is a superset fork that reads the same
pyrightconfig.json and honors the same "# pyright: ignore" comments, so
pyright==1.1.408 in the ci group was dead weight. Regenerated uv.lock
under the same exclude-newer cutoff so the only change is removing
pyright and its package stanza

* ci: un-weaken mypy and error on Any in basedpyright

mypy: enable warn_return_any, drop the valid-type silencer, and stop globally ignoring missing first-party imports via [mypy-litellm.*] ignore_missing_imports = False, which surfaced eight real broken litellm.* imports the blanket ignore was hiding; third-party imports stay ignored. The per-file budget moves 4888 -> 5799 (902 no-any-return, 1 valid-type, 8 import-not-found), all grandfathered so only net-new errors fail and the ceilings ratchet down

basedpyright: error on reportExplicitAny and reportAny. The per-file budget moves 117033 -> 148946 (6931 explicit-Any, 24954 Any-typed expressions), grandfathered the same way

* ci: add Any-discipline gate on changed lines under litellm/

Add scripts/check_any_discipline.py, a type-aware gate that fails when a
changed line holds a value typed Any -- including the X | Any unions that
mypy --strict / basedpyright accept (e.g. re.Match.group() -> str | Any,
json.loads() -> Any, bare dict -> dict[Any, Any]).

It reuses the repo's mypyc-compiled mypy 1.19 via a custom generic AST
walker (mypyc precludes subclassing TraverserVisitor), loads litellm/mypy.ini
for parity with lint-mypy, and uses a dedicated incremental cache
(.mypy_cache_any) with mtime+hash invalidation to force re-checks. Scope is
changed-lines-only so editing a legacy file never forces cleaning its
existing Any debt; suppress a genuine typed/untyped boundary with
# any-ok: <reason> (ANY002 requires the reason).

Wire it into the Makefile (lint-any, lint, lint-dev), a parallel
any-discipline CI job with its own actions/cache, .gitignore, and the
CLAUDE.md / CONTRIBUTING.md docs.

* ci: move Any-gate codes into the shared LIT namespace

Renumber the Any-discipline checker into the LIT*** scheme owned by
scripts/check_type_discipline.py (PR #30500) so the two checkers share one
rule namespace and suppression convention:

  ANY001 -> LIT002  (Any-typed value; LIT002 was the retired/free slot)
  ANY002 -> LIT005  (any-ok without a reason; the shared suppression-reason code)
  ANY000 -> LIT000  (setup/build/read error; the shared error code)

Messages and behavior are unchanged; LIT005's text already matches the
"<token> requires a reason" shape used for cast-ok/guard-ok.

* ci: gate mypy and basedpyright per error rule, not per file

Switch the mypy/basedpyright budget gate from per-file error counts to
per-rule-code totals, mirroring the {rule: {baseline, slack}} shape of
ruff-strict-budget.json. A rule fails when its codebase-wide error count
exceeds baseline + slack, so violations are tracked by category rather
than by file location.

scripts/type_check_gate.py now parses mypy from its text output (trailing
[code]) and basedpyright from --outputjson (the JSON `rule` field), since
basedpyright's wrapped text diagnostics mis-attribute the rule on
continuation lines. Replace the *-file-budget.json files with freshly
captured *-code-budget.json baselines and update the Makefile, CI, and
CLAUDE.md accordingly.

* docs: prefer Pydantic validation over any-ok suppression

Point the Any-discipline guidance at validating Any with Pydantic (a model
or TypeAdapter that returns a typed value or raises) and frame
# any-ok as a last resort that should ideally never be used.

* chore: remove extraneous comment

* chore: make the CLAUDE.md more concise

* chore: clean up bloated CONTRIBUTING.md additions

* chore: make Makefile more concise

* ci: add the lint-budget-update target CLAUDE.md references

CLAUDE.md tells contributors to run make lint-budget-update, but the
target was never defined. Add it as an aggregate that re-captures the
ruff, mypy, and basedpyright budgets in one shot.

* ci: recapture mypy and basedpyright budgets in the lint env

The per-rule baselines were captured in a richer dependency env than the
CI lint job's uv sync --frozen, so CI resolved fewer types and reported
more errors than the budgets allowed (no-any-return 902 over cap 900, plus
several basedpyright reportUnknown* rules). Regenerate both in the frozen
env so they grandfather the true CI debt: mypy 5786 -> 5799 (no-any-return
890 -> 902, valid-type 1 restored), basedpyright 146213 -> 148942.

* ci: check out PR head sha in lint and any-discipline jobs

The default pull_request checkout uses refs/pull/N/merge, which folds the
latest base commits into HEAD. The diff-based gates (ruff delta, Any
discipline) then diff against the event's older base.sha and blame base's
own new commits on this branch; staging's otel-v2 and streaming changes
(#30326, #30485) tripped the Any gate on files this branch never touched.
Checking out the PR head sha makes the gates diff the real branch tip
against base, and pins the tree the mypy/basedpyright budgets were captured
against so their counts stay deterministic as the base advances.

* ci(lint): renumber Any-typed-value rule LIT002 -> LIT009

Free up LIT002 for the sibling type-discipline gate (check_type_discipline.py,
#30500), which groups its mutable-collection family at LIT001 (annotation) and
LIT002 (construction). This gate's Any-typed-value rule moves to LIT009 so the
shared LIT namespace stays contiguous with no holes; LIT000 and LIT005 are
unchanged.

* style: rename lint-strict-budget -> lint-ruff-budget

* ci: harden type-check gates against silent passes (greptile review)

type_check_gate.py: refuse to certify a vacuous run. The CI pipe swallows
the tool's exit code ('tool || true'), so a crashed mypy/basedpyright that
emits nothing would parse to zero errors, breach no ceiling, and pass.
is_vacuous_run() now fails when nothing was parsed but the budget expects
errors. Also wrap basedpyright's json.loads in a JSONDecodeError handler
that prints the offending output instead of dumping a raw traceback.

check_any_discipline.py: ALL_LINES was None, which dict.get() also returns
for a path absent from the line map, so a path-normalisation mismatch could
let a violation on an unchanged file pass the scope filter. Make ALL_LINES a
distinct sentinel object so 'whole file' and 'path missing' are unambiguous.

Adds tests for all three.

---------

Co-authored-by: Sameer Kankute <sameer@berri.ai>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
Co-authored-by: Joel Tony <github@jaytau.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: hcl <chenglunhu@gmail.com>
Co-authored-by: ztko <96878659+koztkozt@users.noreply.github.com>
Co-authored-by: Nahrin <nahrin@nahrinoda.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Humphrey <a739376838@gmail.com>
Co-authored-by: kursadlacin <kursadlacin@gmail.com>
Co-authored-by: kursad <kursad.lacin@brado.net>
Co-authored-by: Dushyant Acharya <dushyantacharya873@gmail.com>
Co-authored-by: Yuriy <yuriy.shuyskiy@gmail.com>
Co-authored-by: Recep S <22618852+us@users.noreply.github.com>
Co-authored-by: Moshe Malawach <moshe.malawach@protonmail.com>
Co-authored-by: Moshe Malawach <moshemalawach@users.noreply.github.com>
Co-authored-by: Rongkun Yan <2493404415@qq.com>
Co-authored-by: Varshith <kvarshithgowda@gmail.com>

* chore: satisfy strict-rule and any-discipline gates for the staging bundle

The strict-rule budget and any-discipline gates added in #30379 flag the
bundle's new lines: blind-except (BLE001), legacy typing imports
(UP006/UP035/UP045), and values typed Any on changed lines (LIT009).

Type-fix the cleanly-fixable cases (function signatures, payload dicts as
dict[str, object], BudgetConfig.model_validate over **kwargs, direct
KeyManagementSettings attribute access over getattr, Optional[X] -> X | None)
and suppress the irreducible untyped boundaries (request/streaming dicts,
cache reads, httpx responses, asyncio primitives, Pydantic model_dump
navigation) with # any-ok and a short reason.

Also fix two any-discipline gate false positives so legitimate code is no
longer flagged: the synthetic Any in Coroutine/Generator send and yield
protocol slots (the awaited/returned value is still checked), and the
special-form Any of a TypedDict field's TempNode rvalue placeholder.

* chore: extend basedpyright slack to the two rules #30563 left at default

PR #30563 raised basedpyright slack to ~10% of baseline across the noisy reportUnknown*/reportAny family so staging bundles clear the per-rule gate, but it left reportArgumentType (slack 3) and reportPrivateUsage (slack 10) at their original tight values. This bundle pushes those two 10 and 1 over their caps respectively, so apply the same ~10% policy: reportArgumentType baseline 1863 -> slack 180, reportPrivateUsage baseline 1625 -> slack 160. No baselines move; only the slack on these two rules

* fix: handle duplicate tool calls and stream tail disconnects

* fix(proxy): mark stream completed before tail yields, not after [DONE]

Clients routinely close the connection right after the final chunk or the
terminating data: [DONE] frame. Setting stream_completed only after those
trailing yields made the GeneratorExit from that close fall into the
disconnect branch, recording false 499 client_disconnected metadata for a
response that already delivered all content and fired success logging, and
double-releasing the max_parallel_requests slot the success callback had
already released. Restore stream_completed before the trailing raw-SSE,
error, and [DONE] yields so terminal-marker closes are treated as the
successful completions they are. The tool_use dedupe guard is kept.

---------

Co-authored-by: apshada <49001649+apshada@users.noreply.github.com>
Co-authored-by: Aarkin Karnik <56022539+Aarkin7@users.noreply.github.com>
Co-authored-by: David Bochenski <david@goincremental.com>
Co-authored-by: Cai Songrui <1922909737@qq.com>
Co-authored-by: Martin Honermeyer <7229+djmaze@users.noreply.github.com>
Co-authored-by: songkuan-zheng <252822057+songkuan-zheng@users.noreply.github.com>
Co-authored-by: fangkang <fangkangm@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Michael <52305679+michaelxer@users.noreply.github.com>
Co-authored-by: michaelxer <michaelxer@users.noreply.github.com>
Co-authored-by: Anuj ojha <ojhaanuj224@gmail.com>
Co-authored-by: 安妮的心动录 <74543653+anneheartrecord@users.noreply.github.com>
Co-authored-by: Zekeriya Akgül <zkry.akgul@gmail.com>
Co-authored-by: Thomas Menard <menardorama@gmail.com>
Co-authored-by: Zang Peiyu <166481866+factnn@users.noreply.github.com>
Co-authored-by: Emerson Gomes <emerson.gomes@thalesgroup.com>
Co-authored-by: Mark Lopez <m@silvenga.com>
Co-authored-by: Varshith <kvarshithgowda@gmail.com>
Co-authored-by: Huynh Duc Tran <110240973+hdt12a1@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Samarth Maganahalli <samarth.maganahalli@rubrik.com>
Co-authored-by: Dushyant Acharya <dushyantacharya873@gmail.com>
Co-authored-by: Dushyant Acharya <dushyantacharya@Dushyants-MacBook-Pro.local>
Co-authored-by: Thijmen Stavenuiter <thijmenstavenuiter@gmail.com>
Co-authored-by: Vineeth Sai <vineethsai4444@gmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mateo Wang <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: rvishwas26 <rvishwas@athenahealth.com>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
Co-authored-by: Joel Tony <github@jaytau.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: hcl <chenglunhu@gmail.com>
Co-authored-by: ztko <96878659+koztkozt@users.noreply.github.com>
Co-authored-by: Nahrin <nahrin@nahrinoda.com>
Co-authored-by: Humphrey <a739376838@gmail.com>
Co-authored-by: kursadlacin <kursadlacin@gmail.com>
Co-authored-by: kursad <kursad.lacin@brado.net>
Co-authored-by: Yuriy <yuriy.shuyskiy@gmail.com>
Co-authored-by: Recep S <22618852+us@users.noreply.github.com>
Co-authored-by: Moshe Malawach <moshe.malawach@protonmail.com>
Co-authored-by: Moshe Malawach <moshemalawach@users.noreply.github.com>
Co-authored-by: Rongkun Yan <2493404415@qq.com>
2026-06-16 18:23:13 -07:00
Yassin Kortam
cd26f7d77a
feat(proxy): add verification_uri_complete to CLI SSO device flow (#30571)
* feat(proxy): add verification_uri_complete to CLI SSO device flow

Add an opt-in verification_uri_complete to POST /sso/cli/start. The URL is
the existing /sso/key/generate?source=litellm-cli&key=<login_id> browser-start
URL with an added user_code query param. The code is carried through the OAuth
flow via the same state channel that already carries login_id, and the post-SSO
verify page pre-fills the user_code input (HTML-escaped) so same-host clients
confirm rather than transcribe.

The manual flow is unchanged and remains the default: when no user_code is
present the verify page renders the empty input byte-for-byte as before, and
submission still hashes and compare_digest-checks both the user_code and the
browser_complete_token. Pre-filling is a UX shortcut, not an auth bypass.

Resolves LIT-3693

* fix(proxy): validate CLI SSO user_code and clarify pre-filled verify page

Address Greptile review on the verification_uri_complete flow. Guard the
user_code query param with the canonical server-issued format
([A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}) before it is threaded into the OAuth
state, so an actor who knows a login_id cannot bloat the size-limited state
with an arbitrary value; a non-conforming code falls back to the manual flow.
Make the verify-page instruction conditional so the pre-filled page reads
"Confirm the verification code below" instead of pointing at a terminal that,
in the daemon use case, does not exist.

* fix(proxy): modern union syntax for new CLI SSO params and regen dashboard types

Use str | None instead of Optional[str] on the CLI SSO signatures touched by
this PR so the ruff strict-rule budget (UP045) stays under its ceiling, and
regenerate ui/litellm-dashboard/src/lib/http/schema.d.ts so the dashboard API
types pick up the new optional user_code query param on /sso/key/generate.

* fix(proxy): gate CLI SSO verification_uri_complete behind operator opt-in (default off)

Gate verification_uri_complete behind a new general_settings flag
allow_cli_sso_verification_uri_complete, default false. When off, /sso/cli/start
does not return verification_uri_complete and /sso/key/generate ignores the
user_code query param, so the default deployment keeps the existing manual flow.
Same-host clients, where the device that starts the flow and the browser run on
the same machine, opt in explicitly. The submitted code is still hashed and
compare_digest-checked and browser_complete_token is still required. Documents
the flag on ConfigGeneralSettings and regenerates the dashboard API types.
2026-06-16 17:23:37 -07:00
Mateo Wang
be4fa702e7
ci(lint): ratcheted type-discipline gate (mutable collections, casts, guards, kwargs, suppressions) (#30500)
* ci(lint): enforce type-discipline budget for casts and type guards

Add a ratcheted gate that blocks net-new typing.cast() usage and bans
TypeGuard/TypeIs outright, layered on the existing ruff-strict budget setup.

- ruff-strict.toml: ban cast/TypeGuard/TypeIs (typing + typing_extensions)
  via flake8-tidy-imports banned-api (TID251) for a coarse import-level freeze.
- ruff-strict-budget.json: bump TID251 baseline 2404 -> 2662 to absorb the
  ~258 pre-existing usages now matched by the new banned-api entries.
- scripts/check_type_discipline.py: AST checker adding LIT006 (cast call sites,
  suppress with `# cast-ok: <reason>`) and LIT007 (TypeGuard/TypeIs annotations,
  suppress with `# guard-ok: <reason>`) for per-call-site granularity.
- scripts/type_discipline_gate.py: baseline+slack gate with delta-vs-base,
  mirroring ruff_strict_gate.py.
- type-discipline-budget.json: LIT006 baseline 1013 (slack 10), LIT007 0/0.
- test-linting.yml: run the gate in CI against the PR base SHA.

* ci(lint): enforce suppression-reason budgets and guard budgets against loosening

- wire the **kwargs ban (LIT008) into the vendored type-discipline checker so it
  matches the budget that already referenced it
- freeze LIT003/LIT004 (noqa / type-ignore without codes or reason) and LIT005
  (*-ok suppression without a reason) at slack 0 so any net-new unexplained
  suppression trips the type-discipline gate
- add scripts/budget_ratchet_check.py and a separate, non-gating budget-ratchet CI
  job that turns red when any *-budget.json ceiling is raised, a rule is dropped,
  or a budget file is deleted

* ci(lint): ban mutable collections in annotations and all mutable construction

Expand LIT001 from coarse builtins at interfaces to any mutable collection
in any annotation (builtins, typing aliases, collections concretes, mutable
ABCs) across signatures, class attributes, locals, and globals. Add LIT009 to
flag mutable-collection construction (literals, comprehensions, constructors)
so the unannotated seed-then-mutate pattern is caught too. Enumerate any-ok in
LIT005 so its reason requirement holds even when only the stdlib checker runs.
Budget LIT001 (21452) and LIT009 (25222) with slack 10 to ratchet down.

* ci(lint): recommend pydantic at boundaries and add functional-refactor guidance

Drop the msgspec mention from the cast banned-api messages so the recommended
validation path matches the codebase's primary pattern (pydantic). Add a note to
CLAUDE.md that lint / type-discipline failures should be resolved by refactoring
to functional, immutable patterns rather than reaching for mutable structures or
`# mutable-ok`.

* style: make CLAUDE.md more concise

* chore: update CLAUDE.md guidelines

* ci(lint): renumber mutable construction LIT009 -> LIT002 next to LIT001

Group the mutable-collection family together: LIT001 (mutable collection in any
annotation) and the construction rule now sit adjacent at LIT001/LIT002. The
freed LIT009 slot is taken by the sibling Any gate (check_any_discipline.py,
#30379), which moves its Any-typed-value rule LIT002 -> LIT009 in lockstep so
the shared LIT namespace stays contiguous with no holes. Budget, gate docstring,
and the checker's own docstring/messages are updated to match.

* fix: numbering in CLAUDE.md

* test(lint): test type-discipline checker, scope LIT007 to return types

Add regression tests for check_type_discipline.py (every LIT rule, its
suppression, and the comment scanner) and for budget_ratchet_check.py.

Confine LIT007 to function return annotations, the only place TypeGuard/TypeIs
are valid, so a runtime name that merely reads those identifiers is no longer
flagged. Switch scan_comments to io.StringIO(source).readline, the standard
readline that returns '' at EOF, dropping the iter(...).__next__ idiom.

* fix(lint): best-effort worktree teardown so cleanup can't mask the real error

base_counts ran `git worktree remove` through the raising `_run` in its finally,
so a failed `git worktree add` (or a failure in the body) was masked by a second
SystemExit from the cleanup. Tear the worktree down best-effort, like the sibling
rmtree, so the original error propagates.

* fix(lint): ratchet fails loudly on an unresolvable base; drop dead checker state

Verify the merge-base ref resolves to a commit before trusting a missing-file
result from git show, so an invalid or empty BASE_SHA now turns the budget-ratchet
guard red instead of skipping every budget and passing vacuously

Also drop the unused Comments.by_line field and the phantom --changed-only usage
line from check_type_discipline's docstring, and cover the ref handling with tests

* fix(lint): degrade malformed source to LIT000 instead of crashing the checker

tokenize.generate_tokens raises IndentationError (a SyntaxError subclass) on a dedent
mismatch, which escaped scan_comments' tokenize.TokenError handler and crashed the whole
checker run, zeroing the gate for that invocation. Catch SyntaxError too so the file
falls through to ast.parse and is reported as LIT000, matching the checker's
graceful-degradation contract. Also add the trailing newline ruff-strict.toml lacked

* perf(lint): skip the base worktree scan when no rule is over its ceiling

cmd_check created a git worktree and re-scanned the base tree on every run, but a
rule can only breach when its head count is already over baseline + slack; when none
are, the base comparison cannot change the verdict. Short-circuit to OK in that case,
which is every green PR, roughly halving the gate's work. Extract over_ceiling and
cover it (and evaluate's drift-safety) with tests

* fix(lint): exempt .dict()/.list()/.set() method calls from LIT002

_construction_kind matched dict/list/set as constructors via func.attr too, flagging
common method calls like pydantic's model.dict() as mutable construction; 200 such
false positives existed in litellm. Recognize dict/list/set construction only when
unqualified while keeping the collections concretes (deque/defaultdict/...) matchable
as attributes, since those are rarely method names. Ratchet the LIT002 baseline down
25222 -> 25022 to reflect the removed false positives

* chore(lint): bump basedpyright ceilings to absorb staging base drift

The basedpyright gate added in #30379 is a total-count check against
basedpyright-code-budget.json and the linting workflow runs only on
pull_request, so pushes to litellm_internal_staging never re-baseline it.
Merging staging into this branch surfaced that drift: seven
reportAny/reportUnknown* rules sit 10-149 errors above their committed ceiling
even though this PR changes no files under litellm/, the only path basedpyright
scans (pyrightconfig include is litellm). The new baselines match the counts CI
measured on the merge commit, with the existing per-rule slack preserved

* fix(lint): ratchet guard watches every budget file, not just two

DEFAULT_BUDGETS only listed ruff-strict-budget.json and
type-discipline-budget.json, so mypy-code-budget.json and
basedpyright-code-budget.json were unguarded and their ceilings could rise with
no signal, which is exactly the failure mode this guard exists to prevent. The
gap became concrete when this PR bumped basedpyright-code-budget.json to absorb
staging drift. All four budgets are now watched, so the budget-ratchet job
surfaces that basedpyright bump for human review the same way it surfaces the
TID251 raise. A regression test pins that every *-budget.json on disk is in
DEFAULT_BUDGETS, failing loudly if a future budget escapes the ratchet

* fix: add a lot more slack

* fix(lint): restore LIT003 frozen slack to 0

The blanket slack bump set LIT003 (bare # noqa without codes or a reason) to a
slack of 50, which contradicts the documented zero-tolerance invariant: the gate
docstring and the PR description table both freeze LIT003/LIT004/LIT005 at slack
0 so any net-new unexplained suppression trips the gate. Slack 50 would let 50
new bare noqas through silently. The actual LIT003 count is 397, well under the
516 baseline, so restoring slack to 0 keeps the gate green while putting the
freeze back. LIT004/LIT005/LIT007 were already correct at 0

* fix(lint): restore documented slack 10 for the buffered LIT rules

The slack bump left LIT001/LIT002/LIT006/LIT008 at 2000/2500/100/100, 10-250x the
"/ 10" the PR description table and the gate docstring document. That buffer was
never needed: the gate already blames a rule only when its count exceeds the
ceiling and grew vs the merge-base, so the violations the staging merge added in
litellm/ sit in both head and base and are never charged to this PR. With slack
back at the documented 10 the gate stays green, and the ceiling is tight again
(LIT006 no longer waves through 99 net-new cast() calls). Baselines are
unchanged; only the slack returns to its documented value

* fix(lint): ratchet LIT003 baseline down to its actual count

The LIT003 baseline was 516 while the current bare-noqa count is 397, leaving
~119 units of headroom that undercut the documented zero-tolerance freeze: the
gate docstring claims any net-new bare noqa trips the gate, but with cap 516 a PR
could add over a hundred first. Drop the baseline to the measured 397 so the
freeze is exact (cap = 397 + slack 0), the same hard-zero-at-the-boundary shape
LIT005 and LIT007 already use and pass in CI. PR table row updated to 397 / 0

* fix: increase slack

* fix: increase slack

* docs(lint): align gate docstring with buffered LIT003/LIT004 slack

The budget now gives LIT003/LIT004 nonzero slack, so the gate's prose no
longer claims they are frozen at slack 0; LIT005 remains the reasonless-
suppression freeze and LIT007 the hard zero.
2026-06-16 16:59:21 -07:00
ryan-crabbe-berri
5a62806fdc
chore(lint): remove PLR0915 too-many-statements ruff rule (#30574)
Drops PLR0915 from ruff's extend-select along with its per-file-ignores,
and strips the now-unused `# noqa: PLR0915` directives across the codebase
(RUF100 would otherwise flag them as unused). The C901 suppression that
shared a directive with PLR0915 in streaming_handler.py is preserved.
2026-06-16 16:52:49 -07:00
Yassin Kortam
27c1dfbdc7
fix(otel): accept UPPER_SNAKE_CASE OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT in v2 (#30562)
V1 read this env var case-insensitively, so SPAN_AND_EVENT enabled content
capture. The v2 config compared the value against its lower_snake_case
canonical constants without normalizing, so an operator carrying the
SPAN_AND_EVENT spelling forward silently left capture off and no
gen_ai.input/output.messages reached the span. Normalize the value to lower
case at the config boundary so both spellings work.
2026-06-16 14:18:42 -07:00
Yassin Kortam
96b9437bdd
fix(budget): recompute budget_reset_at when budget_duration changes on /budget/update (#30555)
POST /budget/update did not recompute budget_reset_at when budget_duration
changed and no explicit budget_reset_at was supplied, leaving shortened
budgets pinned to the old (longer) schedule. The same defect reached
POST /team/update via team_member_budget_duration, which delegates to
update_budget.

update_budget now recomputes budget_reset_at = get_budget_reset_time(duration)
when the caller sets budget_duration without pinning budget_reset_at,
mirroring /budget/new. Explicit budget_reset_at is preserved and updates
that omit budget_duration leave the reset untouched. get_budget_reset_time
now declares its datetime return type so the recomputed value stays concretely
typed.

Resolves LIT-3362
2026-06-16 14:12:39 -07:00
Mateo Wang
ead8a708bb
fix(bedrock): preserve cache_control for ARN models in /v1/messages adapter (#29823)
* fix(bedrock): preserve cache_control for ARN models in /v1/messages adapter

Bedrock Application Inference Profile ARNs contain neither "anthropic" nor
"claude", so is_anthropic_claude_model could not detect them and the
/v1/messages adapter silently dropped cache_control during the Anthropic to
OpenAI translation. Prompt caching never activated for these models, while the
same profile cached correctly through /v1/chat/completions.

Add an is_bedrock_arn_model check scoped to _add_cache_control_if_applicable so
cache_control is preserved for ARN-based models without broadening the shared
is_anthropic_claude_model helper, which also drives thinking translation.

Fixes #26625

* refactor(bedrock): match :bedrock: ARN service field in is_bedrock_arn_model

Tighten the ARN detection so it pins "bedrock" to the colon-delimited service
field of the ARN rather than matching the substring anywhere. This avoids a
false positive for another service's ARN whose resource name merely contains
"bedrock" (e.g. arn:aws:sagemaker:...:endpoint/my-bedrock-transcriber).
2026-06-16 13:04:42 -07:00
Yassin Kortam
f444539ea9
fix(otel): export v2 gen_ai client metrics to the configured meter provider (#30549)
* fix(otel): export v2 gen_ai client metrics to the configured meter provider

The V2 OpenTelemetry integration recorded the six gen_ai.client.* histograms
into a MeterProvider it built locally in _init_metrics and never published. The
recording code ran fine; the metrics simply landed in a provider disconnected
from the global pipeline, so an operator's configured readers/exporters (and the
server-metric instrumentation bound to the global meter provider) never saw them.

Resolve the meter provider the OTel-idiomatic way instead: reuse the operator's
globally configured MeterProvider when one is set so its readers receive the
GenAI histograms, build and register one as the global only when none is set so
V2 owns metrics export (mirroring how V2 owns trace export), and keep the
injected meter_provider as an explicit override for DI and tests.

* refactor(otel): hoist meter imports and harden global resolution

Move the opentelemetry metrics and sdk MeterProvider imports to module top
instead of importing inside resolve_meter_provider/build_meter_provider; the
SDK is already a top-level dependency for tracing, so the per-call imports
added nothing.

resolve_meter_provider now reuses an explicit NoOpMeterProvider as well as a
real SDK provider, so an operator opt-out is honored, and the built provider
is always the one returned so its reader thread is never orphaned.

Drive the regression test through the public metrics.get_meter_provider via
monkeypatch rather than writing opentelemetry's private _METER_PROVIDER slot,
and add focused tests for the injected and no-op resolution branches.

* fix(otel): type resolve_meter_provider as the api MeterProvider base

mypy flagged the return as incompatible because honoring an explicit
NoOpMeterProvider returns a value of the opentelemetry api MeterProvider base
rather than the sdk subclass. Annotate the resolver in terms of the api base and
keep the sdk class for construction and the reuse isinstance check.
2026-06-16 12:15:26 -07:00
Yassin Kortam
b8b0d458af
fix(otel): stamp gen_ai.input/output.messages on v2 spans (#30548)
The canonical GenAI mapper's _LLM_CALL_ATTRS table had no extractors for
gen_ai.input.messages or gen_ai.output.messages, so V2 LLM spans never carried
prompt or completion content even when capture was enabled via
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=span_and_event. The request
and response bodies were already captured onto LLMCallSpanData.messages_in and
choices_out, but the mapper never read them.

Add the two extractors, serializing messages_in and output_messages(d) through
serialize_messages so the keys are omitted when content capture is off and the
spans stay sparse.

Resolves LIT-3788
2026-06-16 12:14:35 -07:00
Shivam Rawat
902122a06b
fix(proxy): allow internal roles to access vector store CRUD routes (#30503)
Add bare /v1/vector_stores/{vector_store_id} to openai_routes so retrieve, update, and delete classify as LLM API routes for internal user and internal viewer roles.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 12:13:31 -07:00
Mateo Wang
d0c2e87810
ci: ratchet lint and type-check gates (ruff preview, ANN, mypy, basedpyright) (#30379)
* ci: enable ruff preview rules under the budgeted strict gate

Turn on ruff preview in the strict-budget lane (ruff-strict.toml) only,
leaving the clean gate (ruff.toml) untouched so make lint-ruff stays at
zero. Enumerate the 118 firing codes explicitly with
explicit-preview-rules so the gate is deterministic and stable across
ruff upgrades rather than depending on preview auto-selecting the broad
catalog.

Grandfather the existing 58438 violations into ruff-strict-budget.json
as per-rule baselines with headroom, so only net-new violations fail CI.
The existing ten rules keep their hand-tuned slack; the new rules get
slack 10 when the baseline is 50 or more and 3 otherwise.

* ci: add ANN return-type rules to the budgeted strict gate

Add ANN201/202/204/205/206 (missing return annotations) to the strict
lane and grandfather the existing counts into ruff-strict-budget.json so
the codebase ratchets toward explicit return types without breaking CI.

* ci: add mypy (disallow_untyped_defs) and basedpyright strict gates with baselines

Add two type-check gates, each grandfathering the current tree so only
net-new violations fail CI, matching the ruff strict-budget ratchet.

mypy gains disallow_untyped_defs in litellm/mypy.ini (the config the CI
invocation actually reads; the root [tool.mypy] is not picked up from the
litellm/ working dir). The 4885 existing missing-annotation errors are
captured in litellm/.mypy-baseline.txt and the run is piped through
mypy-baseline filter so new untyped defs are rejected.

basedpyright runs in strict mode over litellm/, with
enableTypeIgnoreComments disabled so it only honors '# pyright: ignore'
and never polices mypy's '# type: ignore'. The existing strict diagnostics
are grandfathered into .basedpyright/baseline.json.

Both tools are pinned in the dev group and uv.lock; the lint workflow and
Makefile run them filtered through their baselines, with
lint-mypy-baseline-update and lint-basedpyright-baseline-update to ratchet.

* ci: raise lint job timeout to 15m for the basedpyright strict pass

* ci: pin pythonVersion 3.12 and regenerate baselines against merged base

Merge litellm_internal_staging so the baselines cover code the CI merge
includes (e.g. the cisco_ai_defense guardrail), which otherwise tripped
the mypy gate with 3 ungrandfathered no-untyped-def errors. Pin
pythonVersion 3.12 in pyrightconfig so basedpyright's strict analysis is
reproducible across interpreter versions (CI runs 3.12).

* ci: regenerate basedpyright baseline against the frozen lint env

The previous baseline was generated with optional provider deps (azure,
google, anthropic, mcp, numpydoc, google-genai) installed locally, so CI's
dev-only env surfaced ~3500 reportUnknown*/reportMissingTypeStubs errors
not in the baseline. Regenerate after uv sync --frozen so the baseline
reflects the same dependency set the lint job sees.

* ci: regenerate basedpyright baseline on python 3.12 frozen env

The prior baseline still carried proxy-dev packages (e.g. prisma) that the
lint job's dev-only, python 3.12 env lacks, leaving 2 unresolved-import
errors ungrandfathered. Regenerate in a python 3.12 venv synced to the
frozen lock with default groups only, so the baseline matches exactly what
CI sees.

* ci: replace type-check baselines with per-file count budgets

The mypy and basedpyright baselines were position-sensitive (and the
basedpyright one was a 27MB file), so ordinary line shifts churned them.
Replace both with a per-file count gate: scripts/type_check_gate.py reduces
each tool's output to errors-per-file and checks it against a committed
{file: max} budget, ignoring line and column numbers. A file fails only
when it gains more errors than its ceiling; debt can't be shuffled between
files because each file has its own cap and new files default to zero.

Budgets (mypy-file-budget.json 48K, basedpyright-file-budget.json 96K) are
generated in the python 3.12 frozen lint env so they match CI. Drops the
mypy-baseline dependency; basedpyright runs without its native baseline.
ratchet via make lint-mypy-budget-update / lint-basedpyright-budget-update.

* ci: add a small per-file slack to the type-check gate

Allow each file to drift PER_FILE_SLACK (5) errors past its recorded count
before failing, so a basedpyright inference ripple in an unrelated file
doesn't break the build over a couple of errors. Budgets still record exact
counts; the tolerance is applied at check time.

* ci: move type-check slack into the budget json and trim lint timeout

Make slack declarative: the budget is now {"slack": N, "files": {path: count}}
so the tolerance is tuned in JSON without editing the script, mirroring how
ruff-strict-budget.json carries its slack. --update preserves the existing
slack. Also drop the lint job timeout from 15m to 10m; the mypy and
basedpyright passes add ~2m, leaving the job around 4-5m, so 10m is a
comfortable margin.

* ci: collapse fully-adopted ruff categories and drop inert preview flag

ANN (all nine non-removed rules) and BLE (its only rule) were spelled out
code-by-code; replace each with its category selector, which is exactly
equivalent in 0.15.3 (the removed ANN101/ANN102 are skipped by a category
selector and error when named explicitly). explicit-preview-rules was inert:
every selected rule is stable and nothing is selected by category, so the flag
had nothing to gate. Verified the strict-rule counts are identical before and
after (62379 each, zero per-rule drift), so no budget change.

* ci: drop redundant pyright dev dependency

Nothing invokes bare pyright in the Makefile, the linting workflow, or
scripts; the basedpyright gate added on this branch is the only type
checker that runs. basedpyright is a superset fork that reads the same
pyrightconfig.json and honors the same "# pyright: ignore" comments, so
pyright==1.1.408 in the ci group was dead weight. Regenerated uv.lock
under the same exclude-newer cutoff so the only change is removing
pyright and its package stanza

* ci: un-weaken mypy and error on Any in basedpyright

mypy: enable warn_return_any, drop the valid-type silencer, and stop globally ignoring missing first-party imports via [mypy-litellm.*] ignore_missing_imports = False, which surfaced eight real broken litellm.* imports the blanket ignore was hiding; third-party imports stay ignored. The per-file budget moves 4888 -> 5799 (902 no-any-return, 1 valid-type, 8 import-not-found), all grandfathered so only net-new errors fail and the ceilings ratchet down

basedpyright: error on reportExplicitAny and reportAny. The per-file budget moves 117033 -> 148946 (6931 explicit-Any, 24954 Any-typed expressions), grandfathered the same way

* ci: add Any-discipline gate on changed lines under litellm/

Add scripts/check_any_discipline.py, a type-aware gate that fails when a
changed line holds a value typed Any -- including the X | Any unions that
mypy --strict / basedpyright accept (e.g. re.Match.group() -> str | Any,
json.loads() -> Any, bare dict -> dict[Any, Any]).

It reuses the repo's mypyc-compiled mypy 1.19 via a custom generic AST
walker (mypyc precludes subclassing TraverserVisitor), loads litellm/mypy.ini
for parity with lint-mypy, and uses a dedicated incremental cache
(.mypy_cache_any) with mtime+hash invalidation to force re-checks. Scope is
changed-lines-only so editing a legacy file never forces cleaning its
existing Any debt; suppress a genuine typed/untyped boundary with
# any-ok: <reason> (ANY002 requires the reason).

Wire it into the Makefile (lint-any, lint, lint-dev), a parallel
any-discipline CI job with its own actions/cache, .gitignore, and the
CLAUDE.md / CONTRIBUTING.md docs.

* ci: move Any-gate codes into the shared LIT namespace

Renumber the Any-discipline checker into the LIT*** scheme owned by
scripts/check_type_discipline.py (PR #30500) so the two checkers share one
rule namespace and suppression convention:

  ANY001 -> LIT002  (Any-typed value; LIT002 was the retired/free slot)
  ANY002 -> LIT005  (any-ok without a reason; the shared suppression-reason code)
  ANY000 -> LIT000  (setup/build/read error; the shared error code)

Messages and behavior are unchanged; LIT005's text already matches the
"<token> requires a reason" shape used for cast-ok/guard-ok.

* ci: gate mypy and basedpyright per error rule, not per file

Switch the mypy/basedpyright budget gate from per-file error counts to
per-rule-code totals, mirroring the {rule: {baseline, slack}} shape of
ruff-strict-budget.json. A rule fails when its codebase-wide error count
exceeds baseline + slack, so violations are tracked by category rather
than by file location.

scripts/type_check_gate.py now parses mypy from its text output (trailing
[code]) and basedpyright from --outputjson (the JSON `rule` field), since
basedpyright's wrapped text diagnostics mis-attribute the rule on
continuation lines. Replace the *-file-budget.json files with freshly
captured *-code-budget.json baselines and update the Makefile, CI, and
CLAUDE.md accordingly.

* docs: prefer Pydantic validation over any-ok suppression

Point the Any-discipline guidance at validating Any with Pydantic (a model
or TypeAdapter that returns a typed value or raises) and frame
# any-ok as a last resort that should ideally never be used.

* chore: remove extraneous comment

* chore: make the CLAUDE.md more concise

* chore: clean up bloated CONTRIBUTING.md additions

* chore: make Makefile more concise

* ci: add the lint-budget-update target CLAUDE.md references

CLAUDE.md tells contributors to run make lint-budget-update, but the
target was never defined. Add it as an aggregate that re-captures the
ruff, mypy, and basedpyright budgets in one shot.

* ci: recapture mypy and basedpyright budgets in the lint env

The per-rule baselines were captured in a richer dependency env than the
CI lint job's uv sync --frozen, so CI resolved fewer types and reported
more errors than the budgets allowed (no-any-return 902 over cap 900, plus
several basedpyright reportUnknown* rules). Regenerate both in the frozen
env so they grandfather the true CI debt: mypy 5786 -> 5799 (no-any-return
890 -> 902, valid-type 1 restored), basedpyright 146213 -> 148942.

* ci: check out PR head sha in lint and any-discipline jobs

The default pull_request checkout uses refs/pull/N/merge, which folds the
latest base commits into HEAD. The diff-based gates (ruff delta, Any
discipline) then diff against the event's older base.sha and blame base's
own new commits on this branch; staging's otel-v2 and streaming changes
(#30326, #30485) tripped the Any gate on files this branch never touched.
Checking out the PR head sha makes the gates diff the real branch tip
against base, and pins the tree the mypy/basedpyright budgets were captured
against so their counts stay deterministic as the base advances.

* ci(lint): renumber Any-typed-value rule LIT002 -> LIT009

Free up LIT002 for the sibling type-discipline gate (check_type_discipline.py,
#30500), which groups its mutable-collection family at LIT001 (annotation) and
LIT002 (construction). This gate's Any-typed-value rule moves to LIT009 so the
shared LIT namespace stays contiguous with no holes; LIT000 and LIT005 are
unchanged.

* style: rename lint-strict-budget -> lint-ruff-budget

* ci: harden type-check gates against silent passes (greptile review)

type_check_gate.py: refuse to certify a vacuous run. The CI pipe swallows
the tool's exit code ('tool || true'), so a crashed mypy/basedpyright that
emits nothing would parse to zero errors, breach no ceiling, and pass.
is_vacuous_run() now fails when nothing was parsed but the budget expects
errors. Also wrap basedpyright's json.loads in a JSONDecodeError handler
that prints the offending output instead of dumping a raw traceback.

check_any_discipline.py: ALL_LINES was None, which dict.get() also returns
for a path absent from the line map, so a path-normalisation mismatch could
let a violation on an unchanged file pass the scope filter. Make ALL_LINES a
distinct sentinel object so 'whole file' and 'path missing' are unambiguous.

Adds tests for all three.
2026-06-16 12:07:46 -07:00
Sameer Kankute
816fca939f
chore(oss): litellm oss staging 150626 (#30463)
* fix(pricing): add GitHub Copilot MAI Code Flash pricing (#30415)

* fix(pricing): add GitHub Copilot MAI Code Flash pricing

Add GitHub Copilot pricing entries for MAI-Code-1-Flash and the internal Copilot CLI model name so cost calculation can price input, cached input, and output tokens.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(pricing): cover GitHub Copilot MAI Code Flash pricing

Add regression coverage for both GitHub Copilot MAI-Code-1-Flash model names, including cached input pricing, chat endpoint metadata, and cost_per_token arithmetic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(router/proxy): propagate completed_response through FallbackResponsesStreamWrapper for streaming /v1/responses container ownership (#30210) (#30213)

* fix(router/proxy): propagate completed_response through FallbackResponsesStreamWrapper for streaming /v1/responses container ownership (#30210)

#28990 added ownership recording for streaming /v1/responses via
_wrap_responses_stream_for_container_ownership, which reads
`getattr(stream_response, 'completed_response', None)` to extract the
ResponsesAPIResponse. The unit test bypassed the Router, so it never
exercised the production wrapping path.

Through the Router (every proxy deployment), the stream is wrapped by
FallbackResponsesStreamWrapper (router.py:2527). Its __init__ set
`self.completed_response = None` and __anext__ only forwarded chunks
— the inner source iterator's terminal event never bubbled up to the
attribute the ownership hook reads, so the hook silently recorded
nothing and every follow-up /v1/containers/<id>/files call returned
403 for non-admin keys.

This commit:

- router.py: pre-resolves the responses-API terminal event tuple
  (response.completed / .incomplete / .failed) once per
  _aresponses_streaming_iterator call, and has the wrapper's __anext__
  sniff each forwarded chunk's .type. First terminal event hit gets
  stored on the wrapper's completed_response. Iterator-agnostic — works
  for source_iterator AND any future wrapper.

- common_request_processing.py: when _extract_completed_responses_response
  returns None we now warn instead of silently skipping. Reporter on
  #30210 lost a day to this exact silent skip; the warning surfaces
  future regressions of the same shape directly in operator logs.

Fixes #30210

* fix(router): type-ignore wrapper getattr-defaults; broaden ownership-skip warning

CI lint (mypy) flagged the three pre-existing getattr(..., None) assignments
in FallbackResponsesStreamWrapper.__init__:

  router.py:2564 self.response = getattr(source_iterator, 'response', None)
  router.py:2565 self.model    = getattr(source_iterator, 'model', None)
  router.py:2566 self.logging_obj = getattr(..., None)

Those lines also exist on litellm_internal_staging and pass mypy there.
Adding the typed terminal-event tuple above the class made the function
body more narrowable, which surfaced the pre-existing mismatch — base
class declares non-Optional types but the bridge path
(LiteLLMCompletionStreamingIterator) legitimately omits these. Keep
the None fallback and silence with type: ignore[assignment].

Greptile 4/5 note: the ownership-skip warning hard-named code_interpreter
which misleads operators when a non-code_interpreter stream aborts.
Generalize to 'any tool container (e.g. code_interpreter)'.

* fix(register_model): drop synthesized zero costs to preserve sparse entries (#30198) (#30201)

* fix(register_model): drop synthesized zero costs to preserve sparse entries (#30198)

get_model_info synthesizes input_cost_per_token / output_cost_per_token = 0
when they are absent from the raw entry (the price-unknown and free cases
share the same representation). register_model then merges that result back
into litellm.model_cost, which flips a sparse entry from 'no cost keys'
(priced via model name) to 'cost keys = 0' (free).

That defeats _is_cost_explicitly_configured (#24949) on re-registration:
_is_model_cost_zero returns True, common_checks skips every tag / key /
team / user / org budget check for the group, and over-budget traffic
keeps returning 200. Spend keeps recording because cost calc still resolves
by model name, so the symptom is silent and only triggers on the second
register_model pass (router rebuild, /model/update, config sync).

Mirror the existing litellm_provider-None guard one block above and pop
the cost fields from the synthesized result when they are absent from the
raw entry and not in the caller's value. Caller-provided zeros (genuinely
free models, BYOK overrides) are preserved.

Fixes #30198

* fix(register_model): switch _raw_entry to is-None checks + drop dead test assertion

Greptile #30201 review notes:
- the `or`-chain in the raw-entry lookup treated an empty dict (a key
  with no fields) as falsy and fell through to the second arm — replace
  with explicit `is None` checks so a present-but-empty entry is still
  taken at face value.
- the first assertion in `test_router_double_init_keeps_db_model_entry_sparse`
  used `in (None, 0)` which passes under the bug condition (cost = 0
  matches the tuple); the strong follow-up assertion already covers
  every shape, so drop the dead branch.

* fix(bedrock mantle): use unique function-call id for responses->chat tool calls (#30426)

* fix(bedrock mantle): use unique function-call id for responses->chat tool calls

...

* fix(bedrock mantle): scope unique tool-call id fallback to degenerate call_id

The previous revision preferred the Responses item id for every tool call, which broke providers (and existing tests) where call_id is a unique, canonical correlation key. Restrict the fallback to the degenerate index-based call_id that Bedrock Mantle returns (call_0, call_1, ... resetting per response) and keep call_id otherwise. Revert the change to the OUTPUT_ITEM_DONE streaming handler, whose tool_call_chunk is never emitted (dead code, per review). Extend the regression tests to assert a normal call_id is preserved.

* fix(router): preserve azure_ad_token through CredentialLiteLLMParams for /v1/files + batches (#30235) (#30241)

* fix(router): preserve azure_ad_token through CredentialLiteLLMParams for /v1/files + batches (#30235)

Router.get_deployment_credentials_with_provider re-validates a
deployment's litellm_params through CredentialLiteLLMParams before
handing them to file/batch/passthrough callers:

    return CredentialLiteLLMParams(
        **deployment.litellm_params.model_dump(exclude_none=True)
    ).model_dump(exclude_none=True)

Any field NOT declared on CredentialLiteLLMParams gets silently dropped
on the way through. azure_ad_token was undeclared, so Azure deployments
using OAuth/M2M (azure_ad_token instead of a static api_key) silently
lost their token at the files endpoint and the proxy returned:

    Missing credentials. Please pass one of api_key, azure_ad_token,
    azure_ad_token_provider, ...

Declare azure_ad_token on CredentialLiteLLMParams alongside api_key /
api_base / api_version so it rides through the round-trip. Static-key
deployments stay unaffected (Optional, default None, dropped by
exclude_none=True). Provider-callable (azure_ad_token_provider) is a
separate concern and out of scope here.

Fixes #30235

* fix(ui-types): regenerate schema.d.ts for new azure_ad_token field

CI's 'Verify schema.d.ts matches the proxy OpenAPI spec' check
auto-detected the new field and emitted the exact diff to apply.
Two schemas had `aws_secret_access_key` from CredentialLiteLLMParams,
both get the new azure_ad_token marker next to it.

* fix(proxy): org_admin with own user_id now sees all org teams on /v2/team/list (#30247)

When the UI sends the callers own user_id (as it does for non-Admin
global roles), _enforce_list_team_v2_access now nulls it out for org
admins so _build_team_list_where_conditions scopes by organization_id
only -- matching the legacy /team/list behavior and the documented intent.

Fixes #30215

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* test(vertex_ai): multi-region regression coverage for cachedContents host (#29571) (#29707)

litellm_internal_staging already routes the cachedContents URL through
get_vertex_base_url, fixing the multi-region 404 reported in #29571 —
but carries no test coverage for the actual regression scenario (eu/us
must resolve to the REP host aiplatform.{geo}.rep.googleapis.com).

Add TestContextCachingMultiRegionUrls: parametrized eu/us REP-host
assertions (including absence of the old broken {geo}-aiplatform host),
plus regional (us-central1) and global no-regression checks.

* fix(proxy): close upstream LLM stream when client disconnects mid-stream (#30245)

* fix(proxy): close upstream LLM stream when client disconnects mid-stream

When a streaming client disconnects, Starlette abandons the response
body iterator without calling aclose(), so the proxy's connection to
the upstream backend stays open until garbage collection, which may
never come. The backend (e.g. vLLM) keeps generating into a dead pipe:
small responses drain invisibly into TCP buffers while large ones block
the backend on a full send buffer indefinitely (observed via lsof as an
ESTABLISHED proxy->backend connection minutes after the client left)

create_response now returns a StreamingResponse subclass that closes
both its body iterator and the wrapped upstream-facing generator in a
shielded finally. The upstream generator is closed directly rather than
through a cascade because aclose() on a never-started generator skips
its body, which would make the cascade a no-op when the client
disconnects before the first chunk is sent.
async_streaming_data_generator also gains the same shielded
finally-aclose that async_data_generator in proxy_server.py already
had, covering the Anthropic and Google SSE paths

With this, killing a streaming client causes the backend to observe the
abort within about a second and free its slot, while completed streams
are unaffected. No flag is needed, unlike the non-streaming opt-in
cancel in #30223: this only releases resources after the client is
already gone and does not change any response a client can observe

Fixes #30244

* fix(proxy): close upstream even when body iterator aclose raises BaseException

Addresses the Greptile finding on #30245: the cleanup loop caught only
Exception while the generator-level cleanup catches BaseException, so a
CancelledError or GeneratorExit escaping body_iterator.aclose() would
skip closing the upstream generator. Both sites now use the same scope
and a regression test pins that the upstream is closed even when the
body iterator explodes with a BaseException

* fix(llms): expose aclose on BaseModelResponseIterator so stream close reaches the provider connection

The response-level close added for #30244 only worked for SDK-based
providers (e.g. openai), whose streams expose aclose all the way down.
Providers served by base_llm_http_handler (hosted_vllm and most modern
transformation-based providers) wrap a bare response.aiter_lines()
generator in BaseModelResponseIterator, which had no aclose or close at
all, and nothing retained the httpx response object; so
CustomStreamWrapper.aclose() silently did nothing and the upstream
connection stayed open. Verified with a vLLM-style mock: with
hosted_vllm/ the backend streamed all 100 chunks to completion after
the client disconnected, while openai/ aborted at chunk 6

BaseModelResponseIterator now carries an optional http_response and an
aclose() that closes it; make_async_call_stream_helper attaches the
response after building the iterator. With this, hosted_vllm aborts the
backend within ~1.6s of the client dropping, and completed streams are
unaffected

---------

Co-authored-by: kursad <kursad.lacin@brado.net>

* feat(anthropic): surface compaction usage iterations data (#27065)

* feat(anthropic): surface compaction usage iterations data

* style: apply black formatting to fix lint checks

* fix(usage): correct calculate usage with cached tokens when use ChatCompletionUsageBlock (#30422)

* fix(usage): correct calculate usage with cached tokens when use ChatCompletionUsageBlock

* fix(usage): optimize test imports

* feat: add fastCRW search provider (#30434)

* feat(provider): add LibertAI as a JSON-configured OpenAI-compatible provider (#30203)

* feat(provider): add LibertAI as a JSON-configured OpenAI-compatible provider

* libertai: update served endpoints backup + add mode/matrix tests

Addresses review feedback:
- Add libertai to litellm/provider_endpoints_support_backup.json, the file
  actually served by GET /public/supported_endpoints (the root
  provider_endpoints_support.json already had it).
- Add tests asserting bge-m3 normalizes to mode='embedding' and that the
  served matrix lists libertai. embeddings stays false: the JSON-configured
  provider path only wires chat routing (OpenAILike embedding handler is
  reached only for literal openai_like/llamafile/lm_studio), matching the
  llamagate precedent; bge-m3 remains in the cost map for metadata.

---------

Co-authored-by: Moshe Malawach <moshemalawach@users.noreply.github.com>

* feat(provider): add ModelScope as an OpenAI-compatible provider (#28460)

* add ModelScope API support

* add modelscope api support

* update modelscope model list

* add image-genetation support

* update test and multimodal

* fix: address PR review feedback for modelscope provider

* update README

* fix(customer_endpoints): restrict /customer/daily/activity to admin-only (#28849)

* fix(customer_endpoints): restrict /customer/daily/activity to admin-only

* fix(customer_endpoints): check role before prisma_client guard

* fix(custom_guardrail): key disable_global_guardrails takes precedence over team guardrail list (#28563)

* fix(fallbacks): preserve fallback model in SDK fallback responses (#28260)

* fix(fallbacks): preserve fallback model in response when using SDK-level fallbacks

* fix(fallbacks): gate x-litellm-* passthrough to trusted callers only

The previous patch unconditionally let `x-litellm-*` keys bypass the
`llm_provider-` prefix in `process_response_headers`. That function is
also called on raw upstream-provider response headers (e.g. from
`llm_http_handler.py`), so a malicious provider could return
`x-litellm-attempted-fallbacks` and spoof a LiteLLM-internal marker,
bypassing the proxy model-override guard.

Add a `preserve_litellm_internal_headers` flag (default False). Only
`response_metadata.py`, which re-processes the already-built
`_hidden_params["additional_headers"]` dict (LiteLLM-owned), passes
True. Raw provider header callsites keep the default False, so upstream
`x-litellm-*` still gets the `llm_provider-` prefix.

Adds a regression test for the spoofing case and renames the existing
preserve test to make the trusted-path semantics explicit.

* fix(fallbacks): ignore preserve_litellm_internal_headers for raw httpx.Headers inputs

* style(core_helpers): apply black formatting

* fix(lint): remove banned typing.List/Dict/Any imports and suppress PLR0913 on interface overrides

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

* fix(lint): apply black formatting to modelscope chat transformation

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

* fix(lint): replace noqa with proper fixes — use **kwargs and Awaitable instead of Any/List

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

* fix(lint): remove unused AllMessageValues import

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

* revert: restore base_model_iterator.py to original PR state

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

* fix(lint): restore full method signatures for MyPy compatibility; bump PLR0913 budget for new provider files

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

* fix(lint): use @override to suppress PLR0913 on inherited signatures instead of bumping budget

The overrides keep their full base-class signatures for MyPy compatibility, but those signatures carry more than five parameters, which tripped PLR0913 on each subclass redeclaration. Since the arity is dictated by the base class and cannot be reduced, decorate the overrides with typing_extensions.override; ruff treats that as the intended signal that the parameter count is not under the author's control and skips PLR0913. This restores the PLR0913 baseline to 1813.

* fix(lint): add @override to modelscope image generation overrides

Apply the same typing_extensions.override treatment to the image generation config so its inherited-signature overrides do not count against PLR0913.

---------

Co-authored-by: Joel Tony <github@jaytau.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: hcl <chenglunhu@gmail.com>
Co-authored-by: ztko <96878659+koztkozt@users.noreply.github.com>
Co-authored-by: Nahrin <nahrin@nahrinoda.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Humphrey <a739376838@gmail.com>
Co-authored-by: kursadlacin <kursadlacin@gmail.com>
Co-authored-by: kursad <kursad.lacin@brado.net>
Co-authored-by: Dushyant Acharya <dushyantacharya873@gmail.com>
Co-authored-by: Yuriy <yuriy.shuyskiy@gmail.com>
Co-authored-by: Recep S <22618852+us@users.noreply.github.com>
Co-authored-by: Moshe Malawach <moshe.malawach@protonmail.com>
Co-authored-by: Moshe Malawach <moshemalawach@users.noreply.github.com>
Co-authored-by: Rongkun Yan <2493404415@qq.com>
Co-authored-by: Varshith <kvarshithgowda@gmail.com>
Co-authored-by: Mateo Wang <277851410+mateo-berri@users.noreply.github.com>
2026-06-16 12:06:41 -07:00
Yassin Kortam
9fa74ad8b4
fix(guardrails): stop re-initializing DB guardrails on every poll (#30542)
* fix(guardrails): stop re-initializing DB guardrails on every poll

InMemoryGuardrailHandler._has_guardrail_params_changed compared the
in-memory LitellmParams against the raw dict loaded from the DB. The
in-memory side carries every field default and coerces enums via
model_dump(), while the DB side only holds the keys originally stored,
so the two shapes never compared equal and the guardrail was rebuilt on
every poll cycle.

Each rebuild created a fresh instance, but delete_in_memory_guardrail
only removed the old callback from litellm.callbacks. Request handling
promotes guardrail callbacks into the success/failure/async lists, so
the previous instance stayed referenced there and instances accumulated.

Normalize both sides through LitellmParams(...).model_dump() before
diffing, and purge the callback from every callback list on delete.

* refactor(guardrails): narrow params-normalization fallback to ValidationError

The comparison normalizer caught a bare Exception and silently fell back
to the raw dict, which hid the cause and quietly degraded the affected
guardrail back to re-initializing on every poll. Catch only the
ValidationError that LitellmParams construction can raise, log a warning
so the offending row is diagnosable, and let any other error surface
instead of being swallowed.

* refactor(callbacks): add remove_callback_from_all_lists helper to manager

Move the knowledge of which callback lists a callback can be promoted
into out of the guardrail registry and into LoggingCallbackManager, where
the rest of the callback-list bookkeeping already lives. delete_in_memory_guardrail
now delegates to the new helper instead of iterating the lists itself.
2026-06-16 11:17:49 -07:00
Yassin Kortam
4faeabc254
fix(guardrails): run pre_call hook once for model-level guardrails (#30543)
* fix(guardrails): run pre_call hook once for model-level guardrails

A CustomGuardrail attached to a deployment via litellm_params.guardrails
gets its async_pre_call_hook invoked twice per request: once by the proxy
pre-call loop and again by async_pre_call_deployment_hook after the router
spreads the model-level guardrails into the top-level request kwargs.

Record in request metadata that the proxy pre-call loop already ran a given
guardrail, and have the deployment hook skip it when the marker is present.
Direct-SDK usage never runs the proxy loop, so the deployment hook stays the
sole invocation there and still fires exactly once.

The marker key is stripped from untrusted caller metadata so a request body
cannot suppress a model-only guardrail by pre-seeding it.

* fix(guardrails): mark pre_call dedup on the post-hook request data

Record the exactly-once marker after async_pre_call_hook runs, on the data
object that flows downstream, rather than before it. A guardrail whose hook
returns a brand-new request dict (instead of mutating or spreading the one it
received) would otherwise discard the marker, letting the deployment hook
re-run the guardrail a second time.
2026-06-16 11:17:03 -07:00
Sameer Kankute
bed6ce820c
test(batches): move orphan tests into tests/test_litellm for CI coverage (#30510)
Four batch-related tests lived under tests/litellm/ and were never picked
up by GitHub Actions. Relocate them and fix gemini multimodal e2e to use
the batchEmbedContents path expected for gemini/ provider.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 10:20:59 -07:00
Shivam Rawat
fc9d789d24
fix(integrations): cap Anthropic cache_control injection at 4 blocks (#30480)
* fix(integrations): cap Anthropic cache_control injection at 4 blocks

Respect Anthropic's 4 cache_control breakpoint limit by counting client-supplied blocks, skipping messages that already carry cache_control, and stopping further auto-injection once the limit is reached.

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

* fix(integrations): reserve cache slot for tool_config and short-circuit cap

Address review feedback on the cache_control cap: break out of the injection loop before resolving target indices once the limit is reached, and reserve one of the four breakpoint slots when a tool_config injection point is present so the cachePoint appended by the Bedrock transform does not push the total past Anthropic's limit.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 20:50:38 -07:00
ryan-crabbe-berri
039a2d8bf5
fix(mcp): drop phantom 401 span on delegated OAuth2 tool calls (#30494)
The OAuth2 passthrough ran user_api_key_auth on the client's upstream bearer
first and only recovered after the failed validation had already logged a 401
auth event to the tracer, so successful tool calls to a delegated server each
carried a phantom 401 span. Check delegate_auth_to_upstream before validating:
a delegated server skips the doomed call entirely so nothing is logged, and a
non-delegated server validates normally and surfaces a real 401 rather than
being exchanged for an anonymous upstream-passthrough session.
2026-06-15 17:24:18 -07:00
Yassin Kortam
45d5153c12
feat(otel-v2): emit the 6 gen_ai.client.* metrics at parity with v1 (#30326)
* fix(otel): cap metric attribute cardinality with include/exclude lists

OTEL metrics stamped every per-request hidden_params and metadata.* field
onto each gen_ai.client.* sample, so near-unique values created one metric
time series per request and backends like Splunk Observability Cloud throttled
and dropped the data.

Add an attributes block under callback_settings.otel with mutually-exclusive
include_list (allowlist) and exclude_list (denylist), validated against the
known attribute names at startup and applied once to the metric attributes in
_record_metrics. Spans are untouched, and with no config every attribute is
still emitted so existing setups are unaffected.

Resolves LIT-3600

* fix(otel): resolve metric attribute filter from callback_settings

The proxy usually constructs the OpenTelemetry logger without forwarding the
attributes kwarg, while the filter lives under
litellm.callback_settings["otel"]["attributes"]. __init__ only read the kwarg,
so the recording instance kept config.attributes=None and shipped metrics at
full cardinality even when the filter was configured; a live proxy run exposed
this. Fall back to the global at init for the base otel logger, and add a
regression test that drives the real success hook through the callback_settings
path (the unit tests passed before because they injected the config directly).

* fix(otel): reject gen_ai.token.type from metric attribute filter lists

gen_ai.token.type was a member of VALID_METRIC_ATTRIBUTE_NAMES, so an
operator could list it in include_list or exclude_list and pass startup
validation. The attribute is injected into the input/output token series
after _filter_metric_attributes runs, so the filter never sees it and the
request silently has no effect.

Reject it loudly from either list instead, matching the contract that a
non-actionable attribute name fails fast rather than falling through to a
no-op. It stays a structural discriminator on the token-usage histogram.

* fix(otel): resolve metric attribute filter lazily at record time

The proxy constructs the OpenTelemetry logger before it populates
litellm.callback_settings["otel"]["attributes"], so resolving the filter at
__init__ left config.attributes None and shipped metrics at full cardinality. A
live proxy run confirmed the leak. Resolve the filter on the first metric record
instead, when callback_settings is populated, while still validating an explicit
config eagerly so a bad SDK config fails at startup. The regression test now
constructs the logger before populating callback_settings to mirror that
ordering, so it fails if the filter is resolved too early.

* fix(otel): don't cache invalid filter on lazy callback_settings path

On the lazy callback_settings resolution path, _ensure_metric_attribute_filter
wrote self.config.attributes before validating it. When validation then failed,
_metric_attr_filter_resolved stayed False while config.attributes held the bad
filter, so the next record skipped the callback_settings re-read and re-raised
the stale error indefinitely; fixing the misconfiguration required a restart.

Drop the premature write and resolve from the local value. A subsequent record
now re-reads callback_settings, so a corrected config takes effect without a
restart. The write was dead on the success path anyway, since the resolved
frozensets are what the filter reads.

* feat(otel-v2): emit the 6 gen_ai.client.* metrics at parity with v1

The v2 OpenTelemetry integration was a span engine: it declared two metric
histograms but never created a meter or recorded anything. Bring it to parity
with v1 so a v2-default deployment gets bounded metrics.

Adds the 4 missing metric names, all 6 histograms, a meter-provider builder that
mirrors v1's exporter selection, and a GenAIMetricRecorder that records token
usage (split input/output), cost, operation duration, TTFT (streaming), TPOT,
and response duration on the success hook. Gated on config.enable_metrics so the
default is unchanged.

The attribute cardinality filter is reused from v1 by import (no duplication of
the valid-name set or validation) and resolved lazily from
callback_settings.otel.attributes, matching v1. A misconfigured filter raises
out of the recorder; the logger surfaces it once at ERROR and records nothing,
rather than silently disabling metrics, and a corrected config recovers without
a restart.

* test(otel-v2): drop duplicate misconfig logger test (covered in test_otel_v2_logger)
2026-06-15 16:12:49 -07:00
Shivam Rawat
4f54f997f8
fix(streaming): guard raise_on_model_repetition against empty choices (#30485)
Vertex Gemini Flash web-search streaming can append metadata-only and
usage-only chunks with choices=[] to self.chunks, which caused
IndexError mid-stream when repetition detection accessed choices[0].

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:35:45 -07:00
ryan-crabbe-berri
2655d1dd5e
fix(mcp): default Linear MCP registry entry to streamable HTTP (#30396)
Linear is deprecating the SSE transport at https://mcp.linear.app/sse in
favor of streamable HTTP at https://mcp.linear.app/mcp. Update the curated
discovery registry so the Linear preset uses transport "http" and the /mcp
URL.
2026-06-13 14:45:47 -07:00
yuneng-jiang
df92c7fd07
fix(proxy): support SMTP implicit SSL (port 465) (#30395)
* fix: add smtp ssl (#30248)

* add smtp ssl

* fix comments'

* fix(proxy): verify SMTP server certificate on starttls

* dont read ssl from env

* test(proxy): restore regression test for SMTP_TLS=False starttls skip

---------

Co-authored-by: mubashir1osmani <mubashir.osmani777@gmail.com>
2026-06-13 14:34:43 -07:00
yuneng-jiang
82d9f27887
test(fireworks): mock whisper transcription tests instead of live calls (#30391)
TestFireworksAIAudioTranscription inherited two tests from
BaseLLMAudioTranscriptionTest that made real calls to
audio-prod.api.fireworks.ai. The audio host is a separately-entitled
product, and the CI key gets a 401 there, so both tests fail without
signalling anything about litellm.

Override them in the subclass with a dependency-injected mock OpenAI
client (the openai-compatible path uses the openai SDK, which is why the
failure surfaced as openai.AuthenticationError). The real routing,
model-prefix stripping, and response parsing still run; only the network
call is mocked. The sync path mocks audio.transcriptions.create and the
async path mocks audio.transcriptions.with_raw_response.create.

This matches the existing direction in this file, where the
document-inlining and global-disable tests were already converted off
live Fireworks calls.
2026-06-13 11:52:30 -07:00
Yassin Kortam
3b84150137
fix(otel): record full error message on standard exception event in otel v2 (#30380)
The v2 span engine only stamped error.type and stuffed the message into the
span status description; it never recorded the standard OTel exception event.
Backends that dynamic-map unknown string fields (e.g. Elasticsearch) index the
message as a keyword capped at ignore_above:1024, truncating it. Emit the full
message under the recognized exception.message semconv field via a span event so
it is mapped as full text instead.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-13 18:42:43 +00:00
Yassin Kortam
ec9353cb69
feat(guardrails): add Cisco AI Defense integration (#28249) (#30338) 2026-06-12 23:21:23 -07:00
ryan-crabbe-berri
c90eb7e96f
feat: ruff strict-rule suppressions baseline gate (#30303)
* feat: add ruff strict-rule suppressions baseline gate

Introduce a stricter ruff rule set (typed params, no Any, complexity and
arg-count caps, mutable-default and global-rebinding checks) grandfathered
against the current tree and enforced as a budget rather than zero-tolerance

ruff-strict.toml defines the 9 rules separately from ruff.toml so the existing
ruff check stays green. scripts/ruff_suppressions.py builds the per-file,
per-rule baseline in ruff-suppressions.json and gates CI by failing when the
total grows past the baseline plus a 0.5% slack margin. The baseline ratchets
down via `make lint-suppressions-update` after fixes

* fix: surface per-file drift as a warning on a passing suppressions check

Greptile flagged that cmd_check computed per-file regressions but only printed
them on failure, so violations shifted between files (or a brand-new file under
the slack) passed with a silent OK. Print them as a non-fatal warning on the
pass path too; pass/fail behavior is unchanged

* refactor: gate strict ruff rules on the delta vs base, not a frozen baseline

The committed total-count baseline went stale against a moving base. CI lints the
PR merged with the current staging tip, so violations merged by other PRs counted
against this PR and tripped the budget even though nothing here touched them

Replace it with a drift-proof gate. scripts/ruff_strict_gate.py runs ruff on the
head, keeps only violations on lines this change adds relative to the merge-base,
and fails when a rule exceeds its per-rule allowance in ruff-strict-budget.json
(all 0 today). Because the base is measured live, base drift cancels out and only
what the change introduces is gated. Drops ruff-suppressions.json and the old
suppressions script

* chore: allow 5 new ANN001/ANN003/ANN401 per change

Give the three annotation-completeness rules a small per-change allowance so a
large new module is not blocked over a few untyped params or kwargs, while the
correctness and structural rules (B006, C901, PLR0913, PLW0603, RUF012, ANN002)
stay at 0

* feat: add TID251 typing.Any/Dict import ban and widen annotation budgets

Add TID251 (flake8-tidy-imports banned-api) to ruff-strict.toml, banning new
imports of typing.Any and typing.Dict and steering new code toward structured
types. It counts the import site, about one per file, so it is set non-blocking
at 50 as a forward-looking signal

Widen the annotation-completeness budgets so they nudge rather than block:
ANN001 50, ANN401 50, ANN003 25. Correctness and structural rules stay at 0

* refactor: make the strict gate a drift-safe per-rule total ceiling

Switch the gate from a per-change allowance to a hard ceiling on each rule's
total count across the codebase. The ceiling is baseline + slack in
ruff-strict-budget.json, with baseline captured from today's tree

To stay drift-safe, the gate counts each rule on the head and on the merge-base
(via a throwaway git worktree) and fails a rule only when its head total is over
the ceiling and higher than the base, so base drift never blames a change that
did not add to that rule. Annotation rules keep generous slack (ANN001 and
ANN401 50, ANN003 25, TID251 50); structural and correctness rules are frozen at
today's count. Add make lint-strict-budget-update to re-capture baselines

* chore: give the structural strict rules a cushion of 3

To be liberal to start, B006, C901, PLR0913, PLW0603, RUF012, and ANN002 each get
a slack of 3 instead of 0, so an occasional legitimate case is not hard-blocked.
The annotation budgets are unchanged, and these ratchet down later

* feat: ban more typing collection aliases and tighten annotation slack to 10

Add typing.List, typing.Set, typing.MutableSequence, and typing.MutableMapping to
the TID251 banned-api list, steering new code toward tuple, Sequence, Mapping,
frozenset, and frozen dataclasses. This raises TID251's baseline to 2404

Bring the three rules that were at slack 50 (ANN001, ANN401, TID251) down to 10

* docs: document the strict-gate ratchet and Any-avoidance in CLAUDE.md

Add a line on running make lint-strict-budget-update to knock baselines down
after fixes, and a line on validating untyped inputs in the caller rather than
spending the Any budget

* feat: make it a bit more strict

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-06-12 20:14:45 -07:00
yuneng-jiang
5047eaf7f0
fix(proxy): return deprecated-key lookup result directly in get_data combined view (#30327)
The grace-period branch assigned the recursive get_data result (a
finished LiteLLM_VerificationTokenView) back into the variable that the
combined-view dict normalization then subscripts, raising TypeError on
every request made with a rotated key inside its grace window; auth
surfaced that as a 401. Return the recursive result directly instead.

Regression test drives the full get_data flow: old hash misses the view,
deprecated table resolves to the active token, and the call must return
the view object
2026-06-12 17:44:04 -07:00
Yassin Kortam
f49707bc66
fix(otel): cap metric attribute cardinality with include/exclude lists (#30257)
* fix(otel): cap metric attribute cardinality with include/exclude lists

OTEL metrics stamped every per-request hidden_params and metadata.* field
onto each gen_ai.client.* sample, so near-unique values created one metric
time series per request and backends like Splunk Observability Cloud throttled
and dropped the data.

Add an attributes block under callback_settings.otel with mutually-exclusive
include_list (allowlist) and exclude_list (denylist), validated against the
known attribute names at startup and applied once to the metric attributes in
_record_metrics. Spans are untouched, and with no config every attribute is
still emitted so existing setups are unaffected.

Resolves LIT-3600

* fix(otel): resolve metric attribute filter from callback_settings

The proxy usually constructs the OpenTelemetry logger without forwarding the
attributes kwarg, while the filter lives under
litellm.callback_settings["otel"]["attributes"]. __init__ only read the kwarg,
so the recording instance kept config.attributes=None and shipped metrics at
full cardinality even when the filter was configured; a live proxy run exposed
this. Fall back to the global at init for the base otel logger, and add a
regression test that drives the real success hook through the callback_settings
path (the unit tests passed before because they injected the config directly).

* fix(otel): reject gen_ai.token.type from metric attribute filter lists

gen_ai.token.type was a member of VALID_METRIC_ATTRIBUTE_NAMES, so an
operator could list it in include_list or exclude_list and pass startup
validation. The attribute is injected into the input/output token series
after _filter_metric_attributes runs, so the filter never sees it and the
request silently has no effect.

Reject it loudly from either list instead, matching the contract that a
non-actionable attribute name fails fast rather than falling through to a
no-op. It stays a structural discriminator on the token-usage histogram.

* fix(otel): resolve metric attribute filter lazily at record time

The proxy constructs the OpenTelemetry logger before it populates
litellm.callback_settings["otel"]["attributes"], so resolving the filter at
__init__ left config.attributes None and shipped metrics at full cardinality. A
live proxy run confirmed the leak. Resolve the filter on the first metric record
instead, when callback_settings is populated, while still validating an explicit
config eagerly so a bad SDK config fails at startup. The regression test now
constructs the logger before populating callback_settings to mirror that
ordering, so it fails if the filter is resolved too early.

* fix(otel): don't cache invalid filter on lazy callback_settings path

On the lazy callback_settings resolution path, _ensure_metric_attribute_filter
wrote self.config.attributes before validating it. When validation then failed,
_metric_attr_filter_resolved stayed False while config.attributes held the bad
filter, so the next record skipped the callback_settings re-read and re-raised
the stale error indefinitely; fixing the misconfiguration required a restart.

Drop the premature write and resolve from the local value. A subsequent record
now re-reads callback_settings, so a corrected config takes effect without a
restart. The write was dead on the success path anyway, since the resolved
frozensets are what the filter reads.
2026-06-12 17:29:46 -07:00
Sameer Kankute
079c136742
chore(oss): litellm oss staging 120626 (#30292)
* feat(bedrock): add bedrock mantle gemma 4 models (#30264)

* feat(bedrock): add bedrock mantle gemma 4 models

* test(bedrock): harden mantle local cost fixture

* feat(responses): enable the responses API for the Tensormesh provider (#30209)

* feat(responses): enable the responses API for the Tensormesh provider

* Update litellm/llms/openai_like/providers.json

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>

* fix(langfuse_otel): mark LLM spans as generations (#30250)

* fix(bedrock): stop stream_chunk_size leaking into invoke request bodies (#30240)

stream_chunk_size is a LiteLLM-internal knob for re-chunking the HTTP
response stream. The invoke transformations splat optional_params into the
provider request body without dropping it, and Bedrock rejects unknown
fields, so any bedrock/invoke request that sets the parameter fails with
ValidationException: stream_chunk_size: Extra inputs are not permitted.
Drop it in the invoke dispatcher (covers cohere, titan, mistral, meta,
ai21) and in the Claude messages-format request builder (the route used
for bedrock/invoke Anthropic models)

* fix(bedrock): stop buffering streamed tool-call argument deltas (#30231)

* fix(bedrock): stop buffering streamed tool-call argument deltas

Two issues made Bedrock tool-use streaming arrive as a single end-of-stream
burst through LiteLLM while plain text streamed fine.

First, the anthropic-beta allowlist mapped fine-grained-tool-streaming-2025-05-14
to null for bedrock and bedrock_converse, so the header was silently stripped.
Without that beta, Anthropic models on Bedrock buffer tool input server-side and
emit all toolUse.input deltas at once (verified against converse-stream and
invoke-with-response-stream directly). Bedrock accepts the beta via
additionalModelRequestFields.anthropic_beta, so it is now forwarded.

Second, the streaming reads re-chunked the AWS event stream with
iter_bytes(chunk_size=1024). httpx's ByteChunker only releases full 1024-byte
blocks, so the small early events (messageStart, contentBlockStart, first
deltas) sat in the buffer until enough bytes accumulated, pushing
time-to-first-byte from ~1.4s to ~8.5s on buffered tool-use streams. The
default is now no re-chunking; an explicit stream_chunk_size is still honored.

* test(bedrock): cover explicit stream_chunk_size on sync invoke path

* test(bedrock): cover stream_chunk_size plumbing through converse completion

* test(bedrock): cover stream_chunk_size default in legacy BedrockLLM streaming

* test(bedrock): merge converse handler tests into existing mapped test file

pytest imports test modules by basename in non-package test dirs, so the new
tests/test_litellm/llms/bedrock/chat/test_converse_handler.py collided with
the pre-existing tests/test_litellm/llms/chat/test_converse_handler.py and
broke collection in CI. Move the new tests into the existing file

* feat(otel): emit v2 cost breakdown + stamp tracer scope version (#30156)

Read the StandardLoggingPayload cost_breakdown into a typed LLMCost on
LLMCallSpanData and emit each component under litellm.cost.* (absent
components omitted, so spans stay sparse). Stamp litellm.__version__ as
the instrumentation scope version so every v2 span carries a
deterministic scope.version.

Tests under tests/test_litellm/integrations/otel/.

* fix(proxy): cancel in-flight upstream LLM request on client disconnect (opt-in) (#30223)

* fix(proxy): cancel in-flight upstream LLM request on client disconnect (opt-in)

On the non-streaming path, base_process_llm_request awaited the LLM call
with no disconnect monitoring; when the HTTP client went away the
upstream request kept running until completion or request_timeout (6000s
default), holding a backend slot (e.g. a vLLM GPU slot) for output
nobody would read

Add an opt-in general_settings.cancel_on_disconnect flag, default off,
so the default code path is unchanged. When enabled, a receive-based
watcher task observes http.disconnect and cancels the asyncio.gather
driving the upstream call. The resulting CancelledError is converted to
HTTPException 499 only when the disconnect event is set, so
server-initiated cancellations still propagate as-is. The 499 then flows
through _handle_llm_api_exception like any other failure, meaning
post_call_failure_hook still releases max_parallel_requests slots and
fires spend and alerting callbacks; it is logged at info level instead
of a full traceback

Also removes the dead check_request_disconnection helper in
proxy_server.py (zero call sites) along with its behavior-pin tests

Builds on the receive-based design from #25776

Addresses #13774. Re-fixes #22805 (regressed after the #14295 revert)

Co-authored-by: CreateRandom <18438707+CreateRandom@users.noreply.github.com>

* fix(proxy): scope 499 quiet logging to disconnects and harden watcher

Address the two P2 findings from the Greptile review on #30223. The
info-level logging in _log_llm_api_exception now applies only to the
disconnect-specific HTTPException (status 499 plus the shared
_CLIENT_DISCONNECT_DETAIL message), so any other 499 raised by hooks or
guardrails keeps its full traceback. The disconnect watcher now catches
exceptions from request.receive() (e.g. a transport reset) and logs a
warning instead of dying silently, making the degradation to no-op
visible; a test pins that the LLM call is not cancelled in that case

---------

Co-authored-by: kursad <kursad.lacin@brado.net>
Co-authored-by: CreateRandom <18438707+CreateRandom@users.noreply.github.com>

* fix(bedrock): grant aws-external-anthropic:* in OIDC session policy for claude_platform (#30200) (#30205)

The inline STS session policy passed to assume_role_with_web_identity
acts as an IAM PERMISSION CEILING — effective permissions are the
intersection of the role's identity policies and this policy. Any
action not listed is silently denied even when the IAM role grants it.

#27678 added the bedrock/claude_platform/<model> route but its
service-side action namespace is aws-external-anthropic:*, not
bedrock:*. Without a matching statement here, every claude_platform
request via OIDC (GCP federation, EKS Pod Identity webhook, etc.) 403s
with 'no session policy allows the aws-external-anthropic:CreateInference
action' — even with a fully permissive identity policy.

Add a second ClaudePlatformLiteLLM statement covering CreateInference,
CreateBatchInference, CancelBatchInference, DeleteBatchInference,
CountTokens, Get*, List*. Keep aws:SecureTransport=true parity with the
bedrock statement.

Static creds + IRSA flow through different code paths and are not
affected.

Fixes #30200

* fix(proxy): set Retry-After header on RouterRateLimitError 429 responses (#30098)

* Set Retry-After header on RouterRateLimitError responses

When all deployments for a model are in cooldown, the proxy returns a
429 whose cooldown timing is only available by parsing the error
message string. RouterRateLimitError already carries cooldown_time, so
expose it as a standard retry-after header in
_handle_llm_api_exception. The value is rounded up so clients never
retry before the cooldown window ends.

Fixes #27823.

* Set Retry-After after response-headers hook so cooldown wins

The cooldown-derived retry-after was assigned before the
post_call_response_headers_hook merge, so a callback returning a
retry-after key (including a stale or empty value) silently clobbered
it. Move the RouterRateLimitError block after the callback merge so the
cooldown value is authoritative for this error type.

* fix(router): route aspeech through async_function_with_fallbacks (#30104)

* fix(router): route aspeech through async_function_with_fallbacks

Router.aspeech selected a deployment and awaited litellm.aspeech
directly, so TTS requests got no retry on failure and no failover to
backup deployments; the except block only fired an exception alert and
re-raised. Every other router endpoint (acompletion, aembedding,
atranscription, arerank) already delegates to
async_function_with_fallbacks

Mirror the atranscription pattern: move deployment selection and the
litellm.aspeech call into a private _aspeech method, then have the
public aspeech set kwargs["original_function"] = self._aspeech and
await self.async_function_with_fallbacks(**kwargs). _aspeech also picks
up the shared _get_async_openai_model_client helper and the same
total/success/fail call accounting the sibling endpoints use

Fixes #27778.

* fix(router): apply deployment kwargs and rpm semaphore in _aspeech

Bring _aspeech fully in line with _atranscription: call
_update_kwargs_with_deployment so deployment metadata, model_info,
timeout, and default litellm params flow into the request, and wrap
the litellm.aspeech call with the max_parallel_requests semaphore plus
async_routing_strategy_pre_call_checks so TTS respects rpm limits the
same way the other router endpoints do

Also add a unit test that exercises _aspeech directly and asserts the
deployment metadata reaches the underlying call

* fix(slack_alerting): stop false-positive hanging request alerts for requests below the alerting threshold (#30106)

* fix(slack_alerting): skip hanging request alerts below the threshold

The hanging request check alerted on any cached request whose
completion status was not yet recorded, with no minimum age check.
Since the background loop runs every alerting_threshold / 2 seconds,
any request that happened to be in flight at a check fired a
"hanging - Ns+ request time" alert even if it was only seconds old,
producing a steady stream of false positives.

Add a created_at timestamp to HangingRequestData, stamped when the
request enters the hanging request cache, and skip requests younger
than alerting_threshold without evicting them, so a later check can
still alert if they never complete. Extend the cache TTL from
threshold + 60s to 1.5x threshold + 60s; with the age check, entries
only become alertable after threshold seconds, and the check period
is threshold / 2, so the old TTL could evict a genuinely hanging
request before any check saw it cross the threshold.

Fixes #27855.

* fix(slack_alerting): alert once per hanging request

The min-age gate stops false positives for young in-flight requests, but
a genuinely hanging request still re-alerted on every checker tick within
the cache TTL. With the wider TTL (1.5x threshold + 60s) that is 1-2 extra
Slack notifications per stuck request at the default 600s threshold.

Flag a HangingRequestData entry as alerted once its alert fires and skip
flagged entries on later ticks, so each hang produces exactly one alert.
The cache reference is mutated in place, so the TTL is untouched and still
handles cleanup. Adds a regression test asserting one alert across multiple
ticks.

Fixes #27855.

* fix(health): treat all-proxy-models keys as unrestricted in /health (#30087)

* fix(health): treat all-proxy-models keys as unrestricted in /health

A key granted all model permissions stores the literal
"all-proxy-models" marker in its models list. The /health access
filter compared that marker against real model_names, so the model
list filtered down to nothing and the WebUI health check returned
healthy_count=0, unhealthy_count=0 with HTTP 503. Skip the filter
(both the live path and the background-cache model_id scoping) when
the marker is present, matching how auth_checks treats
SpecialModelNames.all_proxy_models.

Fixes #29744.

* fix(health): resolve all-team-models sentinel to the team allowlist

Same failure shape as the all-proxy-models case: a key carrying the
literal "all-team-models" entry matches no real model_name, so the
/health access filter would zero out the model list. Resolve the
sentinel to the key's team models when team_id is set, matching
get_key_models in model_checks.py. Without a team_id the sentinel
stays unresolved and matches nothing, denying rather than widening
access, mirroring _resolve_key_models_for_auth_check.

* feat(proxy): auto-enable drop_params for Claude Code requests (#30218)

* feat(proxy): auto-enable drop_params for Claude Code requests

Claude Code identifies itself with a claude-cli/<version> user agent and
sends Anthropic-specific params (top_k, thinking, etc.) on every request.
When the proxy routes those requests to a non-Anthropic provider, the
unsupported params fail the call unless drop_params is configured. Detect
the Claude Code user agent in add_litellm_data_to_request and default
drop_params to true for those requests, without overriding an explicit
drop_params value sent by the caller.

* feat(proxy): respect operator litellm_settings drop_params over Claude Code default

An explicit drop_params in the operator's litellm_settings (true or false)
now suppresses the Claude Code user agent default, so an operator who
deliberately configured drop_params: false keeps strict param validation
for Claude Code clients too. The auto-default only fills the gap when
neither the request body nor the config sets a value.

* fix(snowflake): migrate to native endpoints with auto-routing for Claude models (#29964)

* fix(snowflake): migrate to native Cortex REST API endpoints

Replaces the legacy /api/v2/cortex/inference:complete endpoint with the
native OpenAI-compatible /api/v2/cortex/v1/chat/completions endpoint,
fixing error 390142 (Incoming request does not contain a valid payload)
when using model: snowflake/<model> in LiteLLM proxy.

Changes:
- litellm/llms/snowflake/chat/transformation.py: route to native
  /cortex/v1/chat/completions, remove Snowflake-specific tool_spec
  payload transformation, remove content_list response handling,
  add stream to supported params
- litellm/llms/snowflake/anthropic/transformation.py (new):
  SnowflakeCortexAnthropicConfig routes Claude models to /cortex/v1/messages
  with anthropic-version header and Anthropic->OpenAI response transform
- tests: 29 unit tests covering URL routing, auth headers, payload
  format, and response parsing

* fix(snowflake): map max_tokens to max_completion_tokens for native endpoint

* fix: handle multi-turn tool conversations and OpenAI→Anthropic tool format conversion

- _extract_system_and_messages now preserves tool_calls from assistant messages
  and converts them to Anthropic tool_use content blocks
- tool role messages are converted to user role with tool_result content blocks
  (as required by Anthropic Messages API)
- Added _transform_tools_to_anthropic() to convert OpenAI tool format
  (type/function/parameters) to Anthropic format (name/input_schema)
- Added comprehensive tests for multi-turn tool conversations

Addresses review feedback on PR #29964

* test: add coverage for malformed JSON and non-string tool arguments

* fix(tests): update chat transformation tests for native OpenAI-compatible endpoint

* style: apply black formatting

* fix: resolve mypy type errors in anthropic transformation

* fix: correct mypy type: ignore error codes (attr-defined)

* fix: use max_tokens instead of max_completion_tokens for Snowflake endpoint compatibility

* refactor: merge Anthropic config into unified SnowflakeConfig with auto-routing

- Remove separate SnowflakeCortexAnthropicConfig and anthropic/ directory
- SnowflakeConfig now auto-routes based on model name:
  - Claude models → /messages endpoint (Anthropic format)
  - All others → /chat/completions endpoint (OpenAI format)
- No new provider needed (stays as SNOWFLAKE = 'snowflake')
- Tool message transformation for Claude: tool_calls → tool_use blocks,
  tool role → user with tool_result
- OpenAI → Anthropic tool format conversion (parameters → input_schema)
- Addresses Greptile feedback about unwired SnowflakeCortexAnthropicConfig

* fix: use max_completion_tokens for /chat/completions (Snowflake deprecated max_tokens on this endpoint)

* fix(tests): update assertions for Claude auto-routing to /messages endpoint

* fix(snowflake): add tool_choice conversion and preserve max_completion_tokens in Anthropic path

* fix(snowflake): use ChatCompletionMessageToolCall objects and strip model prefix on OpenAI path

* fix(snowflake): collect multiple system messages to prevent guardrail override

* chore: remove committed .pyc files and add __pycache__ to .gitignore

* fix: remove unused Union import

* fix: restore original .gitignore (accidentally replaced in earlier commit)

* feat(snowflake): add streaming response handler for both Anthropic and OpenAI SSE formats

* fix: remove unused AsyncIterator and Iterator imports

* fix: add missing total_tokens to ChatCompletionUsageBlock

* fix(snowflake): coalesce consecutive tool results into single user message for Anthropic

* fix(snowflake): handle message_start event for streaming input_tokens tracking

* fix: evict last deleted model in multi-instance deployments (#28608)

* fix: evict last deleted model in multi-instance deployments

_delete_deployment had an early return when db_models was empty,
preventing eviction of the last deleted model during reconciliation.

- Remove len(db_models)==0 early return from _delete_deployment
- Return None (not []) from _get_models_from_db on DB failure so
  callers can distinguish a transient failure from a genuinely empty DB
- Guard _update_llm_router against None to skip updates on DB failure

Fixes #28443

* test: remove dead MagicMock assignment in type_mismatch test

* fix: update test to pass [] not None to _update_llm_router

test_ProxyConfig__update_llm_router_bad_proxy_logging_raises was passing
None as new_models to get through to the proxy_logging_obj check, but
the None guard we added now returns early before reaching that path.
Pass [] instead so the test exercises the intended AttributeError case.

Signed-off-by: Rudra Dudhat <contact.rdudhat@gmail.com>

* chore: regenerate API types to sync schema.d.ts with proxy OpenAPI spec

Signed-off-by: Rudra Dudhat <contact.rdudhat@gmail.com>

---------

Signed-off-by: Rudra Dudhat <contact.rdudhat@gmail.com>

* fix: invalidate Redis spend counter on /key/reset_spend (#29694)

* fix: set Redis spend counter to reset_to value on /key/reset_spend

Previously, the Redis spend counter was always set to 0.0 after a reset,
even when reset_to was a non-zero value (partial reset). This caused
the budget to be under-enforced for up to 60 seconds until the counter
expired and fell through to the DB.

Now the counter is set to the actual reset_to value, so partial resets
are reflected correctly and budget enforcement is consistent.

* test: update reset_key_spend test to match direct cache set

The implementation now sets spend_counter_cache directly instead of
calling _invalidate_spend_counter. Update the test to verify the
in_memory_cache.set_cache call with the correct key, value, and ttl.

---------

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

* fix: add scaleway models pricing (#27659)

* fix: Add embeddings support for Scaleway provider

* fix: resolve merge conflicts

* fix(main): clarify backend route handling for Swagger static assets (#30196)

* fix(main): clarify backend route handling for Swagger static assets

* fix(allowlist): add BACKEND_MOUNT_PATHS for Swagger static assets

* fix(voyage): route multimodal embeddings to correct endpoint (#30193)

* fix(voyage): route multimodal embeddings to correct endpoint

* test(voyage): cover multimodal embedding edge cases

* test(voyage): cover api key fallback

* fix(voyage): raise early on missing api key and malformed image url

* test(voyage): cover utils routing and helper

* fix(voyage): route supported openai params for multimodal models

* style: apply black formatting

* fix(ui): infer Azure API version from API base (#30204)

* fix(ui): infer Azure API version from API base

* fix(ui): address Azure API version feedback

* Update litellm/llms/snowflake/chat/transformation.py

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

* feat(datadog): add team-scoped Datadog callback support (#29947)

Enable teams to configure their own Datadog credentials via
POST /team/{team_id}/callback, following the same pattern as Langfuse.

* Merge pull request #29528 from aanchal22/litellm_byok-alias-merge

fix(proxy): atomic merge for team model aliases and team.models on BYOK create

* feat: add EmpirioLabs as an OpenAI-compatible provider (#30278)

Co-authored-by: Adam Dalloul <adam.d.developer@gmail.com>

* fix: resolve failing tests and lint in snowflake/team endpoints

- Black-format snowflake/chat/transformation.py to fix lint failure
- Update Anthropic config test to expect default max_tokens of 4096 (matches implementation)
- Add AsyncMock + execute_raw mock to team_model_add cache-refresh pin test
- Add model_dump mock and patch cache/logging in test_uses_atomic_array_append_with_dedup

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

* fix(test): update test_db_error_new_model_check for new _delete_deployment logic

_delete_deployment no longer short-circuits on empty db_models — it now
treats [] as a valid empty-DB state and proceeds to check config models.
Mock get_config to return the two router deployments so they appear in
combined_id_list and are protected, which matches the real-world scenario
where a DB error occurs but the models are config-backed.

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

* feat(proxy): register cancel_on_disconnect in ConfigGeneralSettings and config list (#30295)

* feat(proxy): register cancel_on_disconnect in ConfigGeneralSettings and config list

Follow-up to #30223 per maintainer review: documents the flag in
ConfigGeneralSettings with a short description and adds it to
allowed_args in get_config_list so the UI and /config/list expose it.
A test pins that /config/list returns the field with type Boolean,
which requires both registrations to be present

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

---------

Co-authored-by: kursad <kursad.lacin@brado.net>

* fix(datadog): never fall back to env DD_API_KEY for caller-supplied destinations

Team/key-scoped Datadog loggers could be pointed at an arbitrary dd_agent_host or
dd_site while omitting dd_api_key, causing the proxy's global DD_API_KEY to be sent
as the DD-API-KEY header to that destination. Gate the env-var fallback behind an
allow_env_credentials flag, set to False when the destination is caller-supplied,
mirroring the existing langfuse/langsmith pattern.

---------

Signed-off-by: Rudra Dudhat <contact.rdudhat@gmail.com>
Co-authored-by: Emerson Gomes <emerson.gomes@thalesgroup.com>
Co-authored-by: daitran-tensormesh <dai@tensormesh.ai>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Muspi Merol <me@promplate.dev>
Co-authored-by: fangkang <fangkangm@gmail.com>
Co-authored-by: Chris Hoogeboom <chris.hoogeboom@gmail.com>
Co-authored-by: kursadlacin <kursadlacin@gmail.com>
Co-authored-by: kursad <kursad.lacin@brado.net>
Co-authored-by: CreateRandom <18438707+CreateRandom@users.noreply.github.com>
Co-authored-by: hcl <chenglunhu@gmail.com>
Co-authored-by: Filippo Menghi <113345637+Cyberfilo@users.noreply.github.com>
Co-authored-by: Mateo Wang <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: sfc-gh-nashukla <navnit.shukla@snowflake.com>
Co-authored-by: Rudra Dudhat <contact.rdudhat@gmail.com>
Co-authored-by: Michael <52305679+michaelxer@users.noreply.github.com>
Co-authored-by: michaelxer <michaelxer@users.noreply.github.com>
Co-authored-by: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com>
Co-authored-by: mauriceberentsen <mauriceberentsen@live.nl>
Co-authored-by: lost9999 <56498264+lost9999@users.noreply.github.com>
Co-authored-by: GaetanVDB07 <86427581+GaetanVDB07@users.noreply.github.com>
Co-authored-by: Aanchal Khandelwal <aan2210khandelwal@gmail.com>
Co-authored-by: Adam Dalloul <adam_dalloul@icloud.com>
Co-authored-by: Adam Dalloul <adam.d.developer@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 09:49:25 -07:00
Sameer Kankute
7d1f68e72a
fix(proxy): populate access_via_team_ids on /v1/model/info (#30274)
* fix(proxy): populate access_via_team_ids on /v1/model/info

Team metadata enrichment previously only ran on /v2/model/info with
include_team_models=true, leaving /v1/model/info without
access_via_team_ids for project model-picker flows.

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

* docs(dashboard): sync OpenAPI schema for /v1/model/info query params

Add include_team_models and teamId to the generated schema for /model/info
and /v1/model/info after the proxy endpoint gained team-access filtering.

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

* fix(proxy): always return direct_access on /v1/model/info

Set direct_access to true or false on every enriched model so clients
can filter without treating a missing field as ambiguous.

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

* perf(proxy): fail fast when teamId is set without a connected DB on /v1/model/info

Raise the db_not_connected error before building, enriching, and translating the model list instead of after, so a teamId query against a proxy with no database no longer wastes the full enrichment pipeline.

* fix(proxy): fail fast when include_team_models is set without a database

include_team_models=True relies on _populate_team_access_on_models to set
direct_access/access_via_team_ids, which only runs when a database is connected.
Without one, _filter_models_to_user_accessible discarded every model and the
endpoint returned an empty list with HTTP 200. Mirror the teamId guard so the
request fails fast with a clear db_not_connected error before any model-list work.

* fix(proxy): populate direct_access on single-model /model/info lookup

The /v1/model/info list path populates model_info.direct_access (and
access_via_team_ids) when a database is connected, but the
litellm_model_id single-model lookup returned early without it. This
made the two endpoints disagree, breaking the parity assertion in
test_get_specific_model. Run the same population on the single-model
path so both responses match.

* fix(proxy): apply no-DB fast-fail before litellm_model_id branch

The teamId/include_team_models no-DB guard sat after the litellm_model_id
early return, so ?litellm_model_id=X&teamId=Y with no DB returned 200 with
unpopulated access fields instead of the 500 raised on every other path.
Move the guard ahead of the branch so the fast-fail is uniform.

* fix(proxy): apply teamId/include_team_models filters on single-model lookup

The litellm_model_id early-return branch in model_info_v1 populated the
team access fields but returned before the teamId and include_team_models
filters ran, so a single-model lookup surfaced the deployment regardless
of team access when the DB was connected. Run both filters on the
single-model list before returning so the documented query params behave
the same with and without litellm_model_id.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-06-12 09:49:09 -07:00
Sameer Kankute
729b005e4e
fix(google_genai): preserve complete SSE events in Vertex/Gemini image streaming (#30270)
* fix(google_genai): preserve complete SSE events in image streaming

Use iter_lines/aiter_lines instead of byte chunking so large inlineData
base64 payloads from Vertex/Gemini streamGenerateContent are not split
across events, which caused truncated JSON and SDK parse failures.

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

* fix(google_genai): buffer SSE lines until event delimiter

Assemble multi-field SSE events on blank-line boundaries instead of
terminating each field line individually.

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

* fix(tests): update google_ai_studio mocks from aiter_bytes to aiter_lines

Streaming iterator was changed to use iter_lines/aiter_lines instead of
iter_bytes/aiter_bytes. Update the two mocked streaming responses in
test_google_ai_studio.py to match.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 07:49:30 -07:00
Sameer Kankute
e8e5b47fc2
feat(passthrough): add configurable pass-through request timeouts (#30266)
* feat(passthrough): add configurable pass-through request timeouts

Allow operators to set general_settings.pass_through_request_timeout and per-endpoint timeout values, and apply them to native HTTP passthrough routes and SDK passthrough paths such as Bedrock /converse.

* fix(passthrough): address CI lint and regenerate dashboard API types

* refactor(passthrough): extract timeout utils to proxy-free module, fix router_timeout drop

- Move resolve_llm_passthrough_timeout + resolve_pass_through_request_timeout to
  litellm/passthrough/timeout_utils.py (no fastapi/proxy imports at module scope)
- router.py and passthrough/main.py now import from timeout_utils directly,
  avoiding the fastapi transitive import in pure SDK contexts
- pass_through_endpoints.py re-imports from timeout_utils for backward compat
- resolve_llm_passthrough_timeout now accepts router_timeout so Router(timeout=X)
  is respected for passthrough calls instead of being silently dropped
- Use _get_httpx_client (cached) instead of bare HTTPHandler(...) in sync path
  to avoid creating an unclosed client per call

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

* fix(router): use _explicit_timeout for passthrough to not shadow general_settings

self.timeout defaults to litellm.request_timeout (6000s) when the user
doesn't pass timeout= to Router(). Using it as router_timeout caused
general_settings.pass_through_request_timeout to be silently ignored.

Only pass router_timeout when the user explicitly set Router(timeout=X).

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

* fix(timeout_utils): avoid fastapi transitive import by using sys.modules

resolve_pass_through_request_timeout previously did a lazy
`from litellm.proxy.proxy_server import general_settings` which loads
the proxy module (and transitively fastapi) even in pure SDK contexts.

Replace with a sys.modules lookup: if the proxy module is already loaded
(i.e. we're inside the proxy), read general_settings from it; otherwise
skip and fall back to the 600s default. No import is triggered.

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

* fix(lint): remove unused imports from pass_through_endpoints.py

DEFAULT_PASS_THROUGH_REQUEST_TIMEOUT_SECONDS and resolve_llm_passthrough_timeout
are not used in this file; only resolve_pass_through_request_timeout is.

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

* fix(pass_through_endpoints): re-export DEFAULT_PASS_THROUGH_REQUEST_TIMEOUT_SECONDS

Tests import this constant directly from pass_through_endpoints.py;
re-add it to the import from timeout_utils for backward compatibility.

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

* fix(pass_through_endpoints): re-export resolve_llm_passthrough_timeout for backward compat

Tests import both DEFAULT_PASS_THROUGH_REQUEST_TIMEOUT_SECONDS and
resolve_llm_passthrough_timeout from pass_through_endpoints.py; use
noqa comments to suppress the unused-import lint warning on re-exports.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 07:40:02 -07:00
Sameer Kankute
ccc20b121f
fix(responses): presidio PII masking for Azure WebSocket and streaming (#30003)
* fix(responses): Presidio PII masking for Azure WebSocket and streaming

Wire Presidio into native Responses WebSocket forwarding and fix streaming output unmasking so masked tokens are restored for HTTP and WS clients.

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

* Fix unused imports in responses handlers

* fix(responses): address Greptile review - Azure WebSocket model URL and PII logging

- Add model_in_websocket_url() to BaseResponsesAPIConfig (default True) so
  providers can opt out of ?model= being appended to WebSocket URLs.
- Override model_in_websocket_url() to return False for Azure, since Azure
  sends the model in the response.create body, not the URL query string.
- Use this flag in llm_http_handler to conditionally append ?model=.
- Pass masked message to _store_input() instead of the original PII-containing
  message so logging destinations do not receive unmasked PII.

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

* fix(responses): mask nested response.create input format for Presidio PII

Handle the nested {"type":"response.create","response":{"input":[...]}}
format in _mask_response_create. Previously only the flat top-level input
was masked; the nested shape bypassed Presidio and forwarded raw PII
upstream. Now both shapes are normalized and masked before forwarding.

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

* style: apply black formatting to llm_http_handler and streaming_iterator

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

* style: suppress PLR0915 on async_responses_websocket

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

* fix(responses): add apply_to_output masking on Responses API WebSocket path

Previously the WebSocket guardrail filter excluded callbacks with
apply_to_output=True, leaving model-generated PII unmasked before
returning to the client.

- Collect apply_to_output callbacks separately in llm_http_handler and
  pass them to ResponsesWebSocketStreaming as output_guardrail_callbacks.
- Add _mask_response_completed method that calls check_pii(output_parse_pii=False)
  on text blocks in response.completed events, masking model output PII.
- backend_to_client now chains unmask (pii_tokens) → mask (apply_to_output)
  before forwarding each event to the client.

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

* fix(responses): unmask PII tokens in streaming delta events and warn on guardrail init failure

- Rename _unmask_response_completed -> _unmask_response_event and extend
  it to also unmask response.output_text.delta (and other delta types)
  so real-time streaming clients receive original values, not PII tokens.
- Split the broad except-and-swallow into ImportError (expected in SDK-only
  environments) vs Exception (unexpected — now logs a warning so operators
  know masking is disabled).

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

* fix(responses): enforce authorized model on WebSocket frames and remove proxy import

Security: add _enforce_authorized_model to ResponsesWebSocketStreaming that
overwrites both flat and nested model fields in every response.create frame
with the connection-authorized model, preventing deployment-substitution
attacks where an authenticated user sends a different model name in the frame
body after connecting with an allowed model.

Layering: remove the _OPTIONAL_PresidioPIIMasking isinstance check and proxy
import from the SDK handler. Use duck-typed checks (callable check_pii +
get_presidio_settings_from_request_data) so any guardrail implementing the
interface works, not just Presidio.

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

* fix(responses): add _unmask_pii_text to duck-typed contract and mask delta frames

- Add callable(_unmask_pii_text) check to the guardrail_callbacks filter so
  a custom guardrail missing that method cannot cause an AttributeError and
  silently kill the WebSocket session.
- Extend _mask_response_completed to also mask response.output_text.delta
  (and other delta types) for apply_to_output callbacks, so real-time
  streaming clients do not receive unredacted model-generated PII in deltas.

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

* Fix Responses WebSocket guardrail edge cases

* fix(responses): log masked output and suppress deltas when apply_to_output active

- Move _store_event to after _mask_response_completed so logs receive the
  redacted form, not raw model output containing PII.
- Suppress delta event forwarding when output_guardrail_callbacks are
  present: per-fragment Presidio cannot catch PII that spans multiple
  chunks (e.g. "alice@" + "example.com"). Clients receive only the
  fully-masked response.completed, which Presidio scans on complete text.

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

* fix(responses): mask and suppress response.output_item.done for apply_to_output

response.output_item.done carries completed item text in item.content[*].text
before response.completed arrives, allowing unmasked PII to reach the client.

- _unmask_response_event: unmask input-PII tokens in item.content[*].text
- _mask_response_completed: run check_pii on item.content[*].text for
  apply_to_output callbacks (same as response.completed handling)
- backend_to_client suppression: also skip response.output_item.done when
  output_guardrail_callbacks are active; client receives only the
  fully-masked response.completed

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

* fix(types): cast response_obj to ResponsesAPIResponse to satisfy mypy

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

* Revert "fix(types): cast response_obj to ResponsesAPIResponse to satisfy mypy"

This reverts commit d5969557628f9aff58948b9d37cc64d577f95a15.

* Revert "fix(responses): mask and suppress response.output_item.done for apply_to_output"

This reverts commit 219fd54ea3446f4399fde40c07ba0617e2834573.

* fix(types): accept dict responses in guardrail output write-back

Streaming response.completed events pass a dict response object, so widen
_apply_guardrail_responses_to_output to match its existing runtime handling.

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

* perf(responses): skip Presidio masking on suppressed WebSocket delta events

Delta events are dropped wholesale when apply_to_output masking is active,
so masking them first issued a wasted check_pii call per fragment. Move the
suppression check ahead of the unmask/mask passes; the event type is
invariant across both, so client-visible behavior is unchanged.

* test(responses): cover Responses WebSocket PII masking hooks

Add regression tests for the native Responses WebSocket guardrail path:
input masking and model enforcement in _mask_response_create, token
unmasking in _unmask_response_event, apply_to_output masking and delta
suppression in _mask_response_completed/backend_to_client, and the
get_websocket_url / model_in_websocket_url defaults for the base and
Azure configs. Raises diff coverage above the codecov patch target.

* fix(responses): suppress text-bearing done events under output PII masking

When apply_to_output masking is active on a native Responses WebSocket,
response.output_text.done, response.content_part.done, and
response.output_item.done carry the full model output before the masked
response.completed arrives, so an authenticated client could read
unmasked PII from those events. Suppress them alongside delta events; the
client receives only the fully-masked response.completed.

* refactor(responses): drop dead delta branch in WebSocket output masking

Delta events are suppressed in backend_to_client before _mask_response_completed
runs when output masking is active, so the method's delta-handling branch was
unreachable. Restrict it to response.completed and cover the Responses API
unmask path with a Pydantic ResponseCompletedEvent regression test.

* fix(presidio): flush buffered chat chunks on mixed unmask stream

_stream_pii_unmasking buffered ModelResponseStream chunks but returned
early once a /v1/responses event was seen, silently dropping the buffered
chat chunks. Flush them in order before switching to passthrough, mirroring
_stream_apply_output_masking, and cover it with a regression test.

* fix(responses): mask instructions and tool-call arguments in WebSocket PII path

Presidio masking on the native Responses WebSocket path left two gaps. On the
request side _mask_response_create only walked the input containers, so PII
placed in the instructions field of a response.create frame was forwarded
upstream and logged unmasked even with output_parse_pii enabled. Now both the
flat and nested instructions strings are masked alongside input.

On the response side _mask_response_completed only masked content text blocks,
so model-produced PII inside function-call arguments could reach the client when
apply_to_output was enabled, both via the standalone
response.function_call_arguments.done event and via the function_call output
items in response.completed. The done event is now suppressed under output
masking and completed function-call arguments are run through check_pii before
forwarding or logging.

* fix(responses): suppress reasoning_summary_text.done under output PII masking

* fix(responses): mask function_call_output.output in WebSocket PII path

response.create input items of type function_call_output carry
user-controlled text in output, not content, so the Presidio masking
pass forwarded that text upstream unmasked. Mask the output field
(string or list of text blocks) alongside content.

* fix(responses): mask reasoning summary PII in WebSocket output path

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-06-12 07:27:03 -07:00