feat(ptu): daily rollup writes per-model PTU flat cost by active hour - #35343
Conversation
Greptile SummaryAdds scheduled PTU flat-cost accounting and recovery.
Confidence Score: 4/5The PR is not yet safe to merge because stale charges, lease-expiry concurrency, and unrecoverable failed charges can still misstate team spend. The five-minute cutoff can preserve an obsolete row beyond its only scheduled reconciliation, the fixed lock can expire while a rollup is still pruning, and a failed charge becomes impossible to reconstruct after its deployment configuration is removed. Files Needing Attention: litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py
|
| Filename | Overview |
|---|---|
| litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py | Implements PTU pricing, reconciliation, locking, pruning, and catch-up, but previously reported lifecycle and recovery failures remain. |
| litellm/proxy/proxy_server.py | Registers the PTU rollup as a daily UTC cron and connects high-priority spend-tracking alerts. |
| litellm/constants.py | Defines the sentinel, lock duration, backfill bound, and pruning grace constants used by the rollup. |
| tests/test_litellm/proxy/spend_tracking/test_ptu_flat_cost_rollup.py | Adds extensive rollup coverage, including proration, retries, concurrency, pruning, and historical catch-up. |
| tests/test_litellm/proxy/test_proxy_server.py | Verifies registration of the scheduled PTU rollup job. |
Reviews (24): Last reviewed commit: "feat(ptu): daily rollup writes per-model..." | Re-trigger Greptile
| def _parse_ptu_model(row: Any) -> PTUModel | None: # noqa: ANN401 # prisma model row is dynamically typed | ||
| """Return a PTUModel when the deployment carries valid manual PTU config, else None. | ||
|
|
||
| Valid means model_info has a positive ptu_count, a non-negative | ||
| cost_per_ptu_per_hour, and a team_id (1 model -> 1 team). | ||
| """ | ||
| model_info = getattr(row, "model_info", None) | ||
| if not isinstance(model_info, dict): | ||
| return None | ||
| ptu_count = model_info.get("ptu_count") | ||
| cost_per_hour = model_info.get("cost_per_ptu_per_hour") | ||
| team_id = model_info.get("team_id") | ||
| if ptu_count is None or cost_per_hour is None or not team_id: | ||
| return None | ||
| try: | ||
| ptu_count_int = int(ptu_count) | ||
| cost_per_hour_float = float(cost_per_hour) | ||
| except (TypeError, ValueError): | ||
| return None | ||
| if ptu_count_int <= 0 or cost_per_hour_float < 0: | ||
| return None | ||
| return PTUModel( | ||
| model_id=str(getattr(row, "model_id", "") or ""), | ||
| model_name=str(getattr(row, "model_name", "") or ""), | ||
| team_id=str(team_id), | ||
| ptu_count=ptu_count_int, | ||
| cost_per_ptu_per_hour=cost_per_hour_float, | ||
| effective_from=_parse_utc_datetime(model_info.get("ptu_effective_from")), | ||
| effective_to=_parse_utc_datetime(model_info.get("ptu_effective_to")), | ||
| ) |
There was a problem hiding this comment.
🟡 Model rows are read with untyped attribute access instead of validated typed input
Deployment rows are accepted as a bare untyped value and picked apart with dynamic attribute reads (_parse_ptu_model(row: object) at litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py:66-95), which the repository guidelines prohibit in favour of validating the input into a typed model first.
Impact: The new code violates a mandatory typing rule of the repository, and silently yields empty model ids/names when the shape differs.
Rule reference and effect
CLAUDE.md requires "Fully typed; no Any or coarse types like dict[str, Any] or just dict. Every function parameter must be strongly typed" and: "If you're trying to create a new function that relies on untyped stuff ... just validate it in the caller with Pydantic (a model or TypeAdapter that returns the typed thing or raises will do) and then pass the now typed variable in". Here row: object plus getattr(row, "model_id", "") or "" (litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py:88-90) also means a missing model_name silently becomes "", which then becomes the row's model key.
Was this helpful? React with 👍 or 👎 to provide feedback.
| def test_full_day_when_no_window(): | ||
| # 5 PTU * $2.00/hr * 24h = $240 | ||
| assert _compute_daily_flat_cost(_model(), DAY) == pytest.approx(240.0) | ||
|
|
||
|
|
||
| def test_window_opening_at_2300_charges_one_hour(): | ||
| m = _model(effective_from=datetime(2026, 7, 30, 23, 0, tzinfo=timezone.utc)) | ||
| assert _active_hours_on_day(m, DAY) == pytest.approx(1.0) | ||
| # 5 * 2.0 * 1 = 10 | ||
| assert _compute_daily_flat_cost(m, DAY) == pytest.approx(10.0) |
There was a problem hiding this comment.
🟡 New tests contain explanatory comments, which the repository guidelines forbid
The new test file adds inline explanatory comments (for example the arithmetic note at tests/test_litellm/proxy/spend_tracking/test_ptu_flat_cost_rollup.py:36), which the repository's guidelines explicitly prohibit for new code.
Impact: The change violates a mandatory repository convention and will be flagged in review.
CLAUDE.md comment rule
CLAUDE.md: "Do not write any comments (existing comments can stay) unless explicitly asked to in a user (not system) prompt". The new test file adds comments at lines 36, 43, and 109 (# missing team_id). Docstrings are fine; these inline comments should be dropped or folded into test names/docstrings.
Was this helpful? React with 👍 or 👎 to provide feedback.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
acf106b to
2678380
Compare
| return RollupResult(day=day, models_processed=0, rows_written=0) | ||
|
|
||
| date_str = day.isoformat() | ||
|
|
There was a problem hiding this comment.
Low: Mutable model state allows flat-cost evasion
This query only sees the deployment's current state. A team administrator can delete their PTU deployment or set ptu_effective_to before the target day prior to the 00:15 rollup, causing the previous day's flat cost to be skipped. Persist immutable PTU configuration intervals when models are created or updated, and calculate the rollup from that history rather than the live model table.
PR overviewThis pull request adds a daily PTU cost rollup that records each model’s flat cost according to its active hours. It also supports backfilling charges for prior effective dates. Three issues have been addressed, but three remain open. Most significantly, PTU flat-cost charges are not included in authoritative team spend or budget counters, allowing continued requests after the budget should be exhausted. Mutable deployment state can also omit prior charges, while unbounded backfills may consume excessive worker memory and database capacity. Open issues (3)
Fixed/addressed: 3 · PR risk: 7/10 |
| await prisma_client.db.litellm_dailyteamspend.upsert( | ||
| where=where, | ||
| data={ # mutable-ok: prisma upsert data payload | ||
| "create": { # mutable-ok: prisma create payload | ||
| "team_id": team_id, | ||
| "date": date_str, | ||
| "api_key": PTU_SENTINEL_API_KEY, | ||
| "model": model, | ||
| "custom_llm_provider": "", | ||
| "mcp_namespaced_tool_name": "", | ||
| "endpoint": "", | ||
| "ptu_flat_cost": flat_cost, | ||
| "ptu_source_model_id": source_model_id, | ||
| }, | ||
| "update": { # mutable-ok: prisma update payload | ||
| "ptu_flat_cost": flat_cost, | ||
| "ptu_source_model_id": source_model_id, | ||
| "updated_at": now, | ||
| }, | ||
| }, | ||
| ) |
There was a problem hiding this comment.
🟡 Team usage reports now show a fake API key entry for reserved-capacity rows
Reserved-capacity rows are written into the same team daily-spend table under a made-up key name (api_key=PTU_SENTINEL_API_KEY at litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py:136-156) that existing usage reports do not filter out, so teams see a phantom key with zero usage in their activity breakdowns.
Impact: Team daily activity responses and the usage UI display a bogus key entry and extra records for every reserved-capacity model.
Read path has no sentinel filter yet
/team/daily/activity queries litellm_dailyteamspend via get_daily_activity with no api_key exclusion (litellm/proxy/management_endpoints/team_endpoints.py:5235-5248), and litellm/proxy/management_endpoints/common_daily_activity.py:339-347 buckets every record by record.api_key, looking up alias metadata that will be missing for __ptu_flat_cost__. The sentinel rows also carry custom_llm_provider=""/endpoint="", adding empty-provider and empty-endpoint buckets and consuming pagination slots. The PR defers the read path to a follow-up, so on this commit alone the write pollutes existing responses.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
|
||
| await cls._initialize_spend_tracking_background_jobs(scheduler=scheduler) | ||
|
|
||
| ### PTU DAILY ROLLUP ### |
This comment was marked as low quality.
This comment was marked as low quality.
Sorry, something went wrong.
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2678380. Configure here.
| """ | ||
| model_info = getattr(row, "model_info", None) | ||
| if not isinstance(model_info, dict): | ||
| return None |
There was a problem hiding this comment.
Rollup skips string model_info
High Severity
_parse_ptu_model only accepts model_info as a dict and returns None otherwise. Elsewhere the codebase documents that LiteLLM_ProxyModelTable.model_info can arrive as a JSON string depending on the query path, and model_info_as_mapping / ModelRepository already normalize that. Raw find_many() rows can therefore make every PTU deployment look invalid, so the rollup silently writes no flat-cost rows.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 2678380. Configure here.
| ) | ||
| verbose_proxy_logger.info( | ||
| "PTU rollup job scheduled at 00:15 UTC daily (only models with PTU config accrue flat cost)" | ||
| ) |
There was a problem hiding this comment.
Cron not pinned to UTC
Medium Severity
The PTU rollup cron is logged as running at 00:15 UTC, but add_job does not set timezone. The shared AsyncIOScheduler is created with timezone=None, which uses the host local zone. On non-UTC hosts the job fires at local midnight instead of UTC, so rollups run at the wrong wall-clock time relative to the UTC day the cost math uses.
Reviewed by Cursor Bugbot for commit 2678380. Configure here.
2678380 to
0643385
Compare
|
@greptileai fixed: the rollup now parses JSON-string model_info, and the daily cron is pinned to UTC to match the cost day. Re-review please |
0643385 to
42f848e
Compare
1d40110 to
4814983
Compare
|
@greptileai the rollup now clears each day's PTU sentinel rows before rewriting, so a removed or expired config leaves no stale charge. Re-review please |
4814983 to
6725bf4
Compare
|
@greptileai the rollup now writes current charges before pruning stale rows (no delete-before-write underbilling window) and sums same-name deployments per team into one row. Re-review please |
6725bf4 to
e714ccd
Compare
|
@greptileai a failed sentinel upsert now retries with backoff before the batch moves on, and the run reports rows_failed so a lost day is visible. Re-review please |
|
@greptileai review latest head and the comment i gave you |
c81b5a4 to
7d13d50
Compare
7d13d50 to
460a1de
Compare
|
@greptileai review |
460a1de to
da48a56
Compare
| async def _load_ptu_models(prisma_client: "PrismaClient") -> tuple[PTUModel, ...]: | ||
| """Every model deployment currently carrying valid manual PTU config.""" | ||
| rows: Final = await prisma_client.db.litellm_proxymodeltable.find_many() | ||
| return tuple(parsed for parsed in (_parse_ptu_model(row) for row in rows) if parsed is not None) |
There was a problem hiding this comment.
🟡 Deleting a provisioned-throughput model loses that day's charge for good
Charges are computed only from the deployments that still exist when the nightly job runs (_load_ptu_models at litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py:318-321), so a model removed before the next run is never billed for the hours it was active that day and nothing later recovers it.
Impact: A team can use provisioned capacity all day and be charged nothing for it simply because the model was deleted before midnight.
Why no later pass recovers the day
The scheduled job at 00:15 UTC prices only the previous UTC day, and both the day's rollup and the catch-up pass derive charges from the live LiteLLM_ProxyModelTable rows (litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py:350-351 and litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py:456-468). Once the deployment row (or just its ptu_count / cost_per_ptu_per_hour / ptu_effective_from fields) is gone, _parse_ptu_model returns None for it forever, so the last unpriced day (and any earlier days the job never got to, e.g. after a multi-day outage) produce no charge and no alert, since rows_failed stays 0 for a charge that was never attempted. The PR description claims deletion "stops it accruing new charges and leaves the closed days standing", which holds for already-written rows but silently drops the in-progress and any still-unpriced days.
Prompt for agents
The rollup and its catch-up pass both read PTU config from the live LiteLLM_ProxyModelTable, so any day not yet priced when a deployment is deleted (or has its PTU fields cleared) is lost permanently, including the day the deletion happened. Consider pricing the elapsed portion of the current day at deletion time in the model-delete/update path, or persisting a small immutable record of PTU config windows that the rollup can read even after the deployment row is gone. At minimum, document/alert on the gap so operators know deleting a PTU deployment forfeits the unbilled hours.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
@greptileai review |
da48a56 to
839364f
Compare
| if isinstance(raw, str): | ||
| try: | ||
| return json.loads(raw) | ||
| except (TypeError, ValueError): | ||
| return None |
There was a problem hiding this comment.
🟡 A single malformed model record can abort the whole nightly PTU cost run
A stored model configuration that decodes to something other than a key/value object (json.loads at litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py:113) is returned as-is instead of being rejected, so reading it later crashes and the entire nightly charge run stops before pricing anyone.
Impact: One bad model entry can silently prevent every team's provisioned-throughput charge from being recorded for that day.
Mechanism: the JSON branch of _decode_model_info does not verify the decoded value is a dict
_decode_model_info (litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py:109-118) returns json.loads(raw) directly when raw is a string. Valid JSON that is not an object (e.g. "123", "[]", "true") decodes to an int/list/bool, which is then returned as dict[str, object]. _parse_ptu_model immediately calls model_info.get("ptu_count") (:131) and raises AttributeError. _load_ptu_models (:318-321) has no per-row guard, and neither run_ptu_flat_cost_rollup nor run_ptu_flat_cost_backfill catches it, so the exception propagates all the way out of the scheduled job (and, on the locked path, only the finally release runs). The dict branch at :116 shows the intent that only mappings are accepted; the string branch skips that check. Note the test test_a_bad_row_does_not_abort_pricing_for_other_teams asserts exactly the property this breaks, but only for a dict-shaped bad row.
| if isinstance(raw, str): | |
| try: | |
| return json.loads(raw) | |
| except (TypeError, ValueError): | |
| return None | |
| if isinstance(raw, str): | |
| try: | |
| decoded: Final = json.loads(raw) | |
| except (TypeError, ValueError): | |
| return None | |
| return decoded if isinstance(decoded, dict) else None |
Was this helpful? React with 👍 or 👎 to provide feedback.
Add the daily rollup that reads PTU config off model deployments and writes flat cost to LiteLLM_DailyTeamSpend. For each UTC day a deployment carrying ptu_count and cost_per_ptu_per_hour accrues ptu_count * cost_per_ptu_per_hour * active_hours, where active_hours is the overlap between the day and the optional [ptu_effective_from, ptu_effective_to) window clamped to 24; a window opening at 23:00 charges one hour that day. Rows use a sentinel api_key so they stay distinguishable from per-request rows and share the existing unique constraint, and the write is idempotent so re-runs never double count. The cron is registered at proxy startup and runs at 00:15 UTC. Pricing one day per fire leaves two ways for a day to end up unpriced and stay that way: a window backdated at configuration time, which no fire ever revisits, and a fire that is missed or lands late, which the next one does not replay because the billed day comes from the wall clock rather than the scheduled time. Both are silent, since the failure alert only fires for a charge that was attempted. Each scheduled run therefore follows the day's reconcile with a catch-up pass that prices the (team, model, date) charges inside every declared window that carry no row yet, bounded at the earliest ptu_effective_from and floored at PTU_ROLLUP_MAX_BACKFILL_DAYS. It writes only what is missing: a day already priced keeps the amount it was billed whatever the config says now, and it runs no prune, so deciding a row is stale stays the single-day path's job. Zero-cost days write nothing, which leaves an out-of-window day reconsidered each run rather than recorded as done. A catch-up pass that fails cannot take the day's own result with it, and an explicit target_date still means reconcile exactly that day. The sentinel row keys on the deployment id, with the operator-facing name alongside it in model_group, which sits outside the table's unique key. The name is what a usage view displays, but a deployment can be renamed, and two runs holding config views from either side of a rename then wrote the same day under two different keys, so nothing collided and both charges survived. A multi-pod rig reproduced that as a permanent double charge that no later run repaired. Keyed on the id both writes land on one key and the upsert collapses them; when the rate changed too, last writer wins on the amount rather than adding a row. Deployments sharing a public name inside a team therefore no longer need collapsing into a single charge: each keys its own row, and the read path merges them back under the shared name. The read path that surfaces the amount lands in a follow-up PR. The prune is the one destructive step, so it only runs when the pod took the cross-pod lock. The upserts stay unguarded, since they are idempotent and no lock problem may cost a day, but the delete compares a cutoff and an updated_at stamped on different hosts, and a live rig showed a pod whose clock ran ten minutes ahead sweeping the charge a concurrent pod had just written, leaving the day at zero. Its cutoff also allows PTU_PRUNE_SKEW_GRACE_SECONDS of slack, which separates the two populations without requiring clocks to agree: a stale row is hours old and a concurrently written one is seconds old. The catch-up deletes nothing. Removing a deployment or narrowing its window stops it accruing new charges and leaves the days it was already billed for standing, since those days were incurred and a usage view has to keep reporting them. A deployment carrying no ptu_effective_from is skipped rather than treated as open ended. The endpoints require a start, and substituting the cap floor for a missing one meant a windowless deployment accrued the whole ninety day window on its first run, billing days it did not exist while the result still reported a single row written.
839364f to
803a577
Compare
…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 …


TLDR
Problem this solves:
How it solves it:
Relevant issues
Linear ticket
Resolves LIT-4077
Pre-Submission checklist
Screenshots / Proof of Fix
Captured live against a proxy on this branch (commit acf106b), real DB, no mocks. A model with ptu_count 5, cost_per_ptu_per_hour 2.0 and ptu_effective_from 2026-07-31T23:00:00Z is rolled up for 2026-07-31; the window opens at 23:00 so the day has one active hour and the flat cost is 5 * 2.0 * 1 = 10.00
A separate always-on model in the same run wrote 480 (10 PTU * 2.0 * 24h), confirming the full-day path
The rollup's output, surfaced
One deployment at 10 PTU x $2.00/hr with no effective window accrues a full day each run. Rolling up four consecutive UTC days and viewing the team gives 4 x $480 = $1,920, one violet bar per day:
Multi-pod
Three separate proxy processes against one Postgres and one Redis, each constructing its own PodLockManager and so its own pod_id, all released at the same wall-clock instant the way a cron fires everywhere at once. Ten PTU deployments, four sentinel rows
With Redis healthy the lock elects a single writer and the other two return without touching the database:
With Redis stopped every pod proceeds unguarded rather than skipping the day, and the reconcile is still exactly once:
Three concurrent full reconciles, twelve upserts, and the day is byte-identical afterwards. That is the property the design rests on: the lock avoids duplicate work, while correctness comes from the idempotent upsert on the sentinel key and a prune that deletes only rows this run did not refresh, so losing the lock costs effort rather than money
Renaming a deployment's public name at the barrier instant, with all three pods running unguarded, was attempted six times and never produced a duplicate charge; one row per model on every trial. The interleaving that would produce one is narrow, the config read measured at about 5 ms, so this bounds the failure rather than proving it impossible
This is the second of a stack, on top of the model-config PR. The read path that surfaces this on /team/daily/activity and the UI land in follow-up PRs
Catch-up proof
Live against a proxy on this branch (commit 67c8d40), real Postgres, no mocks.
Rebased onto litellm_internal_staging at f6587fa on 2026-08-06, which had moved 441 commits past the original branch point. The whole matrix below was re-run against the rebased head on a live proxy and a real Postgres, so nothing here is carried over from the pre-rebase run
An operator configures PTU on a deployment whose window opened 30 days ago, which the form accepts and the backend validates and stores
One scheduled run, called exactly as the 00:15 UTC cron calls it, with no target_date
30 days at 10 PTU and 2.00 an hour is 480 a day, so the declared window is billed in full. A second run finds nothing to do and leaves the figure at 14400.0
Deleting one day's row, which is what a pod restart across 00:15 leaves behind, drops the total to 13920.0; the next run restores it to 14400.0 without touching any other day
The property that matters most for a billing job is that catching up never restates a closed day. Doubling the rate to 4.00 and running again
The 480 delta is the one day the daily reconcile owns. Every earlier day keeps the amount it was billed
Rename safety
A review pass found that matching gaps on the model name alone double charges a renamed deployment: the historical rows stay filed under the old name, since the catch-up never prunes, and every one of those days then reads as an unpriced gap under the new name. The gap check now also matches on ptu_source_model_id, which survives a rename
Proven live on the same database, ten days of a backdated window at 10 PTU and 2.00 an hour. The deployment is renamed through the dashboard's own PATCH shape, which rewrites team_public_model_name, and the rollup is then run again
Only the day the daily reconcile owns moves to the new name, because that path does prune. Every earlier day keeps the single charge it was billed
Concurrency across a rename
A four-pod rig found that two runs holding config views from either side of a rename wrote
the same day under two different composite keys, so nothing collided and both charges
survived. It never healed: no later run repaired it and none alerted. The sentinel row now
keys on the deployment id with the display name in model_group, outside the key
Same rig, same forced interleave, before and after
Four pods firing the cron at once still elect exactly one runner, a second run writes
nothing, and deleting a mid-window day is repaired by the next catch-up back to 2400.00
The prune and clock skew
A third pass found that the prune compares a cutoff and an updated_at stamped on different
hosts. On the rig, a pod whose clock ran ten minutes ahead swept the charge a concurrent
pod had just written, and because both pods key the same row now, the day was left at zero
The prune is the only destructive step, so it now runs only when the pod took the
cross-pod lock. The upserts stay unguarded, since they are idempotent and no lock problem
may cost a day. The cutoff also allows PTU_PRUNE_SKEW_GRACE_SECONDS of slack, which
separates the two populations without requiring clocks to agree: a stale row is hours old
and a concurrently written one is seconds old
The catch-up deletes nothing
An earlier revision had the catch-up delete sentinel rows for deployments that no longer
carry PTU config, reasoning that the single-day prune sweeps a date only once and a charge
it missed would be billed forever. That went too far: it also erased days a deployment was
legitimately billed for, so deleting a deployment wiped its history out of every usage view
that reports it. Removing a deployment or narrowing its window now stops it accruing new
charges and leaves the closed days standing, since those days were incurred
Covered by unit tests that assert the catch-up issues no delete at all, for a removed
deployment, for a narrowed window, and after the last PTU deployment is gone. The live
multi-pod matrix in this description was captured before this change and has not been
re-run against it
Live run 2026-08-08, commit c81b5a4
Fresh Postgres, real Gemini traffic, no mocks. One deployment at 15 PTU and $2.00/hr from 2026-08-01, a second at 10 PTU windowed 2026-08-06 23:00 to 2026-08-08 00:00
Pricing and identity
Rename, which used to file a second charge
Hourly proration across a window boundary
The catch-up, after deleting the windowed deployment
Deleting a deployment stops it accruing and leaves its closed days standing. The earlier revision of this PR deleted those rows
Known limitations
The effective window's start is required, not optional.
ptu_effective_frommust be set wheneverptu_countandcost_per_ptu_per_hourare, because flat cost accrues from that instant and an inferred start would bill days the deployment did not exist. Onlyptu_effective_tois optional, and leaving it blank means open-ended. Earlier revisions of this description called the whole window optional, which was wrongSingle-writer election needs Redis.
run_scheduled_ptu_rolluptakes the cross-pod lock throughPodLockManager, and when no Redis cache is configured it skips election and runs on every pod. That path deliberately passesmay_prune=False, so the destructive step never runs unelected and the remaining work is idempotent upserts against the sentinel key: the cost is duplicated effort, not a wrong number. A fleet that wants one writer per day needsgeneral_settings.use_redis_transaction_bufferand a reachable RedisThe catch-up reaches back at most
PTU_ROLLUP_MAX_BACKFILL_DAYS, currently 90. A window whose start is older than that never has its earliest days priced by any run, and nothing reports the omission. The cap exists so an open-ended window cannot scan the whole table; a deployment needing a longer history has to be priced by an explicit single-day run per dateType
New Feature
Changes
run_ptu_flat_cost_rollup enumerates model deployments, parses each model_info into a validated PTUModel (positive count, non-negative rate, team_id present, effective dates coerced to UTC), computes the day's active hours as the clamped overlap of the day with the effective window, and upserts one sentinel-api_key row per model into LiteLLM_DailyTeamSpend keyed by (team_id, model, date). The charge is keyed on the operator-facing name: creating a team-scoped deployment rewrites model_name to a synthetic routing key and keeps the chosen name in model_info.team_public_model_name, and PTU is only accepted alongside a team_id, so reading model_name directly would file every charge under a UUID no usage view can resolve. Deployments that share a public name inside a team are summed into one sentinel row, the current charges are written before the day's unrefreshed rows are pruned so a removed or expired config never leaves a positive charge behind, and a failing write is retried with backoff before the batch moves on; the run reports rows_failed so a day that still needs a manual rerun is visible. The composite-key upsert makes re-runs idempotent
The cron is registered unconditionally at startup and runs daily at 00:15 UTC; a model without PTU config simply produces no row. Because every proxy process schedules it, the run takes the shared PodLockManager lock first, so one pod reconciles the day and a loser cannot prune rows the winner just wrote; a deployment with no Redis-backed lock manager runs unguarded, as
SpendLogCleanupdoes.acquire_lockreports contention and a Redis outage identically, so a failed acquire is followed by a read of the lock key: if nobody actually holds it the run proceeds unguarded rather than skipping, since an unreachable Redis would otherwise drop the whole day's charges on every pod at once. The lock exists to avoid duplicate work, so no lock problem is allowed to cost a dayThe registration itself is deliberately not wrapped in a try/except, unlike the spend-log-cleanup and batch-cost registrations that follow it. Those guard values that can legitimately be bad at runtime, such as an operator-supplied retention string; the only way this one raises is a code defect, since the module imports nothing beyond the standard library and two litellm modules already loaded by that point, with PrismaClient and PodLockManager behind TYPE_CHECKING and no module-level executable code. Swallowing that would leave the rollup silently unregistered and the flat cost quietly missing until someone reads an invoice, which is the more expensive failure for a billing job than refusing to start
The prune deletes by
updated_at < run_startedrather than by "the keys I just computed". Whether a sentinel row is garbage is then a property of the row, not of one run's in-memory config snapshot, which is the same shapeSpendLogCleanupdeletes retired spend logs by (WHERE startTime < cutoff). It matters under concurrency: with a key-list predicate, two pods that read the config either side of a model rename each compute a correct-for-them list, and whichever deletes last removes the charge the other just wrote. With the timestamp predicate a row written after a run began is out of reach of that run's delete, so the worst case is a renamed-away charge lingering until the next run sweeps it. The lease can therefore lapse mid-run without risking the data; it costs duplicate work, not correctnessThe prune is still skipped when any charge failed to write, since a row whose replacement never landed would otherwise look unrefreshed and be deleted
Charges that exhaust their retries raise a
failed_tracking_spendalert naming the date and the count, so the gap surfaces through whatever alerting the deployment already has rather than only in proxy logsPricing one day per fire leaves two ways for a day to end up unpriced and stay that way. A window backdated at configuration time is a validated, supported input, and no fire ever revisits the days it already covers, so declaring a window that opened a month ago silently bills one day of it. A fire that is missed or lands late is worse in kind: the next one does not replay it, because the billed day comes from the wall clock rather than the scheduled time, which is also why no scheduler tuning recovers it. Neither raises anything, since the failure alert only fires for a charge that was attempted, and a day never attempted leaves rows_failed at zero
So each scheduled run follows the day's reconcile with a catch-up pass. It reads the sentinel rows already present across the scanned range, computes the charges every declared window implies over that range, and writes only the (team_id, model name, date) charges with no row, bounded at the earliest ptu_effective_from and floored at PTU_ROLLUP_MAX_BACKFILL_DAYS so an open-ended window does not scan the whole table. Three properties keep it from being worse than the gap it closes. It never rewrites an existing row, so a day already priced keeps the amount it was billed however the config has changed since; re-pricing it would silently restate a bill that was correct for that day, which is the failure this design is most careful to avoid. It runs no prune, so deciding a sentinel row is stale stays the single-day path's job and a deployment removed after a day was billed does not lose that day's charge. And zero-cost days write nothing, which keeps absence of a row from meaning "priced at zero" and leaves an out-of-window day reconsidered on each run rather than recorded as done
The pass runs inside the same pod lock as the day's reconcile, and only on the scheduled shape: passing an explicit target_date still means reconcile exactly that day. Its failure is contained, since the day's own rollup has already committed and its result must reach the caller whatever the catch-up does. A charge the catch-up cannot write raises its own alert naming the range
Behavior changes
A deployment whose model_info carries an unparseable or inverted PTU effective window now accrues no flat cost until the config is fixed, where an earlier revision of this PR read the bad bound as "no bound" and billed the full day
A scheduled run now writes rows for elapsed in-window days beyond yesterday, where an earlier revision of this PR wrote yesterday only. It adds rows and never changes or deletes one, so a team whose days are all priced sees no difference, and a proxy with no PTU config anywhere does no extra database work beyond one model-table read the rollup already performed. Nothing else in the stack changes for a well-formed config
Final Attestation
Note
Medium Risk
Writes directly to daily team spend aggregates on a schedule; incorrect proration or upsert keys could misstate billing, though upserts are idempotent and failures are isolated per model.
Overview
Adds a daily PTU flat-cost rollup so provisioned-throughput settings on model deployments turn into team spend rows, prorated by how many UTC hours the PTU window overlaps each day.
A new
run_ptu_flat_cost_rollupjob scansLiteLLM_ProxyModelTable, validatesmodel_info(ptu_count,cost_per_ptu_per_hour,team_id, optional effective window), computesptu_count × rate × active_hours, and idempotently upserts intoLiteLLM_DailyTeamSpendusing the sentinel api key__ptu_flat_cost__plusptu_flat_cost/ptu_source_model_id. Per-model upsert failures are logged and skipped so one bad deployment does not stop the batch.Proxy startup registers an APScheduler cron at 00:15 UTC (defaults to rolling up yesterday). Unit tests cover hour-window edge cases, parsing, rollup behavior, and that the job is registered at startup.
Reviewed by Cursor Bugbot for commit 2678380. Bugbot is set up for automated code reviews on this repo. Configure here.