Repository navigation
fix(auth): stop the team fallback from widening model access - #36837
yassin-berriai merged 1 commit into
Conversation
|
|
Greptile SummaryThis PR hardens team-resolution behavior in centralized proxy authorization while preserving a configured degraded-service path.
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/proxy/auth/auth_checks.py | Introduces a distinct not-found exception so successful absence can be separated from lookup failures. |
| litellm/proxy/auth/user_api_key_auth.py | Restricts token-derived team fallback according to lookup outcome, token grant, and availability configuration. |
| tests/test_litellm/proxy/auth/test_auth_checks.py | Verifies that absent and unreadable team rows produce distinguishable exception types. |
| tests/test_litellm/proxy/auth/test_user_api_key_auth.py | Covers centralized authorization behavior for deleted teams, unreadable teams, and recorded model grants. |
| tests/proxy_unit_tests/test_user_api_key_auth.py | Updates existing fixtures to retain their intended non-fallback test paths under the stricter authorization behavior. |
Reviews (4): Last reviewed commit: "fix(auth): stop the team fallback from w..." | Re-trigger Greptile
fae2acf to
0e04e4b
Compare
|
@greptileai re-review at 0e04e4b, the verbose commentary finding is addressed and behavior is unchanged |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
0e04e4b to
7f2ae5e
Compare
|
@greptileai re-review at 7f2ae5e, adds two legacy fixture updates only; the source change is unchanged since your 5/5 |
When get_team_object fails, the centralized auth gate rebuilds the team from the token's own fields. A token whose team row was missing when the key was read carries team_models=[] and team_blocked=False, and the model-access check reads an empty model list as every model, so the rebuilt team grants more than the real team ever did. get_team_object reported a deleted team and a database that would not answer as the same 404, so the fallback could not tell a definitive answer from a degraded read. Raise a TeamNotFoundError subclass, still a 404 with the same detail so every other caller is unaffected, only when the database answers and the row is absent. A team that is provably gone now refuses, and no setting overrides that. Otherwise the grant is merely unknown: a token carrying one may vouch, since replaying a recorded grant cannot widen it, and a token carrying none may not. allow_requests_on_db_unavailable still opts back out there, and is only consulted once the failure is known to be a degraded read.
7f2ae5e to
a45fbba
Compare
|
@greptileai re-review at a45fbba, the availability opt-out no longer overrides a definitively deleted team; see Review notes |
ab2333b
into
litellm_internal_staging
上游 PR BerriAI#36837 新增 TeamNotFoundError 区分"确实不存在"与"读不到",前者不再允许 token 自带字段兜底。但 litellm-dashboard 是设计上永远没有 DB 行的保留 sentinel (/team/new 显式拒绝创建),于是查库查不到对它是常态却被等同于团队已删除。 崩点在 _run_centralized_common_checks,那是所有路由的唯一鉴权关口,导致 Admin UI 整站数据加载不出来(API 模型调用不受影响)。 改为对该 sentinel 直接放行,等价于把这条路径退回 1.95.0 的旧行为,只对这一个 id 生效,不重新引入上游要堵的权限放大漏洞。 回归测试走完整的 _run_centralized_common_checks 路径,并断言重建的 team_object 真的传给了 common_checks —— 只断言不抛异常的话,返回 None 也会通过。已验证撤掉 补丁该测试失败、装上通过。
…efusal Every Admin UI session key is stamped with team_id=litellm-dashboard, a reserved sentinel that /team/new refuses to create, so resolving it against the database can only fail. The absent-versus-unreadable distinction #36837 added read that as a deleted team and 404'd every dashboard request. Resolve the sentinel from the token instead of asking for a row that will never exist. The reserved id alone does not earn the exemption. A JWT names its own team, so a claim could otherwise ask for a synthesized team whose empty models reads as every model, which widens rather than merely bypasses. jwt_claims is a required dict on every JWT path, so the identity must carry none, and it must carry a token that a standard JWT identity does not have. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…efusal Every Admin UI session key is stamped with team_id=litellm-dashboard, a reserved sentinel that /team/new refuses to create, so resolving it against the database can only fail. The absent-versus-unreadable distinction #36837 added read that as a deleted team and 404'd every dashboard request. Resolve the sentinel from the token instead of asking for a row that will never exist. The reserved id alone does not earn the exemption. The synthesized team's empty models reads as every model, so an identity that merely names the sentinel is widened rather than waved through, and several auth paths take team_id straight from data the proxy did not mint: JWT claims, an OAuth2 introspection response, a custom auth handler's return value. Rather than enumerate the producers to exclude, require proof of where the credential came from. A database-minted session key is already marked as a virtual key. The EXPERIMENTAL_UI_LOGIN blob has no key row, and a proxy-admin one returns before the virtual-key paths, so mark it where it is decrypted with the proxy's own ui_hash_key. Both markers are stripped from validated input, so no claim, header or handler return can carry one in. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…efusal Every Admin UI session key is stamped with team_id=litellm-dashboard, a reserved sentinel that /team/new refuses to create, so resolving it against the database can only fail. The absent-versus-unreadable distinction #36837 added read that as a deleted team and 404'd every dashboard request. Resolve the sentinel from the token instead of asking for a row that will never exist. The reserved id alone does not earn the exemption. The synthesized team's empty models reads as every model, so an identity that merely names the sentinel is widened rather than waved through, and several auth paths take team_id straight from data the proxy did not mint: JWT claims, an OAuth2 introspection response, a custom auth handler's return value. Rather than enumerate the producers to exclude, require proof of where the credential came from. A database-minted session key is already marked as a virtual key. The EXPERIMENTAL_UI_LOGIN blob has no key row, and a proxy-admin one returns before the virtual-key paths, so mark it where it is decrypted with the proxy's own ui_hash_key. Both markers are stripped from validated input, so no claim, header or handler return can carry one in. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ssion Revert "fix(auth): stop the team fallback from widening model access" (#36837)
…efusal Every Admin UI session key is stamped with team_id=litellm-dashboard, a reserved sentinel that /team/new refuses to create, so resolving it against the database can only fail. The absent-versus-unreadable distinction #36837 added read that as a deleted team and 404'd every dashboard request. Resolve the sentinel from the token instead of asking for a row that will never exist. The reserved id alone does not earn the exemption. The synthesized team's empty models reads as every model, so an identity that merely names the sentinel is widened rather than waved through, and several auth paths take team_id straight from data the proxy did not mint: JWT claims, an OAuth2 introspection response, a custom auth handler's return value. Rather than enumerate the producers to exclude, require proof of where the credential came from. A database-minted session key is already marked as a virtual key. The EXPERIMENTAL_UI_LOGIN blob has no key row, and a proxy-admin one returns before the virtual-key paths, so mark it where it is decrypted with the proxy's own ui_hash_key. Both markers are stripped from validated input, so no claim, header or handler return can carry one in. That leaves the id itself, which /team/new reserves but key creation did not, so a proxy admin could put an ordinary key on the sentinel team and it would inherit the exemption along with the skip of its owner's user-level model check. Reserve the id on the key create and update paths too. The UI mints its session key through generate_key_helper_fn directly, so it never passes through either one. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…erriAI#36837)" This reverts commit ab2333b. Every Admin UI login mints its session key against the sentinel team_id `litellm-dashboard`, and no LiteLLM_TeamTable row is ever created for it. That lookup is therefore a provably-absent row on every UI request, which BerriAI#36837 turned into a hard refusal with no override, so the whole dashboard 404s. Reverting restores the token-derived fallback. The model-access widening BerriAI#36837 closed is reopened and needs a re-land that exempts the UI sentinel team.
…8.0) (#393)
This PR contains the following updates:
| Package | Update | Change |
|---|---|---|
| [ghcr.io/berriai/litellm](https://images.chainguard.dev/directory/image/wolfi-base/overview) ([source](https://github.com/BerriAI/litellm)) | minor | `v1.97.0` → `v1.98.0` |
---
### Release Notes
<details>
<summary>BerriAI/litellm (ghcr.io/berriai/litellm)</summary>
### [`v1.98.0`](https://github.com/BerriAI/litellm/releases/tag/v1.98.0)
[Compare Source](https://github.com/BerriAI/litellm/compare/v1.98.0...v1.98.0)
##### Verify Docker Image Signature
All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](https://github.com/BerriAI/litellm/commit/0112e53046018d726492c814b3644b7d376029d0).
**Verify using the pinned commit hash (recommended):**
A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:
```bash
cosign verify \
--key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
ghcr.io/berriai/litellm:v1.98.0
```
**Verify using the release tag (convenience):**
Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:
```bash
cosign verify \
--key https://raw.githubusercontent.com/BerriAI/litellm/v1.98.0/cosign.pub \
ghcr.io/berriai/litellm:v1.98.0
```
Expected output:
```
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the specified public key
```
***
##### What's Changed
- fix(bedrock): drop toolSpec.strict for Claude Sonnet 5 on Converse by [@​kr0k](https://github.com/kr0k) in [#​33196](https://github.com/BerriAI/litellm/pull/33196)
- fix(batches): attribute Vertex passthrough batch cost to key/team/tags by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​34456](https://github.com/BerriAI/litellm/pull/34456)
- docs: rewrite the CLAUDE.md comment rule with explicit exceptions by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36301](https://github.com/BerriAI/litellm/pull/36301)
- fix(proxy): scope file list pagination cursors to the caller by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36093](https://github.com/BerriAI/litellm/pull/36093)
- fix(proxy): skip prisma-dependent hooks when no database is attached by [@​mateo-berri](https://github.com/mateo-berri) in [#​36273](https://github.com/BerriAI/litellm/pull/36273)
- fix(proxy): report has\_more false on caller-scoped file list pages by [@​mateo-berri](https://github.com/mateo-berri) in [#​36326](https://github.com/BerriAI/litellm/pull/36326)
- fix(proxy): restore management\_v1 query-param validation under fastapi>=0.140.7 by [@​HuanQian571](https://github.com/HuanQian571) in [#​35773](https://github.com/BerriAI/litellm/pull/35773)
- fix(proxy): stop /{provider}/v1/files from capturing /openai\_passthrough by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36092](https://github.com/BerriAI/litellm/pull/36092)
- chore(typing): remove 914 basedpyright Any errors across 16 hotspot files by [@​mateo-berri](https://github.com/mateo-berri) in [#​36386](https://github.com/BerriAI/litellm/pull/36386)
- fix(router): keep batch fallbacks inside the model group that owns the file by [@​mateo-berri](https://github.com/mateo-berri) in [#​36181](https://github.com/BerriAI/litellm/pull/36181)
- feat(ptu): configure provisioned-throughput flat cost on a model deployment by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​35341](https://github.com/BerriAI/litellm/pull/35341)
- docs: clarify the CLAUDE.md comment exceptions are any-of by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36421](https://github.com/BerriAI/litellm/pull/36421)
- docs: replace the Changes PR template section with Caveats by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36423](https://github.com/BerriAI/litellm/pull/36423)
- fix(bedrock): enable native structured output for GLM 5 and DeepSeek V3.2 by [@​alexshtf](https://github.com/alexshtf) in [#​35669](https://github.com/BerriAI/litellm/pull/35669)
- feat(ptu): daily rollup writes per-model PTU flat cost by active hour by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​35343](https://github.com/BerriAI/litellm/pull/35343)
- feat(logging): add opt-in session\_id and trace\_id correlation to JSON log records via contextvars by [@​deepanshululla](https://github.com/deepanshululla) in [#​34418](https://github.com/BerriAI/litellm/pull/34418)
- feat(ptu): surface PTU flat cost on the daily activity read path by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​35391](https://github.com/BerriAI/litellm/pull/35391)
- feat(router): add per-deployment allowed\_fails\_policy and cooldown\_time override support by [@​deepanshululla](https://github.com/deepanshululla) in [#​34416](https://github.com/BerriAI/litellm/pull/34416)
- feat(ptu): add PTU inputs to the model form and flat cost to the Usage page by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​35393](https://github.com/BerriAI/litellm/pull/35393)
- fix(cost): price dict-shaped image input token details at the image rate by [@​vairodp](https://github.com/vairodp) in [#​33490](https://github.com/BerriAI/litellm/pull/33490)
- fix(model\_prices): refresh deprecation dates, correct xAI pricing and add missing provider models by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36403](https://github.com/BerriAI/litellm/pull/36403)
- feat(ptu): gate PTU flat-cost attribution behind an opt-in env var by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​36138](https://github.com/BerriAI/litellm/pull/36138)
- ci: cache Prisma CLI and engine binaries, split test timeout from setup by [@​mateo-berri](https://github.com/mateo-berri) in [#​36417](https://github.com/BerriAI/litellm/pull/36417)
- feat(rate limiting): configurable estimated output tokens per key, team and model by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36143](https://github.com/BerriAI/litellm/pull/36143)
- fix(ui): hide admin-only Logs tabs from roles that cannot call their endpoints by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36333](https://github.com/BerriAI/litellm/pull/36333)
- test(proxy): guard management\_v1 against fastapi names removed in supported releases by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36336](https://github.com/BerriAI/litellm/pull/36336)
- fix(ui): gate policy and prompt lookups on an admin capability by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36335](https://github.com/BerriAI/litellm/pull/36335)
- build(deps): bump pypdf to 6.15.0 to clear osv-scan by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36350](https://github.com/BerriAI/litellm/pull/36350)
- fix(proxy): isolate guardrail load failures per row by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​36432](https://github.com/BerriAI/litellm/pull/36432)
- fix(ui): gate organization and agent usage views behind capabilities by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36334](https://github.com/BerriAI/litellm/pull/36334)
- fix(reset\_budget\_job): atomic budget cascade with chunked reset scans by [@​ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#​36287](https://github.com/BerriAI/litellm/pull/36287)
- feat(proxy): add GET /v1/indexes to list vector store indexes by [@​ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#​36289](https://github.com/BerriAI/litellm/pull/36289)
- feat(ui): show vector store indexes on the Vector Stores page by [@​ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#​36306](https://github.com/BerriAI/litellm/pull/36306)
- fix(proxy): treat SAML as configured in UI SSO detection by [@​fancybear-dev](https://github.com/fancybear-dev) in [#​36196](https://github.com/BerriAI/litellm/pull/36196)
- fix(bedrock): reject Anthropic server-side web\_search tool with actionable error by [@​ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#​36473](https://github.com/BerriAI/litellm/pull/36473)
- fix(ui): open the classifier prompt editor above the edit auto-router form by [@​tin-berri](https://github.com/tin-berri) in [#​36438](https://github.com/BerriAI/litellm/pull/36438)
- fix(arize): trace MCP tool calls instead of crashing on CallToolResult by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​36453](https://github.com/BerriAI/litellm/pull/36453)
- refactor(ui): make illegal DataTable prop combinations unrepresentable by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36470](https://github.com/BerriAI/litellm/pull/36470)
- fix(ui): scope Virtual Keys and Logs team lists to the caller by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36472](https://github.com/BerriAI/litellm/pull/36472)
- fix(ui): gate the Old Usage page behind a proxy-admin capability by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36469](https://github.com/BerriAI/litellm/pull/36469)
- docs(terraform): describe the provider release as automatic by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36467](https://github.com/BerriAI/litellm/pull/36467)
- feat(proxy): add per-deployment keepalive\_seconds SSE heartbeat to prevent load-balancer timeout on long streams by [@​deepanshululla](https://github.com/deepanshululla) in [#​34423](https://github.com/BerriAI/litellm/pull/34423)
- fix(router): cool down failed fallback deployments and correct cooldown TTL after Redis backfill by [@​deepanshululla](https://github.com/deepanshululla) in [#​35104](https://github.com/BerriAI/litellm/pull/35104)
- perf(spend): write each daily spend batch in one upsert statement by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36448](https://github.com/BerriAI/litellm/pull/36448)
- fix(ui): gate four sidebar pages on the roles their endpoints allow by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36475](https://github.com/BerriAI/litellm/pull/36475)
- fix(ui): restore the Logs Deleted Teams tab for organization admins by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36478](https://github.com/BerriAI/litellm/pull/36478)
- fix(websearch): stop leaking interception control fields to providers by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36480](https://github.com/BerriAI/litellm/pull/36480)
- test(e2e): cover the Anthropic web\_search server tool on Bedrock by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36443](https://github.com/BerriAI/litellm/pull/36443)
- fix(router): warn when a deployment's credentials contradict its provider by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36486](https://github.com/BerriAI/litellm/pull/36486)
- fix: net prompt-caching savings against the cache-write premium by [@​tin-berri](https://github.com/tin-berri) in [#​36452](https://github.com/BerriAI/litellm/pull/36452)
- feat(ui): deployment affinity toggle for the auto-router by [@​tin-berri](https://github.com/tin-berri) in [#​36302](https://github.com/BerriAI/litellm/pull/36302)
- fix(bedrock): use deployment credentials for AWS requests by [@​daleselaji-dev](https://github.com/daleselaji-dev) in [#​36160](https://github.com/BerriAI/litellm/pull/36160)
- fix(anthropic): preserve midturn system corrections by [@​eugene-yao-zocdoc](https://github.com/eugene-yao-zocdoc) in [#​34290](https://github.com/BerriAI/litellm/pull/34290)
- fix(email): stop duplicate legacy invitation email and fix its onboarding link by [@​mubashir1osmani](https://github.com/mubashir1osmani) in [#​36455](https://github.com/BerriAI/litellm/pull/36455)
- feat(ui): show models under each tier in routing benchmark chart by [@​tin-berri](https://github.com/tin-berri) in [#​36291](https://github.com/BerriAI/litellm/pull/36291)
- fix(proxy): inject streaming usage cost on openai passthrough streams by [@​mateo-berri](https://github.com/mateo-berri) in [#​36503](https://github.com/BerriAI/litellm/pull/36503)
- docs: require a user flow and live-proxy proof in bug reports by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36498](https://github.com/BerriAI/litellm/pull/36498)
- fix(proxy): add config\_updated\_at audit timestamp for virtual keys by [@​ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#​36488](https://github.com/BerriAI/litellm/pull/36488)
- docs: require a user flow and a stuck-at proof in feature requests by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36500](https://github.com/BerriAI/litellm/pull/36500)
- feat(router): add required-AND (&) tag prefix and allow\_fail\_open flag by [@​deepanshululla](https://github.com/deepanshululla) in [#​36193](https://github.com/BerriAI/litellm/pull/36193)
- feat(proxy): per-key prompt caching toggle via enable\_prompt\_caching by [@​ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#​36466](https://github.com/BerriAI/litellm/pull/36466)
- fix(bedrock): send tool-search beta header for Haiku 4.5 on Invoke /v1/messages by [@​mateo-berri](https://github.com/mateo-berri) in [#​36502](https://github.com/BerriAI/litellm/pull/36502)
- fix(bedrock): preserve adaptive thinking effort through the /v1/messages bridge by [@​mateo-berri](https://github.com/mateo-berri) in [#​36507](https://github.com/BerriAI/litellm/pull/36507)
- ci: retry transient network fetch failures in lint workflow by [@​mateo-berri](https://github.com/mateo-berri) in [#​36563](https://github.com/BerriAI/litellm/pull/36563)
- fix(ui): stub useIsOrgAdmin in UsageTab tests so useCan needs no QueryClient by [@​ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#​36565](https://github.com/BerriAI/litellm/pull/36565)
- fix(alerting): dedupe scheduled Slack spend reports across pods by [@​ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#​36489](https://github.com/BerriAI/litellm/pull/36489)
- chore(typing): clear 1.6k basedpyright Any errors across 56 files by [@​mateo-berri](https://github.com/mateo-berri) in [#​36543](https://github.com/BerriAI/litellm/pull/36543)
- fix(bedrock): add text block to converse user messages carrying documents by [@​mateo-berri](https://github.com/mateo-berri) in [#​36499](https://github.com/BerriAI/litellm/pull/36499)
- fix(deps): ship boto3 with the base SDK so bedrock works out of the box by [@​mubashir1osmani](https://github.com/mubashir1osmani) in [#​36568](https://github.com/BerriAI/litellm/pull/36568)
- fix(model\_prices): add provider-announced deprecation dates for Bedrock, Mistral, Cohere and Gemini models by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36538](https://github.com/BerriAI/litellm/pull/36538)
- chore: bump litellm-enterprise 0.1.54 -> 0.1.55, litellm-proxy-extras 0.4.84 -> 0.4.85, litellm 1.97.0 -> 1.98.0 by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36577](https://github.com/BerriAI/litellm/pull/36577)
- fix(bedrock\_guardrails): skip ApplyGuardrail when there is no content to scan by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​36441](https://github.com/BerriAI/litellm/pull/36441)
- fix(e2e): assert on the gen-AI span that served the stream, not the span count by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36582](https://github.com/BerriAI/litellm/pull/36582)
- test(e2e): harden vendor API coverage by [@​mubashir1osmani](https://github.com/mubashir1osmani) in [#​34557](https://github.com/BerriAI/litellm/pull/34557)
- test(e2e): add reproducers for passthrough and model budget gaps by [@​mubashir1osmani](https://github.com/mubashir1osmani) in [#​34657](https://github.com/BerriAI/litellm/pull/34657)
- test(e2e): cover google-native generateContent framing and prometheus queue time by [@​mubashir1osmani](https://github.com/mubashir1osmani) in [#​34650](https://github.com/BerriAI/litellm/pull/34650)
- chore(ci): promote internal staging to main by [@​tin-berri](https://github.com/tin-berri) in [#​36560](https://github.com/BerriAI/litellm/pull/36560)
- feat(router): make routing groups callable as virtual models and list them in /v1/models by [@​tin-berri](https://github.com/tin-berri) in [#​36519](https://github.com/BerriAI/litellm/pull/36519)
- fix(xai): bill web\_search from server\_side\_tool\_usage\_details by [@​geraint0923](https://github.com/geraint0923) in [#​30817](https://github.com/BerriAI/litellm/pull/30817)
- fix(responses): init completed\_response on bridge streaming iterator ([#​35411](https://github.com/BerriAI/litellm/issues/35411)) by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​35413](https://github.com/BerriAI/litellm/pull/35413)
- fix(batches): attribute Anthropic passthrough batch cost to the creating key, team and tags by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​36468](https://github.com/BerriAI/litellm/pull/36468)
- feat(dashscope): add latest Model Studio models to the cost map by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36496](https://github.com/BerriAI/litellm/pull/36496)
- fix(proxy): track streamed passthrough Responses cost by [@​william-xue](https://github.com/william-xue) in [#​36529](https://github.com/BerriAI/litellm/pull/36529)
- fix(model\_prices): advertise native structured output on every Bedrock DeepSeek V3.2 and GLM 5 id by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36597](https://github.com/BerriAI/litellm/pull/36597)
- test(bedrock): repoint live Claude tests off the retired Claude 3 Sonnet by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36600](https://github.com/BerriAI/litellm/pull/36600)
- fix(anthropic): preserve speed=fast in usage for /v1/messages and pass-through by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36447](https://github.com/BerriAI/litellm/pull/36447)
- fix(proxy): forward resolved provider and deployment pricing in /cost/estimate by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​35880](https://github.com/BerriAI/litellm/pull/35880)
- feat(proxy): global SSE keepalive ping interval for OpenAI-shaped streaming routes by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36154](https://github.com/BerriAI/litellm/pull/36154)
- fix(responses): preserve Codex namespace tool calls by [@​dcadenas](https://github.com/dcadenas) in [#​32536](https://github.com/BerriAI/litellm/pull/32536)
- fix(nvidia\_nim): preserve image passages and stop sending top\_k to /v1/ranking by [@​atomic](https://github.com/atomic) in [#​34177](https://github.com/BerriAI/litellm/pull/34177)
- fix: refactor HTTP handler initialization with client support by [@​Praveen11558](https://github.com/Praveen11558) in [#​30952](https://github.com/BerriAI/litellm/pull/30952)
- feat(lint): gate writable TypedDict fields with LIT012 by [@​mateo-berri](https://github.com/mateo-berri) in [#​36590](https://github.com/BerriAI/litellm/pull/36590)
- perf(proxy): stagger scheduled background jobs across jobs and pods by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36589](https://github.com/BerriAI/litellm/pull/36589)
- test: remove four mirror test files that exercise none of their module by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​34635](https://github.com/BerriAI/litellm/pull/34635)
- fix(router): stop re-applying router-selecting request tags to the routed tier's deployments by [@​mateo-berri](https://github.com/mateo-berri) in [#​36628](https://github.com/BerriAI/litellm/pull/36628)
- test: remove tests that never execute by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36681](https://github.com/BerriAI/litellm/pull/36681)
- fix(ui): align spend and budget columns by [@​daniel-meismer-zocdoc](https://github.com/daniel-meismer-zocdoc) in [#​35176](https://github.com/BerriAI/litellm/pull/35176)
- test: rename tests that a later definition shadowed by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36685](https://github.com/BerriAI/litellm/pull/36685)
- fix(passthrough): carry the budget reservation into request metadata by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36592](https://github.com/BerriAI/litellm/pull/36592)
- fix(mcp): bound MCP client requests with a session read timeout by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36675](https://github.com/BerriAI/litellm/pull/36675)
- fix(proxy): log requests rejected for an unparsable body in spend logs by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36673](https://github.com/BerriAI/litellm/pull/36673)
- refactor(ui): migrate cost-optimization to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36629](https://github.com/BerriAI/litellm/pull/36629)
- refactor(ui): migrate cost-tracking to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36631](https://github.com/BerriAI/litellm/pull/36631)
- refactor(ui): migrate admin-panel to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36635](https://github.com/BerriAI/litellm/pull/36635)
- refactor(ui): migrate users dashboard to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36642](https://github.com/BerriAI/litellm/pull/36642)
- refactor(ui): migrate prompts to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36643](https://github.com/BerriAI/litellm/pull/36643)
- refactor(ui): migrate team settings to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36641](https://github.com/BerriAI/litellm/pull/36641)
- refactor(ui): migrate models-and-endpoints to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36648](https://github.com/BerriAI/litellm/pull/36648)
- refactor(ui): migrate policy impact popover to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36653](https://github.com/BerriAI/litellm/pull/36653)
- fix(proxy): expand config-defined model access groups when resolving team models for /v2/model/info by [@​ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#​34211](https://github.com/BerriAI/litellm/pull/34211)
- fix(batches): strip NUL bytes from passthrough batch tags before the managed object write by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​36688](https://github.com/BerriAI/litellm/pull/36688)
- test(e2e-ui): verify UI mutations against the API instead of trusting the toast by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36632](https://github.com/BerriAI/litellm/pull/36632)
- fix(proxy): serialize model reconciles so concurrent model writes stop evicting each other by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36687](https://github.com/BerriAI/litellm/pull/36687)
- chore(e2e): port the compat-matrix cron publisher to tests/e2e/claude\_code by [@​mateo-berri](https://github.com/mateo-berri) in [#​36465](https://github.com/BerriAI/litellm/pull/36465)
- fix(router): never price a strategy-router alias by [@​tin-berri](https://github.com/tin-berri) in [#​36691](https://github.com/BerriAI/litellm/pull/36691)
- feat(model\_prices): add NVIDIA Nemotron 3.5 Lightning on OpenRouter and DeepInfra by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36696](https://github.com/BerriAI/litellm/pull/36696)
- feat(terraform/aws): make VPC, Aurora, and Redis optional by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36676](https://github.com/BerriAI/litellm/pull/36676)
- feat(ui): warn in the Admin UI when no Redis is configured by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36495](https://github.com/BerriAI/litellm/pull/36495)
- fix(ui): show and edit key-level router settings on a virtual key by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36674](https://github.com/BerriAI/litellm/pull/36674)
- fix(router): forward auto-router alias params from the marker entry, not the first same-name deployment by [@​mateo-berri](https://github.com/mateo-berri) in [#​36626](https://github.com/BerriAI/litellm/pull/36626)
- fix(bedrock\_mantle): 1M context window and long-context pricing for GPT-5.6 Sol/Terra/Luna by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36698](https://github.com/BerriAI/litellm/pull/36698)
- fix(model\_prices): sync the Groq registry with Groq's docs by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36664](https://github.com/BerriAI/litellm/pull/36664)
- fix(router): let untagged requests bypass a tagged pre-routing strategy on shared model names by [@​mateo-berri](https://github.com/mateo-berri) in [#​36627](https://github.com/BerriAI/litellm/pull/36627)
- fix(spend): stop losing spend log rows when a flush is cancelled by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​34826](https://github.com/BerriAI/litellm/pull/34826)
- docs(claude): drop the @​ prefix from the PR template path by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36726](https://github.com/BerriAI/litellm/pull/36726)
- fix(langfuse): emit otel trace version and release on the keys langfuse v4 reads by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​36702](https://github.com/BerriAI/litellm/pull/36702)
- test(interactions): follow Google spec drift replacing Turn with typed steps by [@​mateo-berri](https://github.com/mateo-berri) in [#​36730](https://github.com/BerriAI/litellm/pull/36730)
- refactor(ui): migrate team detail controls to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36695](https://github.com/BerriAI/litellm/pull/36695)
- refactor(ui): migrate guardrail and duration controls to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36693](https://github.com/BerriAI/litellm/pull/36693)
- refactor(ui): migrate guardrails-monitor, projects, logs to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​34606](https://github.com/BerriAI/litellm/pull/34606)
- refactor(ui): migrate search and user controls to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36694](https://github.com/BerriAI/litellm/pull/36694)
- fix(guardrails): scan and re-emit raw Anthropic SSE streams in the bedrock post-call hook by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​36598](https://github.com/BerriAI/litellm/pull/36598)
- fix(helm): render nodeSelector on the migrations job by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36747](https://github.com/BerriAI/litellm/pull/36747)
- fix(langfuse): coerce header-sourced mask and trace-update steering values by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​36740](https://github.com/BerriAI/litellm/pull/36740)
- refactor(ui): migrate usage tables to shared DataTable by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36707](https://github.com/BerriAI/litellm/pull/36707)
- refactor(ui): migrate guardrails monitor table to shared DataTable by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36709](https://github.com/BerriAI/litellm/pull/36709)
- refactor(ui): migrate guardrails content tables to shared DataTable by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36708](https://github.com/BerriAI/litellm/pull/36708)
- feat(gemini): day-0 pricing for gemini-3.7-flash by [@​mateo-berri](https://github.com/mateo-berri) in [#​36792](https://github.com/BerriAI/litellm/pull/36792)
- ci: promote staging to main by [@​mateo-berri](https://github.com/mateo-berri) in [#​36725](https://github.com/BerriAI/litellm/pull/36725)
- build(deps): bump nanoid to 3.3.18 to clear osv-scan by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36787](https://github.com/BerriAI/litellm/pull/36787)
- fix(router): stop scoring system prompt text for code/technical complexity by [@​tin-berri](https://github.com/tin-berri) in [#​36721](https://github.com/BerriAI/litellm/pull/36721)
- feat(complexity\_router): calibrate the classifier rubric with worked examples, selectable per router by [@​tin-berri](https://github.com/tin-berri) in [#​36578](https://github.com/BerriAI/litellm/pull/36578)
- fix(interactions): map step and turn history to Responses API roles and content types by [@​mateo-berri](https://github.com/mateo-berri) in [#​36733](https://github.com/BerriAI/litellm/pull/36733)
- fix(ui): restore playground model filtering by endpoint by [@​mubashir1osmani](https://github.com/mubashir1osmani) in [#​36130](https://github.com/BerriAI/litellm/pull/36130)
- fix(proxy/batches): stop forwarding custom\_llm\_provider twice in list and cancel by [@​anxkhn](https://github.com/anxkhn) in [#​32813](https://github.com/BerriAI/litellm/pull/32813)
- refactor(ui): migrate TokenFlow and JsonViewer to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36735](https://github.com/BerriAI/litellm/pull/36735)
- feat: pre-adoption shadow eval for the auto-router (blind pairwise judge, derived state) by [@​tin-berri](https://github.com/tin-berri) in [#​36587](https://github.com/BerriAI/litellm/pull/36587)
- refactor(ui): migrate SimpleMessageBlock and SimpleToolCallBlock to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36737](https://github.com/BerriAI/litellm/pull/36737)
- refactor(ui): migrate HistoryTree and CollapsibleMessage to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36738](https://github.com/BerriAI/litellm/pull/36738)
- refactor: replace Any with precise types across responses, proxy, and llms modules by [@​mateo-berri](https://github.com/mateo-berri) in [#​36763](https://github.com/BerriAI/litellm/pull/36763)
- refactor(ui): migrate TruncatedValue and OutputCard to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36739](https://github.com/BerriAI/litellm/pull/36739)
- refactor(ui): migrate SectionHeader and ToolsSection to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36793](https://github.com/BerriAI/litellm/pull/36793)
- feat(ui): migrate playground chat controls to shadcn by [@​mubashir1osmani](https://github.com/mubashir1osmani) in [#​36129](https://github.com/BerriAI/litellm/pull/36129)
- feat(xai): day-0 pricing for grok-4.6 by [@​mateo-berri](https://github.com/mateo-berri) in [#​36805](https://github.com/BerriAI/litellm/pull/36805)
- feat(ui): highlight Auto Router in the navbar announcement by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36315](https://github.com/BerriAI/litellm/pull/36315)
- test(e2e): assert the model allow-list permits, not only denies by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36823](https://github.com/BerriAI/litellm/pull/36823)
- fix(proxy): tolerate a concurrent creator when creating spend views by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36824](https://github.com/BerriAI/litellm/pull/36824)
- fix(proxy): honor explicit null budget\_duration on team and key create + clearable UI dropdowns by [@​ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#​36699](https://github.com/BerriAI/litellm/pull/36699)
- feat(model\_prices): add meta/muse-spark-1.2 and its contributor tier by [@​mateo-berri](https://github.com/mateo-berri) in [#​36717](https://github.com/BerriAI/litellm/pull/36717)
- fix(auth): carry team grants in lite login session tokens by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36826](https://github.com/BerriAI/litellm/pull/36826)
- feat(ui): show provider prompt cache tokens in chat response metrics by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36827](https://github.com/BerriAI/litellm/pull/36827)
- fix(auth): stop the team fallback from widening model access by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36837](https://github.com/BerriAI/litellm/pull/36837)
- fix(proxy/team): resolve member\_delete cleanup by user id, not the addressed email by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36839](https://github.com/BerriAI/litellm/pull/36839)
- fix(cli): launch agents as a child process on Windows by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36822](https://github.com/BerriAI/litellm/pull/36822)
- feat(ui): shadow evals tab beside auto-router usage by [@​tin-berri](https://github.com/tin-berri) in [#​36588](https://github.com/BerriAI/litellm/pull/36588)
- feat(cli): make the hidden `lite` command list configurable by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36816](https://github.com/BerriAI/litellm/pull/36816)
- feat(azure\_ai): add Fireworks FW model pricing on Azure AI Foundry by [@​emerzon](https://github.com/emerzon) in [#​35613](https://github.com/BerriAI/litellm/pull/35613)
- fix: enable xhigh reasoning support for gpt-5.4-mini models by [@​emerzon](https://github.com/emerzon) in [#​26909](https://github.com/BerriAI/litellm/pull/26909)
- feat(azure-ai): add Grok 4.3 model metadata by [@​emerzon](https://github.com/emerzon) in [#​27932](https://github.com/BerriAI/litellm/pull/27932)
- feat(ui): render request metrics on the /ui/chat surface by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36845](https://github.com/BerriAI/litellm/pull/36845)
- fix(ui): stop a deselected MCP server keeping its grant on a virtual key by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36840](https://github.com/BerriAI/litellm/pull/36840)
- fix(team): sweep dangling team references and cache on team delete by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36819](https://github.com/BerriAI/litellm/pull/36819)
- fix(mcp): resolve admin OAuth sessions from any worker via DB-backed drafts by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36844](https://github.com/BerriAI/litellm/pull/36844)
- refactor(ui): migrate usage to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36834](https://github.com/BerriAI/litellm/pull/36834)
- refactor(ui): migrate guardrails-monitor to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36838](https://github.com/BerriAI/litellm/pull/36838)
- refactor(ui): migrate playground to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36847](https://github.com/BerriAI/litellm/pull/36847)
- refactor(ui): migrate guardrails to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36832](https://github.com/BerriAI/litellm/pull/36832)
- fix(batches): stop uncostable batches from starving the cost poll page by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36714](https://github.com/BerriAI/litellm/pull/36714)
- perf(spend-logs): bound retention cleanup so one run cannot saturate the database by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36594](https://github.com/BerriAI/litellm/pull/36594)
- fix(proxy): fail config load when a callbacks entry is not dispatchable by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36858](https://github.com/BerriAI/litellm/pull/36858)
- fix(bedrock): hoist custom.defer\_loading before dropping custom on invoke tools by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36855](https://github.com/BerriAI/litellm/pull/36855)
- fix(access groups): sync assigned\_key\_ids from the key write paths by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36843](https://github.com/BerriAI/litellm/pull/36843)
- fix(mcp): expose client HTTP headers to logging callbacks and hooks by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36724](https://github.com/BerriAI/litellm/pull/36724)
- fix(ptu): stop per-token billing on a PTU-configured deployment by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​36829](https://github.com/BerriAI/litellm/pull/36829)
- fix(ui): add nvidia riva to the model provider list by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36769](https://github.com/BerriAI/litellm/pull/36769)
- fix(scripts): end make check with a ran/skipped summary and verdict by [@​mateo-berri](https://github.com/mateo-berri) in [#​36864](https://github.com/BerriAI/litellm/pull/36864)
- fix(proxy): track spend for OpenAI passthrough /v1/embeddings by [@​lostmartian](https://github.com/lostmartian) in [#​36660](https://github.com/BerriAI/litellm/pull/36660)
- test(proxy): stop monkeypatch.undo re-planting fixture-mocked prisma\_client by [@​mateo-berri](https://github.com/mateo-berri) in [#​36872](https://github.com/BerriAI/litellm/pull/36872)
- fix(access groups): sync assigned\_team\_ids from the team write paths by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36825](https://github.com/BerriAI/litellm/pull/36825)
- ci: drop the CircleCI ui\_build and ui\_unit\_tests jobs by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36893](https://github.com/BerriAI/litellm/pull/36893)
- fix(langfuse)!: source the emitted metadata blob from StandardLoggingPayload by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​36744](https://github.com/BerriAI/litellm/pull/36744)
- refactor(ui): migrate Navbar off antd to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36902](https://github.com/BerriAI/litellm/pull/36902)
- refactor(ui): migrate log details drawer off antd to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36904](https://github.com/BerriAI/litellm/pull/36904)
- refactor(ui): migrate AI Hub off antd and tremor to shadcn by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36908](https://github.com/BerriAI/litellm/pull/36908)
- refactor(ui): move the shared dropdowns and selectors onto shadcn primitives by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36924](https://github.com/BerriAI/litellm/pull/36924)
- refactor(ui): move the root-level dashboard components onto shadcn primitives by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36927](https://github.com/BerriAI/litellm/pull/36927)
- refactor(ui): move the settings page and bulk user invite onto shadcn primitives by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36936](https://github.com/BerriAI/litellm/pull/36936)
- refactor(ui): move the cost tracking components onto shadcn primitives by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36955](https://github.com/BerriAI/litellm/pull/36955)
- ci: drop the duplicate proxy\_unit\_tests letter-shard workflow by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36866](https://github.com/BerriAI/litellm/pull/36866)
- refactor(ui): migrate shared common\_components off antd and tremor by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36910](https://github.com/BerriAI/litellm/pull/36910)
- refactor(ui): migrate key info and permissions views off antd and tremor by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36913](https://github.com/BerriAI/litellm/pull/36913)
- feat(proxy): serve Anthropic-native /v1/models for Claude Code gateway discovery by [@​Ar-maan05](https://github.com/Ar-maan05) in [#​35455](https://github.com/BerriAI/litellm/pull/35455)
- refactor(ui): migrate router settings and shared badges off antd and tremor by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36915](https://github.com/BerriAI/litellm/pull/36915)
- refactor(ui): move the model hub and model select onto shadcn primitives by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36918](https://github.com/BerriAI/litellm/pull/36918)
- fix(ui): keep the cost tracking removal confirmation open until it settles by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36960](https://github.com/BerriAI/litellm/pull/36960)
- refactor(ui): declare DateRangePickerValue locally instead of importing it from tremor by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36962](https://github.com/BerriAI/litellm/pull/36962)
- fix(main): an explicit provider outranks a known OpenAI model name by [@​FahimaGold](https://github.com/FahimaGold) in [#​36800](https://github.com/BerriAI/litellm/pull/36800)
- fix(exception\_mapping): bare 429 in an error body no longer outranks the status code by [@​FahimaGold](https://github.com/FahimaGold) in [#​36705](https://github.com/BerriAI/litellm/pull/36705)
- refactor(ui): move MCP permission panels onto shadcn primitives by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36964](https://github.com/BerriAI/litellm/pull/36964)
- refactor(ui): migrate ten small dashboard files off antd and tremor by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36966](https://github.com/BerriAI/litellm/pull/36966)
- fix(proxy): force prisma recreate on postgres cached-plan error by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36428](https://github.com/BerriAI/litellm/pull/36428)
- fix(transcription): stop a zero output rate from zeroing transcription cost by [@​hMED22](https://github.com/hMED22) in [#​36914](https://github.com/BerriAI/litellm/pull/36914)
- fix(langfuse): restrict trace steering keys to real langfuse trace fields by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​36862](https://github.com/BerriAI/litellm/pull/36862)
- Revert "fix(auth): stop the team fallback from widening model access" ([#​36837](https://github.com/BerriAI/litellm/issues/36837)) by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36982](https://github.com/BerriAI/litellm/pull/36982)
- fix(ui): show zeroed auto-router usage stats when a window has no sessions by [@​tin-berri](https://github.com/tin-berri) in [#​36868](https://github.com/BerriAI/litellm/pull/36868)
- fix(mcp): keep admin-entered oauth endpoints in management reads by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36888](https://github.com/BerriAI/litellm/pull/36888)
- fix(ui): distinguish hosted and local vLLM in the provider dropdown by [@​mateo-berri](https://github.com/mateo-berri) in [#​36974](https://github.com/BerriAI/litellm/pull/36974)
- fix(openai,azure): return a length-truncated 200 when the output budget fits no token by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36859](https://github.com/BerriAI/litellm/pull/36859)
- fix(proxy): always emit the Anthropic /v1/models token limits, null when unknown by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36961](https://github.com/BerriAI/litellm/pull/36961)
- feat(helm): add startupProbe and hpa.behavior to the componentized chart by [@​Louis-Vauterin](https://github.com/Louis-Vauterin) in [#​36382](https://github.com/BerriAI/litellm/pull/36382)
- fix(proxy): serve aggregate MCP endpoint on bare /mcp instead of 307-redirecting by [@​tin-berri](https://github.com/tin-berri) in [#​34845](https://github.com/BerriAI/litellm/pull/34845)
- feat(shadow\_eval): add reverse-direction shadow eval jobs by [@​tin-berri](https://github.com/tin-berri) in [#​36865](https://github.com/BerriAI/litellm/pull/36865)
- fix(proxy): requeue Redis spend buffer transactions when the DB commit fails by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​33881](https://github.com/BerriAI/litellm/pull/33881)
- feat(search): add Nimble as a search provider by [@​ilchemla](https://github.com/ilchemla) in [#​36347](https://github.com/BerriAI/litellm/pull/36347)
- fix(mcp): drop caller host and configured upstream headers from logged metadata by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​36901](https://github.com/BerriAI/litellm/pull/36901)
- fix(azure\_ai): recognize real Search doc endpoints so teams can read/write via passthrough by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36798](https://github.com/BerriAI/litellm/pull/36798)
- fix(anthropic): aggregate 5m/1h cache-write split across iterations path by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​34860](https://github.com/BerriAI/litellm/pull/34860)
- fix(anthropic cost): apply regional geo uplift to cached tokens by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​34850](https://github.com/BerriAI/litellm/pull/34850)
- fix(ui): match the MCP servers count badge to its sibling permission badges by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36984](https://github.com/BerriAI/litellm/pull/36984)
- fix(batches): mark terminal batch with no output file as processed in CheckBatchCost by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​35360](https://github.com/BerriAI/litellm/pull/35360)
- fix(caching): cache anthropic /v1/messages responses, including streaming by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​34581](https://github.com/BerriAI/litellm/pull/34581)
- fix(anthropic\_messages): make tool\_result images visible to OpenAI-compatible providers by [@​hMED22](https://github.com/hMED22) in [#​34462](https://github.com/BerriAI/litellm/pull/34462)
- feat(fireworks\_ai): translate NIM/vLLM extra params to Fireworks-native args by [@​milesadkins](https://github.com/milesadkins) in [#​35969](https://github.com/BerriAI/litellm/pull/35969)
- fix(ui): stop the models tab strip from scrolling vertically by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36993](https://github.com/BerriAI/litellm/pull/36993)
- fix(ui): anchor chips-combobox popups to the field instead of the inner input by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36995](https://github.com/BerriAI/litellm/pull/36995)
- feat(proxy): per-component response cost headers by [@​erensh27](https://github.com/erensh27) in [#​36965](https://github.com/BerriAI/litellm/pull/36965)
- fix(cost): track OpenAI/Azure web search tool cost per call by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​35286](https://github.com/BerriAI/litellm/pull/35286)
- fix(bedrock): resolve aliases in batch file records by [@​daleselaji-dev](https://github.com/daleselaji-dev) in [#​36159](https://github.com/BerriAI/litellm/pull/36159)
- fix: report real token usage on guardrail-blocked /v1/responses replies by [@​guptaishaan](https://github.com/guptaishaan) in [#​36907](https://github.com/BerriAI/litellm/pull/36907)
- fix(proxy): requeue spend logs when the DB write fails with a transport error by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36716](https://github.com/BerriAI/litellm/pull/36716)
- fix(cost): tiered pricing supports cache creation cost and is all-or-nothing by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36720](https://github.com/BerriAI/litellm/pull/36720)
- fix(vertex\_ai): translate /v1/embeddings batch rows to the Gemini embedding shape by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​35092](https://github.com/BerriAI/litellm/pull/35092)
- docs(claude): require ReadOnly on every TypedDict field (LIT012) by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​37005](https://github.com/BerriAI/litellm/pull/37005)
- refactor(ui): migrate access group create modal to RHF + zod + shadcn by [@​ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#​37033](https://github.com/BerriAI/litellm/pull/37033)
- refactor(ui): re-sync badge and skeleton onto the base-vega shadcn style by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​36991](https://github.com/BerriAI/litellm/pull/36991)
- feat(ui): link user detail team names to team pages by [@​ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#​37022](https://github.com/BerriAI/litellm/pull/37022)
- fix(model\_prices): correct DeepSeek V4 max output tokens by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36925](https://github.com/BerriAI/litellm/pull/36925)
- fix(ui): rename models table Status column to Source by [@​ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#​37021](https://github.com/BerriAI/litellm/pull/37021)
- chore: bump litellm-enterprise 0.1.55 -> 0.1.56, litellm-proxy-extras 0.4.85 -> 0.4.86 by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​37045](https://github.com/BerriAI/litellm/pull/37045)
- feat(proxy): gate the Global Control Plane worker registry on an enterprise license by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​36996](https://github.com/BerriAI/litellm/pull/36996)
- fix(model\_prices): add gemini 3.1 flash tts preview and legacy OpenAI shutdown dates by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36788](https://github.com/BerriAI/litellm/pull/36788)
- fix(panw\_prisma\_airs): surface scan\_id on allowed requests by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​37037](https://github.com/BerriAI/litellm/pull/37037)
- fix(model\_map): flag native structured outputs on Anthropic-direct claude-sonnet-5 and claude-haiku-4-5 by [@​anmolg1997](https://github.com/anmolg1997) in [#​35930](https://github.com/BerriAI/litellm/pull/35930)
- fix(router): stop get\_router\_model\_info from wiping cached pricing by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36985](https://github.com/BerriAI/litellm/pull/36985)
- fix(redis): unwrap decorated \_\_init\_\_s when deriving the from\_url kwargs allowlist by [@​anmolg1997](https://github.com/anmolg1997) in [#​36654](https://github.com/BerriAI/litellm/pull/36654)
- fix(proxy): reserve the larger declared output budget for TPM limits by [@​yassin-berriai](https://github.com/yassin-berriai) in [#​37001](https://github.com/BerriAI/litellm/pull/37001)
- fix(databricks): surface provider usage, including prompt-cache counts, in streaming chunks by [@​pokepoke81](https://github.com/pokepoke81) in [#​36943](https://github.com/BerriAI/litellm/pull/36943)
- fix(spend): give a batch's cost row a primary key of its own by [@​marty-sullivan](https://github.com/marty-sullivan) in [#​36876](https://github.com/BerriAI/litellm/pull/36876)
- feat: shadow eval samples /v1/messages and /v1/responses traffic by [@​tin-berri](https://github.com/tin-berri) in [#​36830](https://github.com/BerriAI/litellm/pull/36830)
- fix(ptu): stop a PTU deployment billing for grounded search by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​37043](https://github.com/BerriAI/litellm/pull/37043)
- fix(fireworks\_ai): support router slugs via routers/ prefix by [@​heathriel](https://github.com/heathriel) in [#​34257](https://github.com/BerriAI/litellm/pull/34257)
- fix(bedrock): register managed-batch litellm\_params so they stop leaking to the provider (internal copy of [#​36633](https://github.com/BerriAI/litellm/issues/36633)) by [@​mateo-berri](https://github.com/mateo-berri) in [#​37048](https://github.com/BerriAI/litellm/pull/37048)
- fix(bedrock): resolve the managed-batch output bucket on every path that reads it by [@​mateo-berri](https://github.com/mateo-berri) in [#​37047](https://github.com/BerriAI/litellm/pull/37047)
- fix(bedrock): resolve the managed-batch output bucket on every path that reads it by [@​marty-sullivan](https://github.com/marty-sullivan) in [#​36634](https://github.com/BerriAI/litellm/pull/36634)
- feat(scripts): queue heavy gates behind a machine-wide slot lock by [@​mateo-berri](https://github.com/mateo-berri) in [#​36988](https://github.com/BerriAI/litellm/pull/36988)
- feat(mcp): scope gateway session bearers to the RFC 8707 resource by [@​tin-berri](https://github.com/tin-berri) in [#​35045](https://github.com/BerriAI/litellm/pull/35045)
- feat(ui): direction picker and reverse-mode display for shadow evals by [@​tin-berri](https://github.com/tin-berri) in [#​36994](https://github.com/BerriAI/litellm/pull/36994)
- fix(guardrails): return the full PANW AIRS scan response on blocked requests by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​37036](https://github.com/BerriAI/litellm/pull/37036)
- fix(passthrough): stop forwarding client Accept-Encoding upstream by [@​mateo-berri](https://github.com/mateo-berri) in [#​37058](https://github.com/BerriAI/litellm/pull/37058)
- fix(batches): account a managed batch's cost exactly once by [@​mateo-berri](https://github.com/mateo-berri) in [#​37050](https://github.com/BerriAI/litellm/pull/37050)
- fix(panw\_prisma\_airs): scan tool call args as plain text, not a tool\_event by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​37038](https://github.com/BerriAI/litellm/pull/37038)
- feat(lint): exempt TypedDict-annotated dict literals from LIT002 by [@​mateo-berri](https://github.com/mateo-berri) in [#​36869](https://github.com/BerriAI/litellm/pull/36869)
- docs(claude): tell agents to let heavy gates queue for machine-wide slots by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​37057](https://github.com/BerriAI/litellm/pull/37057)
- test: unstick the suites CircleCI is failing on by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​37059](https://github.com/BerriAI/litellm/pull/37059)
- docs(github): proof-of-fix section shows only the latest run as Before/After with nested cases by [@​mateo-berri](https://github.com/mateo-berri) in [#​37063](https://github.com/BerriAI/litellm/pull/37063)
- test(e2e): assert provider error shape instead of pinned prose by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​37065](https://github.com/BerriAI/litellm/pull/37065)
- fix(ui): de-duplicate the reset budget option and polish shadcn surfaces by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​37010](https://github.com/BerriAI/litellm/pull/37010)
- chore: rebuild Admin UI bundle from litellm\_internal\_staging by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​37066](https://github.com/BerriAI/litellm/pull/37066)
- test(e2e/ui): assert the log drawer chevrons by their lucide classes by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​37069](https://github.com/BerriAI/litellm/pull/37069)
- chore(ci): promote internal staging to main by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​37042](https://github.com/BerriAI/litellm/pull/37042)
- fix(ui): keep completion-mode models in the playground chat dropdown (backport to rc/1.98.0) by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​37955](https://github.com/BerriAI/litellm/pull/37955)
##### New Contributors
- [@​kr0k](https://github.com/kr0k) made their first contribution in [#​33196](https://github.com/BerriAI/litellm/pull/33196)
- [@​HuanQian571](https://github.com/HuanQian571) made their first contribution in [#​35773](https://github.com/BerriAI/litellm/pull/35773)
- [@​alexshtf](https://github.com/alexshtf) made their first contribution in [#​35669](https://github.com/BerriAI/litellm/pull/35669)
- [@​vairodp](https://github.com/vairodp) made their first contribution in [#​33490](https://github.com/BerriAI/litellm/pull/33490)
- [@​fancybear-dev](https://github.com/fancybear-dev) made their first contribution in [#​36196](https://github.com/BerriAI/litellm/pull/36196)
- [@​daleselaji-dev](https://github.com/daleselaji-dev) made their first contribution in [#​36160](https://github.com/BerriAI/litellm/pull/36160)
- [@​eugene-yao-zocdoc](https://github.com/eugene-yao-zocdoc) made their first contribution in [#​34290](https://github.com/BerriAI/litellm/pull/34290)
- [@​geraint0923](https://github.com/geraint0923) made their first contribution in [#​30817](https://github.com/BerriAI/litellm/pull/30817)
- [@​william-xue](https://github.com/william-xue) made their first contribution in [#​36529](https://github.com/BerriAI/litellm/pull/36529)
- [@​dcadenas](https://github.com/dcadenas) made their first contribution in [#​32536](https://github.com/BerriAI/litellm/pull/32536)
- [@​atomic](https://github.com/atomic) made their first contribution in [#​34177](https://github.com/BerriAI/litellm/pull/34177)
- [@​Praveen11558](https://github.com/Praveen11558) made their first contribution in [#​30952](https://github.com/BerriAI/litellm/pull/30952)
- [@​anxkhn](https://github.com/anxkhn) made their first contribution in [#​32813](https://github.com/BerriAI/litellm/pull/32813)
- [@​lostmartian](https://github.com/lostmartian) made their first contribution in [#​36660](https://github.com/BerriAI/litellm/pull/36660)
- [@​FahimaGold](https://github.com/FahimaGold) made their first contribution in [#​36800](https://github.com/BerriAI/litellm/pull/36800)
- [@​Louis-Vauterin](https://github.com/Louis-Vauterin) made their first contribution in [#​36382](https://github.com/BerriAI/litellm/pull/36382)
- [@​ilchemla](https://github.com/ilchemla) made their first contribution in [#​36347](https://github.com/BerriAI/litellm/pull/36347)
- [@​milesadkins](https://github.com/milesadkins) made their first contribution in [#​35969](https://github.com/BerriAI/litellm/pull/35969)
- [@​erensh27](https://github.com/erensh27) made their first contribution in [#​36965](https://github.com/BerriAI/litellm/pull/36965)
- [@​guptaishaan](https://github.com/guptaishaan) made their first contribution in [#​36907](https://github.com/BerriAI/litellm/pull/36907)
- [@​pokepoke81](https://github.com/pokepoke81) made their first contribution in [#​36943](https://github.com/BerriAI/litellm/pull/36943)
- [@​heathriel](https://github.com/heathriel) made their first contribution in [#​34257](https://github.com/BerriAI/litellm/pull/34257)
**Full Changelog**: <https://github.com/BerriAI/litellm/compare/v1.97.0...v1.98.0>
### [`v1.98.0`](https://github.com/BerriAI/litellm/releases/tag/v1.98.0)
[Compare Source](https://github.com/BerriAI/litellm/compare/v1.97.0...v1.98.0)
##### Verify Docker Image Signature
All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](https://github.com/BerriAI/litellm/commit/0112e53046018d726492c814b3644b7d376029d0).
**Verify using the pinned commit hash (recommended):**
A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:
```bash
cosign verify \
--key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
ghcr.io/berriai/litellm:v1.98.0
```
**Verify using the release tag (convenience):**
Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:
```bash
cosign verify \
--key https://raw.githubusercontent.com/BerriAI/litellm/v1.98.0/cosign.pub \
ghcr.io/berriai/litellm:v1.98.0
```
Expected output:
```
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the specified public key
```
***
##### What's Changed
- fix(bedrock): drop toolSpec.strict for Claude Sonnet 5 on Converse by [@​kr0k](https://github.com/kr0k) in [#​33196](https://github.com/BerriAI/litellm/pull/33196)
- fix(batches): attribute Vertex passthrough batch cost to key/team/tags by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​34456](https://github.com/BerriAI/litellm/pull/34456)
- docs: rewrite the CLAUDE.md comment rule with explicit exceptions by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36301](https://github.com/BerriAI/litellm/pull/36301)
- fix(proxy): scope file list pagination cursors to the caller by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36093](https://github.com/BerriAI/litellm/pull/36093)
- fix(proxy): skip prisma-dependent hooks when no database is attached by [@​mateo-berri](https://github.com/mateo-berri) in [#​36273](https://github.com/BerriAI/litellm/pull/36273)
- fix(proxy): report has\_more false on caller-scoped file list pages by [@​mateo-berri](https://github.com/mateo-berri) in [#​36326](https://github.com/BerriAI/litellm/pull/36326)
- fix(proxy): restore management\_v1 query-param validation under fastapi>=0.140.7 by [@​HuanQian571](https://github.com/HuanQian571) in [#​35773](https://github.com/BerriAI/litellm/pull/35773)
- fix(proxy): stop /{provider}/v1/files from capturing /openai\_passthrough by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36092](https://github.com/BerriAI/litellm/pull/36092)
- chore(typing): remove 914 basedpyright Any errors across 16 hotspot files by [@​mateo-berri](https://github.com/mateo-berri) in [#​36386](https://github.com/BerriAI/litellm/pull/36386)
- fix(router): keep batch fallbacks inside the model group that owns the file by [@​mateo-berri](https://github.com/mateo-berri) in [#​36181](https://github.com/BerriAI/litellm/pull/36181)
- feat(ptu): configure provisioned-throughput flat cost on a model deployment by [@​yucheng-berri](https://github.com/yucheng-berri) in [#​35341](https://github.com/BerriAI/litellm/pull/35341)
- docs: clarify the CLAUDE.md comment exceptions are any-of by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​36421](https://github.com/BerriAI/litellm/pull/36421)
- docs: replace the Changes …
* feat(lint): exempt TypedDict-annotated dict literals from LIT002
* fix(scripts): unwrap PEP 604 unions in LIT002 TypedDict detection
* fix(mcp): expose client HTTP headers to logging callbacks and hooks (#36724)
* fix(mcp): expose client HTTP headers to logging callbacks and hooks
MCP protocol tool calls built a synthetic Request with only content-type, so metadata.headers reaching logging callbacks and guardrails was empty while /mcp-rest/tools/call exposed the full set. Rebuild the synthetic request from the connection's raw headers (shared with the sampling path), and pass sanitized headers to the pre-call hook, the MCP to LLM guardrail bridge and the Responses API MCP bridge. Credential headers stay masked and proxy key headers stripped.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(mcp): strip custom proxy key and upstream MCP credential headers from logging copies
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* refactor(mcp): make client side auth header name accessor public
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(mcp): strip custom proxy key and client redaction opt-out from mcp headers
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(mcp): drop custom proxy key header in the synthetic request builder
Strips general_settings.litellm_key_header_name in build_synthetic_mcp_request so every caller, including sampling, is covered, and reverts passing general_settings into add_litellm_data_to_request on the tool call path since that also switches on enforced_params.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: shivam <shivam@berri.ai>
* test(proxy): stop monkeypatch.undo re-planting fixture-mocked prisma_client
* fix(ptu): stop per-token billing on a PTU-configured deployment (#36829)
A deployment with PTU flat-cost attribution also billed every request per
token, so a team paid for reserved capacity and again for the traffic that
capacity serves. Nothing set the per-token price and an unset price falls
back to the public cost map, which made the double charge the default.
/model/new and /model/{id}/update now store zero for every pricing field the
cost map could otherwise fill, refuse a price the caller supplies alongside
PTU config with a 400 naming the field, zero a price already on the row
rather than rejecting later edits of unrelated fields, and drop the zeros
again when the PTU config goes.
A PTU deployment is no longer read as a free model by the budget checks,
which would have waived every budget for it.
* fix(batches): mark terminal batch with no output file as processed in CheckBatchCost
A managed batch whose request lines all failed can reach a terminal provider
status (completed) with output_file_id=None and only an error_file_id. Such a
row matched neither the completed-with-output billing branch nor the
failed/expired/cancelled branch, so batch_processed stayed False and the poller
re-selected it on every cycle for the lifetime of the deployment; output/error
file deletion is also gated on batch_processed, so those files could never be
deleted.
Broaden the terminal handling so a completed/complete/expired batch with an
output file is billed, and any terminal batch with nothing to bill
(failed/cancelled, or completed/expired with no output) is marked terminal
exactly once. Non-terminal statuses (validating/in_progress) are still left for
the next poll, and an expired batch that did produce output is now billed.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(proxy): track spend for OpenAI passthrough /v1/embeddings (#36660)
* fix(proxy): track spend for OpenAI passthrough /v1/embeddings
OpenAI passthrough embeddings returned 200 but wrote no spend because the
route was unsupported and Cohere's /v1/embed prefix stole the match.
* fix(proxy): clear embeddings lint and Greptile comment nits
Inline embeddings cost tracking to avoid new LIT001/002 hits, trim
redundant doc comments, and cover the Cohere /v1/embeddings collision.
* fix(proxy): drop unreachable embeddings TypeError guard
convert_to_model_response_object with response_type=embedding already
returns EmbeddingResponse; the isinstance check was dead patch coverage.
* fix(batches): persist real terminal status when billing expired batches
* fix(access groups): sync assigned_team_ids from the team write paths (#36825)
* fix(spend): give a batch's cost row a primary key of its own
request_id is the primary key of LiteLLM_SpendLogs and the flush inserts with
skip_duplicates, so a spend log whose id already exists is dropped with no error
raised and a "processed 1 spend log" line still logged. Batch cost accounting
produced exactly such an id twice over, and on a proxy with message redaction
enabled no batch cost row could be written at all.
get_spend_logs_id derived the id by md5-hashing the response for two call types,
aretrieve_batch and acreate_file. Redaction makes that hash a constant:
perform_redaction returns the fixed {"text": "redacted-by-litellm"} placeholder
for any shape it cannot redact, which is what a batch object and a file body both
become, so every such row hashed to md5('{"text": "redacted-by-litellm"}') =
00fcbef15a3b0097e14b0ca016ed30a0 regardless of provider, user, or amount. The
first row to claim that id owned it and every later row was discarded. Verified
against a live proxy: four payloads spanning two providers and three distinct
spend values all computed that id, and the table held one acreate_file row dating
to 2025-05-25, the row that had claimed it.
Keying off the batch's own identity instead is necessary but not sufficient,
because creating a batch already writes an acreate_batch row under exactly that
id, so the cost row becomes a duplicate of the batch's own creation row. Also
verified live: after the hash was removed the poller computed and flushed a
batch's cost, and the only row carrying that id was the acreate_batch row from
when the batch was submitted.
The id now comes from the response's own id, then the standard logging payload's
id, then litellm_call_id, and a batch cost row is namespaced with a _batch_cost
suffix so it cannot collide with the creation row. The middle term is what keeps
this correct under redaction: that payload is built from the unredacted response,
so it still carries the batch id after redaction has flattened the body. Keying
the cost row to the batch rather than to the call also keeps accounting the same
batch twice collapsing to one row instead of billing it twice. Every other call
type still derives its key exactly as before.
Cost and usage themselves are unaffected by redaction: the token columns fall back
to the standard logging payload and spend comes from its response_cost, neither of
which redaction touches. generate_hash_from_response had no other caller and is
removed with it.
* test(spend): annotate the batch cost row constants as Final
* fix(bedrock): resolve the managed-batch output bucket on the model-routed and cost-poller paths
get_configured_s3_bucket_name accepts the output bucket only from the immutable
_litellm_internal_model_credentials snapshot or AWS_S3_BUCKET_NAME. That refusal to read
litellm_params is deliberate: the bucket is what validate_managed_cloud_file_id checks a
file id against, so trusting a request-supplied value would let a caller redirect reads
to a bucket of their choosing
Two live entry points reach the Bedrock file-content transformation without ever building
that snapshot. The managed-files pre-call hook sets data["model"] for any id carrying
llm_output_file_id, which is every batch output, so get_file_content always takes the
model-routed branch; that branch called llm_router.afile_content directly, and
managed_files_obj.afile_content, the only caller that built the snapshot, is therefore
unreachable for batch output. CheckBatchCost spread the deployment credentials as plain
kwargs, and get_litellm_params does not carry s3_bucket_name across (gcs_bucket_name is
listed for exactly this reason, its S3 counterpart is not), so the poller lost the bucket
the same way
The result was that every completed Bedrock managed batch failed files.content with
"S3 bucket_name is required" and never had its cost tracked, leaving the row to be
re-polled every cycle. Both paths now resolve the deployment credentials and pass the
same MappingProxyType snapshot the managed-files hook already builds
* test(files): capture routed retrieval calls immutably
The mock merged every call into one shared dict, so a second routed retrieval would
overwrite the first and the assertions would still pass. Keep one frozen snapshot per
call and assert exactly one call, which also makes an unintended second retrieval a
failure rather than something the merge hides
* fix(bedrock): resolve the managed-batch output bucket on the inline accounting path too
A third path reads a completed batch's output file, and it could not resolve the
bucket either. When cost is accounted from the retrieve itself rather than from
the poller, the batch success handler calls _handle_completed_batch, which fetches
the output file through _extract_file_access_credentials. That helper forwarded a
whitelist covering Azure and Vertex, gcs_bucket_name included, but nothing for
Bedrock, and retrieve_batch built its litellm_params through get_litellm_params,
whose fixed signature drops the trusted credential snapshot. So the snapshot never
reached the file read and it failed with "S3 bucket_name is required" for a bucket
the deployment had configured, leaving the batch's cost unrecorded.
Adding s3_bucket_name to that whitelist would not have worked. The Bedrock file
config deliberately resolves the bucket only from the immutable server-side
snapshot or the environment, never from a request param, because the bucket is
what managed file ids are validated against. The snapshot is therefore what has to
flow, exactly as it already does for the model-routed and cost-poller paths.
retrieve_batch now re-adds the snapshot after get_litellm_params, the same way the
file operations already do, the whitelist forwards it, and the proxy attaches it
for router-routed managed batches from the deployment behind the unified id.
Verified against a live proxy reading a real completed Bedrock batch: the cost row
appears within seconds of the retrieve carrying the batch's real spend and usage,
where before the read raised and no row was written.
Resolving those credentials is best effort. A batch whose deployment no longer
resolves, which happens when a model group is removed while batches are in
flight, still serves its status instead of failing the request on the lookup.
This matters for the OSS and polling-disabled configurations, where the retrieve
path is the only thing that accounts for a batch at all.
* refactor(batches): share the trusted-credentials helper across both call paths
The helper that carries the credential snapshot into litellm_params lived private
in files/main.py, and the batch retrieve needed it too. It now sits beside
get_litellm_params, which is what it augments, so neither caller reaches into the
other's private surface. Typed as Mapping/MutableMapping of object rather than
Any, which the strict import rules ban.
The file-content route builds the snapshot through the same helper as the batch
route instead of assembling a conditional mapping inline, which drops two mutable
constructions and leaves one way to attach it. Its name loses the batch suffix now
that both routes use it.
* fix(batches): account a managed batch's cost exactly once
Two components computed a managed batch's cost and each assumed it was the only
one. Retrieving a batch computed it through the @client decorator's success
callback, and CheckBatchCost computed it on its own schedule. Whichever observed
completion first decided the outcome, so cost was either counted once per
retrieve or not at all.
The lockout is the worse half. Retrieving a batch that had reached completion set
batch_processed=True, which is what takes a batch out of CheckBatchCost's queue,
since it selects batch_processed=False. That write claimed the cost had been
accounted for on behalf of a callback that had not run yet and was not awaited.
When the callback then failed the cost was gone permanently, with the poller
already retired and no retry left. Observed on a live proxy: two completed
batches whose callbacks raised inside the logging worker, one on a provider
output path that did not resolve and one on a batch whose output file id was
still None, both left marked processed with no spend row and no way to recover
them. Nothing logged at error level for the batches themselves.
The over-count is the other half. Nothing suppressed recomputation, so each
retrieve of an already-completed batch recorded that batch's full cost again. A
caller polling its own batch to see whether it had finished inflated spend by
however many times it looked.
The flag now means what its name says, and only the component that actually
recorded the cost sets it. When the poller is running it owns accounting, so
retrieving a managed batch records no cost and leaves the flag alone; the poller
computes once and sets it. When the poller cannot be relied on, either because
polling is disabled by config or because the enterprise job never registered,
the retrieve path is the only accountant and behaves exactly as before. Batches
with no managed object row are untouched either way, since neither the flag nor
the poller queue applies to them.
* fix(batches): only hand accounting to the poller once it can mark batches done
The handoff asked whether the poller was running, when what matters is whether it
will actually account for the batch. Those differ on a schema without the
batch_processed column: the poller cannot filter on it, so it falls back to a
query that excludes complete and completed rows, and it cannot set it either. A
caller retrieving a provider-completed batch before the poller saw it therefore
suppressed inline accounting, then marked the row complete, and the fallback query
could never find it again. Nobody accounted for that batch, so its cost escaped
the caller's budget entirely.
The poller now publishes batch_processed_support_confirmed, set only once a
filtered query has actually succeeded, and the handoff requires it. Defaulting to
unconfirmed keeps accounting on the retrieve path in exactly the cases the poller
would drop the batch, including the window before the poller's first cycle. All
four combinations account exactly once: unconfirmed leaves the retrieve
accounting and setting the marker, whether or not the column exists, and
confirmed is only reachable when the column is present, where the poller accounts
and sets it.
A scheduler that hands back something other than a bound method leaves no poller
to interrogate, which reads as unconfirmed rather than as working.
* fix(batches): decide batch cost ownership once per retrieve
The ownership question was asked twice for one retrieve: once before the provider
call to decide whether to suppress inline accounting, and again afterwards to
decide whether to mark the batch accounted. Between those two points the poller
can complete its first successful filtered query and become usable, so the two
answers disagree. The retrieve then accounts for the batch inline, having decided
the poller was unusable, while the later check sees a usable poller and leaves the
marker unset, so the poller accounts for the same batch again and its spend is
counted twice.
The retrieve now decides once and passes that decision to
update_batch_in_database, which prefers it over re-deriving one. Callers that
record no cost of their own leave it unset and keep deriving it as before, so the
cancel path is unchanged.
* ci: drop the CircleCI ui_build and ui_unit_tests jobs (#36893)
Both are covered on GitHub Actions. test-litellm-ui-build.yml runs the
dashboard build on every PR, and test-litellm-ui-unit.yml runs the vitest
suite with ui-unit-tests already a required check, so neither CircleCI job
gates anything that GHA does not already gate.
ui_build additionally produced nothing anyone consumed. It persisted
litellm/proxy/_experimental/out to the workspace, and the only job
downstream of it was ui_unit_tests, which never attached the workspace and
reinstalled from source instead. The requires edge was pure sequencing, so
the build output was written and discarded on every client-touching PR.
One real narrowing comes with this, and it is deliberate. ui_unit_tests ran
the full vitest suite on PRs, while the GHA job scopes PR runs to tests
reachable from the diff and keeps the full suite on pushes to staging. That
split was a measured decision in #34175 and it still holds: the suite is
252s and 248s of that is CreateMCPServer.integration.test.tsx alone, so
running everything per PR buys about four minutes to re-run one file.
Note that assert-ci-coverage does not speak to this. It walks
tests/**/test_*.py only, so it is blind to vitest files by construction;
it stays green here because no Python test lost a runner, which is a
narrower claim than the UI side being unaffected.
auth_ui_unit_tests is a different job, a Python suite on a Postgres
sidecar, and is untouched
* fix(langfuse): source the emitted metadata blob from StandardLoggingPayload (#36744)
Request metadata carries the whole UserAPIKeyAuth object, whose team_metadata
holds the customer's own langfuse callback_vars. The only filter on the emitted
blob was a four key deny list written as a circular reference crash guard, so
those credentials reached the customer's own langfuse traces.
The emitted blob is now the StandardLoggingPayload allowlist plus the litellm
computed enrichments, and nothing is copied across from raw request metadata.
That makes the credential exclusion structural rather than a filter someone has
to keep correct. Steering keys keep reading raw metadata, matching literal_ai.
Proxy callers are unaffected: their request metadata already rides under the
allowlisted requester_metadata key, nesting intact.
debug_langfuse dumped raw request metadata into the trace as a second copy of
the same leak. It now emits caller scalars only.
When StandardLoggingPayload is absent the trace is still emitted with the
existing trace_id fallback, so failure traces survive.
* refactor(ui): migrate Navbar off antd to shadcn
Replaces Ant Design with the in-repo shadcn layer across every Navbar
component, removing the last antd imports from src/components/Navbar.
- CommunityEngagementButtons, NotificationsBell, ViewSwitcher, BlogDropdown,
WorkerDropdown and UserDropdown now compose @/components/ui primitives
- antd icons render at 1em while lucide defaults to 24px, so every icon
carries an explicit size class matching what it replaced
- UserDropdown uses Popover rather than DropdownMenu: its panel holds
switches and badges, and form controls inside role="menu" are invalid
- WorkerDropdown moves to Combobox since shadcn Select has no search
- drops the nine no-restricted-imports suppressions these files no longer need
* refactor(ui): migrate log details drawer off antd to shadcn
Replaces Ant Design across every source file under src/components/view_logs,
so the request log drawer and its viewers compose @/components/ui primitives.
- Drawer becomes Sheet, Collapse becomes Collapsible, Segmented and Radio.Group
become Tabs, Tag becomes Badge, Descriptions becomes a local grid helper
- every lucide icon carries an explicit size class, since antd icons render at
1em while lucide defaults to 24px
- two tests dropped assertions on antd internal class names in favour of
rendered text and roles, and the Pretty/JSON case now proves the toggle
actually swaps the body rather than only that both controls render
- drops the eslint suppressions these files no longer need
* fix: report real token usage on guardrail-blocked /v1/responses replies
## TLDR
Signed-off-by: Ishaan <ishaangupta0408@gmail.com>
* refactor(ui): migrate AI Hub off antd and tremor to shadcn
Replaces Ant Design and Tremor across src/components/AIHub, so the model,
agent, MCP and skill hub views compose @/components/ui primitives.
- Modal becomes Dialog, tremor TabGroup becomes Tabs, tremor Card and Table
become their shadcn counterparts, and Tag and tremor Badge become Badge
- the three publish forms wrapped antd Form around zero Form.Item fields, so
the wrapper became a div and the dead useForm and resetFields calls went
with it, rather than pulling in react-hook-form for a form with no fields
- antd Steps has no shadcn equivalent, so each form inlines a small ol stepper
- cells holding model names, server ids and URLs gained min-w-0 and break-words
so a long value cannot bleed into the neighbouring column
- the three form tests dropped assertions invented by their antd mocks in
favour of roles and rendered text
- drops the eslint suppressions these files no longer need
* fix(ui): announce the account popover as a dialog, not a menu
The panel holds switches and ordinary buttons rather than menu items, so
menu semantics promised keyboard behavior it does not provide.
* fix(ui): give the request details drawer an accessible name
Screen readers announced an unnamed dialog. The visible header is a custom
layout, so the title is visually hidden to keep the drawer layout unchanged.
* refactor(ui): migrate shared common_components off antd and tremor
Replaces Ant Design and Tremor in the six shared components under
src/components/common_components, which between them are reached by
nine routes.
- antd Table becomes the ui/table primitives, and the Actions column keeps
antd's fixed: "right" behaviour via a sticky cell
- Tremor Icon, Text and Badge become a plain span, p and StatusBadge
- antd Tooltip and Typography copyable become the shadcn Tooltip and the
shared CopyButton
- every public prop signature is unchanged, since these are shared components
and a renamed prop would break callers far from this folder
- two tests dropped assertions on antd internal class names and on DOM
structure, and gained cases proving a disabled action does not fire onClick
MemberTable keeps a type-only import of antd's ColumnsType because a consumer
annotates its own column array with it. No antd code ships from the file.
* test(ui): assert the publish button is disabled while submitting
The migration closed a double submit hole that antd left open, but the
rewritten tests only proved the flow had not completed, so removing the
guard would not have failed them. Verified by mutation: dropping
disabled={loading} fails exactly this case.
* refactor(ui): migrate key info and permissions views off antd and tremor
Replaces Ant Design and Tremor in the key info header and detail view, the
agent and vector store permission panels, and the team member permissions
table.
- antd Popover, Dropdown and Modal become HoverCard, DropdownMenu and Dialog,
and Tremor TabGroup becomes Tabs with keepMounted so panel state survives
a tab switch the way Tremor's did
- the key id copy control moves to the shared CopyButton, which also fixes an
icon that rendered at 24px because it inherited the heading font size
- antd Checkbox onChange becomes onCheckedChange
- every public prop signature is unchanged, since these are shared views
- three member permission tests were passing vacuously: they searched for an
unchecked box by reading .checked, which is undefined on a Base UI checkbox,
so the assertions sat inside an if that never ran. They now scope the
checkbox to its own row and assert the toggle, the save and the revert
- drops the eslint suppressions these files no longer need
* refactor(ui): migrate router settings and shared badges off antd and tremor
Replaces Ant Design and Tremor in the fallbacks views, the router general
settings panel, and the two shared banner and badge components.
- Tremor Card, Table and Icon become the ui/card, ui/table and lucide
equivalents, reproducing Tremor's icon box so click targets keep their size
- antd Alert becomes a composed role="alert" region, since the shadcn CLI's
alert pulls in class-variance-authority, which this repo does not have
- antd InputNumber becomes a native number input, and Switch onChange becomes
onCheckedChange
- shadcn TableCell ships whitespace-nowrap where Tremor's did not, so cells
holding model names and setting descriptions get whitespace-normal back
- adds a DeprecationBanner test covering naming, the link, and dismissal,
proven against the antd version first and mutation checked
- drops the eslint suppressions these files no longer need
* refactor(ui): move the model hub and model select onto shadcn primitives
Rebuilds public_model_hub, MakeSkillPublicForm, ModelSelect and the
guardrail LogViewer on the in-repo shadcn layer, so they inherit the
dashboard's design tokens instead of styling themselves through Ant
Design and Tremor.
Public prop signatures are unchanged, so no caller moves. The two
teams e2e steps that reached into antd's Select internals now drive
the combobox through its test id, role and data-slot instead.
* refactor(ui): give MemberTable its own extra-column type
extraColumns was typed as antd's ColumnsType while the adapter only
honoured string/ReactNode titles, plain-string dataIndex values and
element/string/number render results, so several valid antd column
forms produced blank cells. MemberTableColumn now describes exactly
what the table renders, and a column with a dataIndex but no render
falls back to the member value instead of rendering nothing.
* refactor(ui): move the shared dropdowns and selectors onto shadcn primitives
Rebuilds the thirteen form-free components under common_components on
the in-repo shadcn layer, so they inherit the dashboard's design tokens
instead of styling themselves through Ant Design and Tremor.
SearchSelect and the three dropdowns that wrap it now forward an
optional input id, so an antd Form.Item label still resolves to its
control. The e2e steps that reached into antd's Select and Modal
internals now go through the test id, role and data-slot.
* fix(model_prices): correct Gemini 2.5 shutdown dates and DeepSeek V4 max output tokens
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* test(ui): cover appending a second model in ModelSelect
The rewritten suite only ever picked one ordinary model, so a
regression that replaced the selection instead of appending to it
would have gone unnoticed. The case passes against the antd version
too, so it pins behavior the migration preserves rather than adds.
* test(ui): spread the real lucide-react module in the KeyInfoView mock
The mock returned only CopyIcon and CheckIcon, so any icon a child later
imports resolves to undefined. DeleteResourceModal now renders CircleAlert,
which broke all twelve cases in this file.
* fix(ui): hold the delete dialog open mid-deletion and keep unmatched select values
DeleteResourceModal let escape, the backdrop and the close button dismiss it
while the delete request was still in flight. SearchSelect blanked its field
whenever the value was missing from options, which happens while they load;
it now falls back to the raw value the way PaginatedSearchSelect already did.
* refactor(ui): move the root-level dashboard components onto shadcn primitives
Rebuilds nine components under src/components on the in-repo shadcn layer:
both banners, the navbar chrome, the onboarding link dialog, the model
filters, the model group alias table, the object permissions and logging
settings views, and the user dashboard grid. Every public prop signature is
unchanged, so no caller moves.
* revert(ui): keep the onboarding link modal on antd
The invitation dialog opens over the still-antd Invite User modal. Lifting
only the shadcn dialog content above antd's mask leaves its own backdrop
underneath, so an outside click reaches the wrong modal. Adding a second
backdrop stops that but does not restore dismissal, and the same hazard
already ships in three guardrails modals, so the stacking needs one shared
fix rather than a fourth local workaround.
* fix(databricks): surface prompt-cache token counts in streaming usage
chunk_parser built ModelResponseStream without passing usage, so the
cache_read_input_tokens and cache_creation_input_tokens that Databricks
returns for Anthropic models never reached the cost calculator. Every
streamed request was billed at the full input rate even when served
from cache.
ModelResponseStream already coerces a usage dict into Usage, which maps
those keys into prompt_tokens_details, so passing the chunk's usage
through is sufficient.
* refactor(ui): move the settings page and bulk user invite onto shadcn primitives
Rebuilds settings.tsx and bulk_create_users_button.tsx on the in-repo shadcn
layer. The settings callback form moves from antd Form to react-hook-form with
the shared Field primitives, and the CSV drop zone replaces antd Upload with a
native file input plus drag handlers. Both public prop signatures are
unchanged, so no caller moves.
* Remove comment about prompt-cache usage in test
Remove outdated comment regarding prompt-cache counts in chunk_parser.
* refactor(ui): move the cost tracking components onto shadcn primitives
Rebuilds the provider discount and margin tables, the pricing calculator and
its multi-cost results on the in-repo shadcn layer, and swaps the imperative
antd modal.confirm removals for AlertDialog. Row actions gained accessible
names, which replace the Tremor stub mocks the tests used to drive.
cost_tracking_settings keeps its two antd Modals and Forms, since they wrap
the two add forms that stay on antd for now.
* chore: retrigger e2e gate
* feat(proxy): serve Anthropic-native /v1/models for Claude Code gateway discovery (#35455)
* feat(proxy): serve Anthropic-native /v1/models for Claude Code gateway discovery
* refactor(proxy): move Anthropic model-list formatter into llms/anthropic/common_utils
* fix(proxy): make model_list request param optional for direct callers
* style: apply ruff format to changed lines
* style: satisfy ruff strict-rule budget (UP006, I001)
* style: satisfy type-discipline budget (LIT002 mutable-ok, LIT009 pyright ignore)
* style: satisfy LIT001/LIT010 and drop explanatory comment per contributor rules
* fix(proxy): translate team model names in the Anthropic /v1/models response
* ci: trigger buildkite status report
* feat(proxy): carry token limits into the Anthropic-native /v1/models entries
* fix(proxy): cast the injected request so the anthropic-version guard is a real comparison
* fix(proxy): explain the model listing casts so the type-discipline gate passes
---------
Co-authored-by: yuneng-jiang <yuneng@berri.ai>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
* fix(ui): keep the cost tracking removal confirmation open until it settles
The discount and margin removal confirmation used AlertDialogAction, which
renders AlertDialogPrimitive.Close and dismisses the dialog on click. The
dialog therefore disappeared while the removal request was still in flight,
leaving the admin with no sign that anything happened and free to fire a
duplicate removal.
Swap the confirm control for a plain destructive Button, track an isRemoving
pending state that disables Cancel and relabels Remove to "Removing...", and
clear the pending removal in a finally block once the request settles.
* test(ui): build the deferred removal with Promise.withResolvers
The pending-state test seeded its deferred promise by declaring the resolver
with let and reassigning it inside the executor. Promise.withResolvers is the
standard way to get the same handle without the reassignment, and the
assertions are unchanged.
* refactor(ui): declare DateRangePickerValue locally instead of importing it from tremor
DateRangePickerValue is a plain object shape, not a component, so the
twelve files that used it were each carrying a no-restricted-imports
suppression for a type that tremor declares as
{ from?: Date; to?: Date; selectValue?: string }.
Declare that shape in components/shared/date_picker_types.ts and point
every consumer at it, which drops ten suppressions from the baseline.
advanced_date_picker and usage_date_picker keep their tremor imports:
they still render tremor Button, Text and DateRangePicker, and moving
DateRangePicker itself needs react-day-picker.
* refactor(ui): move MCP permission panels onto shadcn primitives
Replaces antd Radio, Checkbox and Tooltip, plus Tremor Text and Badge,
with the in-repo shadcn equivalents across the three MCP permission
panels, and drops the no-restricted-imports suppressions they no longer
need. Also removes the stale suppression on settings.test.tsx, which
imports neither library.
The tool rows keep their existing click-to-toggle behaviour: the row
owns the toggle and the checkbox no longer carries its own change
handler, since Base UI replays the click through a hidden input that
reaches the row on its own.
Adds payload-level tests for the risk-group view covering group clear,
mixed-state re-arm, single-tool toggles from both the box and the row,
and a controlled round trip proving each control re-renders from the
permissions it emitted.
* feat(proxy): add per-component response cost headers
- Extract input_cost, output_cost, cache_read_cost, cache_creation_cost, reasoning_cost, and tool_usage_cost from logging object cost breakdown
- Populate x-litellm-response-cost-* component headers in ProxyBaseLLMRequestProcessing.get_custom_headers
- Ensure headers are omitted when cost breakdown is absent or values are None
- Add comprehensive test suite covering component headers, math invariants, caching, reasoning, and discounts/margins
* refactor(ui): migrate ten small dashboard files off antd and tremor
Moves the onboarding views, router settings inputs, tag rate limit editor,
fallback buttons, created-key display and the shared numerical input onto the
in-repo shadcn layer. Each control has a direct equivalent, so this is a
like-for-like swap with no layout changes and no new styling.
Router settings saves by reading input values straight off the DOM with
document.querySelector('input[name="..."]'), a path no test covered. Adds a
regression test that types into a field and asserts the typed value reaches
the payload, so the name attribute contract stays enforced.
Also adds tests for TagRateLimitEditor, which had none and whose RPM cell
switched from antd InputNumber to a native number input.
* refactor(ui): drop explanatory comments from the migration tests
* refactor(ui): drop narration comments from the MCP permission panels
* fix(main): an explicit provider outranks a known OpenAI model name (#36800)
* fix(main): an explicit provider outranks a known OpenAI model name
completion() picks the OpenAI handler whenever `model in
litellm.open_ai_chat_completion_models`, and that clause is evaluated before the
gemini and vertex_ai branches. get_llm_provider() already resolves those names
to "openai", so the clause only adds anything when the provider is something
else, and then it silently overrides it: the config built for the requested
provider is handed to the OpenAI handler.
For gemini that is fatal. VertexGeminiConfig.transform_request raises
NotImplementedError by design, since Vertex builds its request in its own
handler, so `gemini/gpt-4o` dies in async_transform_request before anything is
sent. register_model() reaches the same state without an odd model id: an entry
claiming litellm_provider "openai" adds its name to
open_ai_chat_completion_models, so one mislabelled pricing entry reroutes every
later call to that model in the process.
The name clause now applies only when no other provider was resolved.
* test(main): move the routing regression into the mapped test file
CLAUDE.md asks bug fixes to extend the mapped test file, so these belong in
tests/test_litellm/test_main.py rather than a module of their own.
They also no longer swap out the provider handler objects. Both Gemini cases
inject an HTTPHandler whose post() answers like generativelanguage does, then
assert the URL the request went to and read the reply back; the OpenAI case
injects an OpenAI client and patches its own raw-response create. That asserts
the endpoint the call reaches instead of which attribute the test replaced, and
matches the neighbouring tests in the file.
* fix(exception_mapping): bare 429 in an error body no longer outranks the status code (#36705)
is_error_str_rate_limit treats any standalone 429 in the stringified exception as
a rate limit, and for openai-compatible providers that check runs before the
status-code branch. Providers echo the request back in validation errors, so a
400 whose body happens to contain a 429 comes out as RateLimitError.
Tokenised prompts hit this routinely, since 429 is an ordinary token id (" that"
in several tokenisers) and an echoed prompt_token_ids array is enough:
{"error":{"message":"`tools` must not be an empty array",
"type":"invalid_request_error","code":400},
"prompt_token_ids":[9906,429,1234]}
The mislabel is not cosmetic. RateLimitError tells callers and routers to retry,
so a request that cannot succeed gets replayed, and the failure is booked against
provider throttling rather than the caller. Against DeepInfra, one recurring 400
("`tools` must not be an empty array") came back as a rate limit in 77 of 198
occurrences, the split depending only on whether the echoed prompt contained 429.
16482 narrowed '"429" in error_str' to \b429\b after a false positive on
'asbjdad429addad'. Word boundaries cannot separate a real 429 from a token id, so
the same class of false positive survives.
is_error_str_rate_limit now takes an optional status_code, and the bare-number
branch fires only when no explicit status contradicts it. The status is read off
an arbitrary exception, so a non-integer is treated as unknown and left to the
existing behaviour. The repo has a single call site.
The phrase branches are untouched, so a provider reporting a real rate limit in
the message text under a non-429 status still maps to RateLimitError (11455).
This is not "status code wins".
Tests cover the matcher (suppressed under a 400; still detected with no status,
None, 429, or a non-integer status; phrase honoured under a 400) and
exception_type end to end (400 with 429 in the echoed body -> BadRequestError,
real 429 -> RateLimitError). Reverting the source change fails the latter.
* fix(ui): distinguish hosted and local vLLM in the provider dropdown
* test(vector_stores): drop redundant route-map comment
* fix(proxy): force prisma recreate on postgres cached-plan error (#36428)
`_query_first_with_cached_plan_fallback` recovers from Postgres's "cached
plan must not change result type" by recreating the Prisma client, which
drops both the server-side plans and the engine's client-side statement-name
cache. Since #30183 the shared reconnect path probes the writer with
`SELECT 1` first and skips the recreate when it answers, which is right for
the IAM token refresh it was added for and wrong here: the connection is
healthy, it is the session's prepared statements that are stale, so the probe
always passes and always vetoes the recreate. Callers now pass
`force_recreate` to skip that probe, and only the cached-plan fallback does.
Getting past the probe is not enough on its own. Both cooldown checks would
still skip the recreate for 15 seconds after any earlier reconnect, which
outlives the 10 second auth retry window, so a migration landing in that
window kept 503ing. `force=True` would fix that but would also let every
concurrent caller of the same burst kill the engine the first one just built.
The caller instead names the engine it observed before the query, and the
cooldown is waived only while that engine is still the live one, so the first
caller repairs the pool and the rest fall back to the normal cooldown.
That engine has to be the one the query actually ran on. `query_first` is a
top-level read, so with a read replica configured it is dispatched to the
reader and it is the reader's prepared statements that go stale, while
`writer_db` names a different engine with its own counter. The observation
and the cooldown comparison both go through `read_db`, added alongside
`writer_db` and backed by a `read_target` property on the routing wrapper
that `__getattr__` now dispatches through so the two cannot drift.
The observation carries the wrapper, not just its generation. `read_db`
resolves to the reader while it is available and to the writer once it is
not, and those counters are independent and both start at zero, so comparing
a bare number across that switch pits one engine's counter against another's.
Equal by coincidence waives the cooldown for an engine already replaced;
unequal gates a caller that needs the recreate. Identity settles it, and is
sound because the engine object is never re-pointed without the generation
also moving.
Three smaller holes on the way out. The waiver is withdrawn once a repair of
that same engine has been tried and failed, so a burst collapses onto one
attempt instead of each caller running its own recreate serially; the record
is keyed per engine rather than counted globally, so an unrelated reconnect
failure cannot suppress a stale reader's recovery and a writer failure cannot
evict the reader's record. And a forced recreate that the optimistic-lock
guard declines is no longer reported as a success on either the direct or the
heavy path, since the routing wrapper leaves the reader untouched in that
case; a decline is deliberately not counted as a failure, so the caller's own
backoff still gets its waiver on the next attempt.
A decline on the heavy path clears the dead-engine flag before raising. The
clear after the cycle is skipped by any raise, which is right for a failure
and wrong here, and the non-forced path already clears it on a decline, so
this restores that policy rather than inventing one. Stranding the flag would
route the next cycle back down the probe-free heavy branch, where the
refreshed generation matches and the recreate kills the healthy engine a
refresh just spawned, which is #29176.
Clearing that flag is necessary and not sufficient. The escalation check
re-arms it whenever the consecutive-failure count sits at the threshold, so a
decline that left the count alone sent the very next attempt back down the
same path. A decline is raised only at the generation guard, and the
generation moves only after a replacement connects, so a decline is proof
that a replacement succeeded and the count is reset on it.
Fixes #36418
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(transcription): stop a zero output rate from zeroing transcription cost (#36914)
cost_per_second treated a declared-but-zero output_cost_per_second as a real
rate, so the output branch claimed the call and the elif locked out
input_cost_per_second. Every transcription model shipping
output_cost_per_second 0.0 next to a real input rate billed $0, which covers
43 of the 55 per-second entries in the cost map: all 36 deepgram models, both
assemblyai, both elevenlabs scribe, both groq whisper and azure-stt. Custom
deployments pairing the two fields the same way billed $0 as well
Take the output branch only when that rate is actually billable, so a zero
falls through to the input rate. Entries that duplicate one rate into both
fields, whisper-1 among them, keep billing exactly what they bill today
* refactor(caching): accept read-only sequences for redis rpush pipeline payloads
Keeps the spend buffer restore path free of mutable-collection construction so the type discipline gate stays within its LIT002 ceiling.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* Revert "fix(auth): stop the team fallback from widening model access (#36837)"
This reverts commit ab2333b6c4d0fed2d78c55352a7c9d5aa53aea19.
Every Admin UI login mints its session key against the sentinel team_id
`litellm-dashboard`, and no LiteLLM_TeamTable row is ever created for it.
That lookup is therefore a provably-absent row on every UI request, which
#36837 turned into a hard refusal with no override, so the whole dashboard
404s.
Reverting restores the token-derived fallback. The model-access widening
#36837 closed is reopened and needs a re-land that exempts the UI sentinel
team.
* fix(langfuse): gate update_trace_keys behind an operator setting (#36862)
update_trace_keys lets a caller name which request metadata entries get copied
onto an existing trace, and the name is unrestricted. Sending
update_trace_keys: ["user_api_key_auth"] with existing_trace_id serializes the
resolved auth object, including the team callback credentials it carries, onto
the trace through Langfuse.trace(**trace_params). TraceBody is Extra.allow, so
an unexpected key ships rather than being dropped.
Any holder of a team key can do this and read the result in the destination the
team already logs to, so the feature is now inert unless an operator turns it on
with langfuse_enable_update_trace_keys.
* fix(fireworks_ai): let extra_body thinking/reasoning_effort take precedence over chat_template_kwargs
* fix(ui): show zeroed auto-router usage stats when a window has no sessions (#36868)
* fix(ui): show zeroed auto-router usage stats when a window has no sessions
* test(ui): assert the muted track on the empty share-of-turns bar
* fix(mcp): keep admin-entered oauth endpoints in management reads (#36888)
* fix(mcp): keep admin-entered oauth endpoints in management reads
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* test(mcp): cover configured oauth endpoints on the config load path
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(ui): match the MCP servers count badge to its sibling permission badges
The Object Permissions section rendered the MCP Servers badge with shadcn's
default variant (solid bg-primary), so a plain count showed up as a black pill
next to the light Vector Stores and Agents counts. Counts now use secondary
everywhere, and destructive stays reserved for the blocked state.
* refactor(ui): drop the explanatory comment from the badge variant test
* fix(anthropic): bill undetailed iteration cache writes at the 5m rate
* fix(cost_calculator): mirror the anthropic geo uplift in the token-type cost breakdown
* fix(openai,azure): return a length-truncated 200 when the output budget fits no token (#36859)
OpenAI and Azure GPT-5.x answer a chat request whose output budget cannot fit a
single visible token with a 400, while the same models return a length-truncated
200 one or two tokens higher. Agents that probe a model with a hardcoded
max_tokens of 1 read that 400 as "model unavailable".
The four chat request helpers now recognise the provider's own sentence and hand
back the length-truncated response the provider gives at a slightly larger
budget: finish_reason "length", empty content, zero completion tokens. Any other
400 still raises. Streaming is covered by the same seam, and the caller's budget
is never raised on their behalf.
The provider bills the prompt it processed but sends no usage object with the
400, so the prompt tokens are estimated with the same token_counter every other
usage-less path uses. Reporting zero would let a caller send an arbitrarily
large prompt with max_tokens 1 and be charged nothing.
* fix(proxy): always emit the Anthropic /v1/models token limits, null when unknown (#36961)
Anthropic's Models API declares max_input_tokens and max_tokens as nullable, not
optional, and the live vendor endpoint returns both keys on every entry. The
merged Anthropic-native listing dropped either key whenever LiteLLM could not
resolve a limit, so a client validating against a nullable-but-required schema
saw a malformed entry for any model the cost map does not know.
* feat(helm): add startupProbe and hpa.behavior to the componentized chart (#36382)
Two small pod-spec passthroughs the componentized chart was missing, both
additive and empty by default so existing renders are unchanged:
- gateway/backend/ui deployments gain a `startupProbe` knob (same
`{{- with }}` toYaml pattern as liveness/readiness), to gate liveness during
a slow cold start without a kill loop.
- gateway/backend/ui HPAs gain an `hpa.behavior` passthrough rendered verbatim
under spec.behavior (scaleUp/scaleDown policies + stabilization windows).
Tests: extend probe_tests.yaml (startupProbe absent by default / renders
verbatim) and add hpa_behavior_tests.yaml. Full chart suite: 76 tests pass.
Signed-off-by: Louis Vauterin <louis.vauterin@doctolib.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(vector_stores): classify write endpoints before reads on substring collisions
* fix(router): stop get_router_model_info from wiping cached pricing
Merge deployment model_info into a copy of the lru_cache'd get_model_info() dict and drop unset Nones, so Deployment's mirrored pricing defaults no longer overwrite built-in prices process-wide.
Fixes #36980
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(bedrock): resolve aliases in batch file records
* fix(caching): tolerate SSE chunk splits in anthropic stream cache writer
* fix(proxy): return cost breakdown header values as a named tuple
* fix(responses_api): map bridged chat usage on guardrail-blocked replies
Move the blocked-usage mapping for /v1/responses next to
blocked_response_usage in guardrail_translation utils, map bridged chat
prompt/completion tokens to Responses API input/output tokens, and let
raise_passthrough_exception attach the blocked response so post-call
guardrail blocks report real usage
* fix(proxy): serve aggregate MCP endpoint on bare /mcp instead of 307-redirecting (#34845)
The MCP sub-app is attached with app.mount("/mcp", ...) and a Starlette
mount never matches its bare prefix, so POST /mcp fell through to the
router's redirect_slashes 307. Behind a TLS-terminating ingress whose
peer address is not in uvicorn's forwarded-allow-ips (default: loopback
only) the redirect Location is built from the socket scheme as http://,
and MCP clients strip the Authorization header on the cross-origin
follow, so reconnects fail with ECONNRESET right after a successful
OAuth flow. The redirect also fires before auth, so the bare spelling
never returns the RFC 9728 WWW-Authenticate challenge that OAuth
clients need to start the flow.
Add an explicit /mcp route beside the existing /toolset/{name}/mcp and
/{name}/mcp spellings, forwarding to handle_streamable_http_mcp with
the same scope rewrite those routes already use (path=/mcp,
_original_path preserved for OAuth challenge URL selection). When the
mcp package is unavailable the route 404s, matching what the bare
sub-app serves on /mcp/ in that state. /mcp/, /mcp/{server},
/{server}/mcp and /toolset/{name}/mcp spellings are unchanged; the
exact-match route and the mount have disjoint match sets so
registration order cannot matter.
* fix(cost): reach tiered pricing for models without top-level per-token rates
* feat(shadow_eval): add reverse-direction shadow eval jobs (#36865)
Shadow eval only answered "should this key adopt this auto-router". Once a key
is on the router it is invisible to the feature, because the sampling gate skips
any request the shadowed router already served, so post-adoption quality
regressions go unmeasured.
Reverse mode inverts the arms: sample the traffic the router did serve and
duplicate it against a fixed baseline_model, judged by the same blind pairwise
judge. Same job table, same attempt rows, same aggregates.
real_* stays the arm the caller was served and shadow_* the duplicated one, so
in reverse real_model is the router's pick and shadow_model is the baseline. The
active-job slot becomes one per (key, direction) so both directions can run at
once, and tier attribution in reverse reads the control request's routing
decision rather than the shadow call's write-back.
* fix(fireworks_ai): move top-level thinking into extra_body on the text completion path
* feat(search): add Nimble as a search provider (#36347)
* feat(search): add Nimble as a search provider
Adds `NimbleSearchConfig` so `search_provider: nimble` works across the SDK,
the proxy /v1/search endpoint, the Search Tools dashboard, and spend tracking.
Nimble's /v2/search already uses the Perplexity unified spec's parameter names,
so the request transform is close to a pass-through. `search_domain_filter`
splits into include_domains/exclude_domains on the spec's `-` prefix, `country`
is upper-cased to the ISO form Nimble documents, and everything else is
forwarded so focus, search_depth, time_range and the rest stay reachable. On the
response side, snippet prefers `content` and falls back to `description`, and a
malformed body raises an attributed error rather than reporting an empty search.
Also tightens `BaseSearchConfig.get_supported_perplexity_optional_params` to
return `frozenset[str]` instead of a bare mutable `set`, which every caller
already treats as read-only.
* fix(search): surface Nimble error bodies instead of empty results
Greptile flagged that a null or absent `results` degraded to a successful empty
search. A search with no hits comes back as `"results": []`, verified against the
live API, so the field is now required and anything else raises the attributed
schema error the other malformed bodies already take.
Also unwraps Nimble's second error envelope. Collection failures return
`{"success", "task_id", "message"}` rather than the `{"detail"}` shape validation
errors use, and only the latter was being read.
Drops comments that restated the adjacent code.
* docs(search): drop the Nimble param list from the transform docstring
It restated the vendor's API reference, which the module docstring already links,
and would go stale the moment Nimble adds a focus mode.
* fix(bedrock): fall back to the batch deployment model for unmapped record models
* fix(cost-tracking): count dict-shaped web_search_call output items
* fix(mcp): drop caller host and configured upstream headers from logged metadata (#36901)
* fix(mcp): drop caller host and configured upstream headers from logged metadata
The synthetic request that carries MCP client headers into
add_litellm_data_to_request forwarded the caller's Host header, and
Request.url is built from it, so a caller chose the proxy_server_request
url and the metadata endpoint that every logging callback records.
_upstream_credential_headers also only knew the configured client side
auth header and the x-mcp- prefix family, so a header name declared in
mcp_servers.<name>.extra_headers reached logging metadata in cleartext.
Those names are admin chosen, so no prefix rule can recognize them; read
them off the server registry instead. The header is still forwarded
upstream, which is what extra_headers is for. authorization is left out
because clean_headers already strips it and claiming it here would move
authenticated_with_header on the oauth passthrough config.
The Responses bridge tests stub the server manager, so their fakes gain
the registry accessor the sanitizer now reads.
* fix(mcp): drop caller host from the sanitized header mapping too
The synthetic request stopped forwarding host, but the parallel sanitizer
did not, so a forged hostname still reached the guardrail payload and the
list_tools spend row. Drop it there as well.
Exempt the configured identity headers from the upstream credential set.
get_user_from_headers resolves end user attribution off the same request
this module reconstructs, and it only fills end_user_id when auth left it
unset, so claiming user_header_name or a user_header_mappings name would
lose attribution on the MCP paths that authenticate upstream.
Drop the isinstance guard on extra_headers entries: the field is typed
list[str], so the check is dead and basedpyright scores it.
* fix(mcp): accept a bare user_header_mappings entry when exempting identity headers
get_internal_user_header_from_mapping and get_customer_user_header_from_mapping
both normalize a single mapping to a one element list, and config_settings.md
documents the key as a dict. Iterating the bare form yields its keys instead,
so the exemption silently matched nothing and an identity header also named in
an MCP server's extra_headers was dropped after all.
* fix(router): honor tiered_pricing set in a deployment's litellm_params
* feat(scripts): queue heavy gates behind a machine-wide slot lock
* test(proxy): assert production nesting semantics for component cost headers
* fix(cost): bill reasoning tokens at the selected tier's reasoning rate
* fix(vertex_ai): fail an embeddings batch entry whose fan-out came back incomplete
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(router): merge model_info without new mutable constructions
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(cost-tracking): price web search on dated search-preview map entries
* fix(proxy): emit uncached input cost so component headers sum to the total
* refactor(ui): re-sync badge and skeleton onto the base-vega shadcn style
components.json has declared "style": "base-vega" since cfe9e39e55, but badge
and skeleton were added a few days earlier under new-york and never re-synced,
so both still carried the previous style's classes. Badge's destructive variant
rendered as solid red with white text instead of the tinted wash the rest of the
dashboard uses, which is already the convention for Button
Re-runs npx shadcn add for both and keeps the two local deltas the registry
cannot supply: cva comes from @/lib/cva.config, since class-variance-authority
is not a dependency here, and both stay wrapped in React.forwardRef, which the
tripwire in tests/setupTests.ts requires until the React 19 upgrade
Adds Badge to ref-forwarding.test.tsx. Nothing covered it before, even though
two TooltipTrigger sites compose over it, so the wrapper could have been dropped
by the next re-sync without a single test going red
Retargets one assertion in LogDetailContent.test.tsx. It regex-matched the whole
class string for "destructive" to prove a tag was not alarming red, which the
restored aria-invalid classes now satisfy for every variant; it checks the
variant attribute and red utility classes instead
* test(vertex_ai): cover duplicated fan-out rows in embeddings batch reassembly
Also ruff-formats the batch transformation test file, which the formatter
gate flags once the file is touched.
* test(ui): assert cache and retry tags by text instead of class name
Three assertions in LogDetailContent.test.tsx matched a regex against the
rendered class string to prove a tag was green or was not red. That pins styling
rather than behavior, and jsdom does not resolve the utilities anyway, so the
checks only ever proved that a substring survived into the class attribute
The badge re-sync exposed it: base-vega's base string carries aria-invalid
variants of the destructive token, so a "not destructive" regex started matching
every badge regardless of variant
Each one now asserts the tag's text is present, which is what the surrounding
cases already do and what the user actually observes
* fix(ui): stop the models tab strip from scrolling vertically
The tab strip carried overflow-x-auto directly on the TabsList. CSS forces
overflow-y from visible to auto once overflow-x is not visible, and the line
variant's active-tab underline is an absolutely positioned ::after that hangs
5px below its trigger, so the strip picked up a pixel of vertical scroll on top
of the horizontal scroll it actually wants.
The scroll container now lives on a wrapper whose bottom padding leaves room for
the underline, offset by a matching negative margin so the row keeps its exact
geometry.
* fix(anthropic_messages): make tool_result images visible to OpenAI-compatible providers (#34462)
Images nested inside an Anthropic `tool_result` block were dropped when the
request was adapted for an OpenAI-compatible provider, because the OpenAI tool
message shape only carried text. Hoist those images out of the tool result and
into a following user message so the model can still see them, and widen the
tool message content type to accept image parts.
* fix(ui): anchor chips-combobox popups to the field instead of the inner input
Base UI positions a combobox popup against the Combobox.Input by default. In
chips mode the visible field is the ComboboxChips wrapper and the input is a
smaller box nested inside it, so every chips-combobox in the dashboard opened
its popup 11px right of the field and 17px past its right edge.
shadcn ships the wiring for this and their combobox-multiple example uses it:
useComboboxAnchor on the chips container, passed to ComboboxContent as anchor.
The anchor prop also drives data-chips, which cancels the extra min-width an
ordinary combobox wants. Every chips site in the dashboard omitted it.
The anchor is attached through Base UI's render prop rather than a plain ref,
because React 18 drops refs on function components and ComboboxChips is one.
Adds MultiSelect's first test, covering the anchor wiring plus selection,
chip rendering and custom values.
* test(ui): assert which element the chips-combobox popup anchors to
The previous assertion read data-chips, which is derived from the anchor prop
being truthy, so it stayed true even when the ref never reached the DOM and the
popup was still anchored to the inner input.
Stub distinct widths on the chips container and the input, then read the width
the positioner resolved. Reverting the anchor wiring now reports the input's
width instead of the field's, which is the actual bug.
* refactor(cost): make the shared token-details parsers public
parse_prompt_tokens_details and parse_completion_tokens_details are imported
by four modules, so the leading underscore made every import a
reportPrivateUsage violation
* fix(ptu): clear a PTU deployment's tiered_pricing instead of zeroing it
tiered_pricing is a list, so the 0.0 the flat-rate zeroing stores does not
even validate. Supplying tiers alongside PTU config gets the same 400 as a
flat rate; tiers already stored are dropped from both blobs
* fix(ptu): empty a PTU deployment's tiered_pricing instead of dropping it
Dropping it falls back to the public cost map's tier table, whose rates outrank the
zeros written beside them, so a PTU deployment on a tiered model keeps billing its
traffic per token. Stored empty, the tiers no longer apply and the zeros win
* fix(cost): fall back to the model output rate when a tier omits one
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(cost): inherit the backend output rate when a deployment's tiers omit one
* fix(dashscope): honor the model reasoning rate when a tier omits output rates
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(proxy): requeue spend logs when the DB write fails with a transport error (#36716)
* fix(proxy): requeue spend logs when the DB write fails with a transport error
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(proxy): hardcode the spend log queue cap and drop the stale re-export
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* refactor(proxy): keep the spend log requeue within the type discipline budget
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(proxy): apply the spend log queue cap to producer appends too
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(proxy): lower the spend log queue cap to 1k and make it env configurable
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(proxy): bound the spend log queue by bytes instead of row count
A row cap cannot bound memory: a row carries the whole prompt under store_prompts_in_spend_logs, so a cap that rides out an outage of counter-only rows is an OOM once prompts are stored. Every enqueue and dequeue now goes through one pair that tracks what the queue costs and drops the oldest rows past a 64 MB budget.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(proxy): make the spend log queue byte budget env configurable
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(proxy): use a string default …
TLDR
Problem this solves:
How it solves it:
User Flow
Before: a developer's team key keeps working after its team can no longer be resolved, and it now reaches models the team never allowed
gpt-4o-miniand issues them a key that inherits the team's models"model": "gpt-4o-mini"and get a 200 with a completion"model": "gpt-4.1"and get a 403 readingteam not allowed to access model. This team can only access models=['gpt-4o-mini']"model": "gpt-4.1"again and now get a 200 with a realgpt-4.1completionAfter: the same key stops at the point its team can no longer be resolved
gpt-4o-miniand issues them a key that inherits the team's models"model": "gpt-4o-mini"and get a 200 with a completion"model": "gpt-4.1"and get a 403 readingteam not allowed to access model. This team can only access models=['gpt-4o-mini']"model": "gpt-4.1"again and now get a 404 readingTeam doesn't exist in dbRelevant issues
Linear ticket
Resolves LIT-5522
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Live proxy against a real Postgres and real OpenAI calls. One setup serves every leg: a team restricted to
gpt-4o-miniwith a key that inherits the team's allowlist, which is what the Admin UI produces by default, so the team object is the only gate on model accessEvery request below is the same shape, varying only the key and the model
Three team states are exercised. HEALTHY is the ordinary case. GONE is a team removed while its key survives, which is also what a partial restore leaves behind. UNREADABLE is a team whose row is present but will not load, which is what a database fault looks like from here
Before, at commit
373a0fc506The control succeeds outright and the ungranted model is refused, so the allowlist is demonstrably in force before anything is disturbed. The proxy log records the decision rather than leaving it to be inferred from where the request landed
After, at commit
a45fbba01aThe decisive pair, both with
allow_requests_on_db_unavailable: true, same proxy and same request. A degraded read still serves, and a definitive answer still refusesType
🐛 Bug Fix
Review notes
Why the fix reaches into
get_team_objectrather than living entirely at the call site. That function reported a deleted team and a database that would not answer as the same 404, so the fallback had no way to tell a definitive answer from a degraded read. The first version of this PR tried to bound that withallow_requests_on_db_unavailable, which was wrong: that helper is() -> boolwith no exception parameter, a static settings read, and every other caller in the tree pairs it withis_database_connection_error(e). Using only the static half meant an operator who set it for outages would also have handed a deleted team's key the permissive fallback with the database perfectly healthy, which is the case this PR exists to close. An earlier revision of this description published that as proof the opt-out worked, when it was demonstrating the hole, and the GONE leg with the opt-in set is its corrected counterpart. The subclass is what makes consulting the setting legitimate, because by the time it is read the failure is known to be a degraded readWho this denies, stated plainly rather than left qualitative. The refusal keys on the token carrying no team model grant, and a genuinely unrestricted team also records
models: [], which is byte-identical to no grant. So the population that loses access is a key under an unrestricted team, and only while that team's row will not load. Unrestricted is a common team shape rather than an edge case, so this is a wider set than "a deleted team": any team created without an explicit model list looks the same to this check. It costs them nothing while the database is healthy, because the row resolves and this code never runs, andallow_requests_on_db_unavailablerestores the previous behaviour for exactly that case, as the UNREADABLE legs showThe full CI matrix is itself the best evidence that the closed case holds no real traffic. Two legacy tests failed, and they are the only two places in the repo that exercise the widening state, and only because their database is a stub, so their team lookup always fails. Both were repaired by giving their team key a model grant, which is the realistic shape of a team key: that changes the fixture's realism, not the test's meaning, and every assertion in them is untouched
Test isolation worth knowing about: both mapped test files call
importlib.reloadonauth_checks, which rebinds the exception class, so a test importing it from there and injecting it into a patched lookup raises a type the guard has never seen. The new call-site tests import it from the module under test instead. Verified by running both files in one session in both orderingsMutation check, five mutants, each run separately with bytecode cleared and a nonzero test count each time. Downgrading the subclass at the raise site, and dropping its pass-through in
get_team_object, each kill the discriminator test. Removing the absent-team guard at the call site kills both parametrized refusal cases. Making the token grant always vouch, and making the opt-out never apply, each kill their own case. The first of those mutants SURVIVED an earlier revision, which is what exposed that the call-site tests were injecting the exception rather than provoking it, so a test that drives the real lookup was addedOne typing note so it does not read as an oversight: this adds a single
reportUnknownMemberTypefrom readingteam_models, which is annotated as a bareliston the shared token model. The rule's codebase ceiling is 39237 and the gate fails only when a rule is both over its limit and above base, so this is well inside it. Tightening that annotation would have turned a security fix into a change to a shared model, which is the wrong trade hereCaveats (if any)
allow_requests_on_db_unavailablecovers only the unreadable caseFinal Attestation