Skip to content

chore(release): backport #33899, #33978, #34582, #34675 to rc/1.94.0 and bump litellm-proxy-extras to 0.4.79.post2 - #34855

Merged
yuneng-berri merged 6 commits into
rc/1.94.0from
litellm_backport_rc194_toolspend_0727
Jul 28, 2026
Merged

chore(release): backport #33899, #33978, #34582, #34675 to rc/1.94.0 and bump litellm-proxy-extras to 0.4.79.post2#34855
yuneng-berri merged 6 commits into
rc/1.94.0from
litellm_backport_rc194_toolspend_0727

Conversation

@yuneng-berri

@yuneng-berri yuneng-berri commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Relevant issues

Backports the Cost Optimization tool-spend surface onto rc/1.94.0 as a chain, so the line gets the dashboard together with the rollup that keeps it off LiteLLM_SpendLogs

The line was cut on 2026-07-18, before any of this existed on litellm_internal_staging. #33899 added the Cost Optimization tabs on 07-20, #33978 added the spend-by-tool views and GET /v1/tool/spend on 07-23, #34582 bounded that endpoint's reads on 07-24, and #34675 replaced them with a daily rollup on 07-27. #34675 alone does not apply here; the endpoint it rewrites does not exist on this line, so the four land together or not at all

The ordering matters for a reviewer: #33978 is what introduces the unbounded query_raw ... JOIN "LiteLLM_SpendLogs" in get_tool_spend, which is the read this line must never ship at customer scale. #34582 bounds it and #34675 removes it, so the endpoint ends up reading LiteLLM_DailyToolSpend with no SpendLogs access at all. Taking the first two without the last two would put a full-table scan on the dashboard's load path

No root version bump. This line pins pyproject at 1.94.0 across rc.N and ships as v1.94.0-rc.N; the tip is untagged and v1.94.0-rc.3 sits behind it, so an rc.4 would tag the tip. Only litellm-proxy-extras moves, 0.4.79.post1 -> 0.4.79.post2, because the two new migrations live in that package

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

What is included

Cherry-picks in staging merge order, each carrying its -x footer:

Then two generated commits:

  • chore: update Next.js build artifacts, the line's own Node v20 rebuild of litellm/proxy/_experimental/out/, without which the proxy would keep serving the pre-backport bundle and the dashboard would never appear
  • chore: bump litellm-proxy-extras to 0.4.79.post2, covering 20260724000000_add_spend_log_tool_index_start_time_idx and 20260725000000_add_daily_tool_spend

Picks 1 through 3 applied verbatim (patch-id identical to their staging sources). Pick 4 is adapted; see below

Adaptation notes (#34675 only)

Six conflicts, all in UI files and all caused by staging drift the line predates, none semantic. The Python side applied without a single conflict

  • UsageTab.tsx, UsageTab.test.tsx, CostOptimizationView.activity.test.tsx: the shared charts module import moved from DEFAULT_COLOR_CYCLE to CustomLegend + SEQUENTIAL_COLOR_RAMP. Took the incoming side; both symbols resolve here because fix(proxy): roll up tool spend daily instead of scanning SpendLogs #34675's own colors.ts and index.ts hunks applied cleanly, and both are genuinely used by its UsageTab hunks
  • UsageTab.test.tsx also dropped the shows no cap note ... test, which pinned fix(proxy): cap /v1/tool/spend window at 30 days and bound every SpendLogs read #34582's clamp that fix(proxy): roll up tool spend daily instead of scanning SpendLogs #34675 deletes
  • UsageTab.test.tsx one further divergence from the staging source: staging's renderWith takes an options object ({ toolSpend }) after a later PR changed its signature; on this line it still takes a positional toolSpend, so the call was adapted. Without this the test renders no chart and times out
  • bar_chart.tsx: took fix(proxy): roll up tool spend daily instead of scanning SpendLogs #34675's colorByDatum fills line only. The incoming side also carried an unrelated empty-state block from a later staging PR, which no picked test exercises, so it was left out
  • ToolDetail.tsx: took only the Recent logs -> Recent invocations rename. The incoming side also carried the shadcn token migration (bg-card, lucide History) that this line predates, so the line's existing styling is kept
  • eslint-suppressions.json: net change against the line is zero. The incoming block listed suppressions this line does not need, and eslint --prune-suppressions removed them again

Known noise on this line

Judged as deltas against the line tip, captured before any pick:

  • Targeted suite: 369 passed / 0 failed at the tip, 419 passed / 0 failed on this branch. The picks add 50 tests and break none
  • Full mirrored suite at the tip: 16 failed, 3 errors, 30113 passed. Pre-existing
  • npx tsc --noEmit: 46 files report errors at the tip, all of them test files. None is a file this backport touches
  • npx eslint .: 0 errors, 2955 warnings, all pre-existing

Screenshots / Proof of Fix

All captured at 7fec728c7f, against a live proxy running from this branch's own worktree venv (so litellm_proxy_extras resolves to the branch, not a shared checkout) on an isolated Postgres started with log_statement=all, hitting the real Anthropic API

Both migrations apply on a database whose latest was 20260718000000_add_savings_spend

Found 140 migrations at .../litellm-proxy-extras/litellm_proxy_extras
Applying migration `20260724000000_add_spend_log_tool_index_start_time_idx`
Applying migration `20260725000000_add_daily_tool_spend`
All migrations have been successfully applied.
$ psql -c '\d "LiteLLM_DailyToolSpend"'
 date | tool_name | spend | total_tokens | request_count | created_at | updated_at
Indexes:
    "LiteLLM_DailyToolSpend_pkey" PRIMARY KEY, btree (date, tool_name)

$ psql -c "select indexname from pg_indexes where tablename='LiteLLM_SpendLogToolIndex';"
 LiteLLM_SpendLogToolIndex_pkey
 LiteLLM_SpendLogToolIndex_tool_name_start_time_idx
 LiteLLM_SpendLogToolIndex_start_time_idx     <- new

Invoked-not-declared attribution, with prompt storage OFF. One request declaring five tools where the model calls one. STORE_PROMPTS_IN_SPEND_LOGS is unset, which is the point: the old payload-mining path needed it for non-MCP coverage

$ curl -s localhost:4077/v1/chat/completions -H "Authorization: Bearer $KEY" -d @req.json
response id   : chatcmpl-0b2c4ae3-f1c9-4df2-b560-41a232deceff
tools declared: 5
tools INVOKED : ['bp_get_weather']
usage tokens  : 838

$ psql -c 'select date, tool_name, spend, total_tokens, request_count from "LiteLLM_DailyToolSpend";'
    date    |   tool_name    |  spend   | total_tokens | request_count
 2026-07-28 | bp_get_weather | 0.001062 |          838 |             1
(1 row)

$ psql -c 'select tool_name, request_id from "LiteLLM_SpendLogToolIndex";'
   tool_name    |                  request_id
 bp_get_weather | chatcmpl-0b2c4ae3-f1c9-4df2-b560-41a232deceff
(1 row)

One row, not five. The declared-tools mining would have attributed the full request cost to all five

The read path never touches SpendLogs. The endpoint serves from the rollup

$ curl -s "localhost:4077/v1/tool/spend?start_date=2026-07-01&end_date=2026-07-28" -H "Authorization: Bearer $KEY"
{"by_tool":[{"tool_name":"bp_get_weather","spend":0.001062,"call_count":1,"total_tokens":838}],
 "daily":[{"date":"2026-07-28","tool_name":"bp_get_weather","spend":0.001062,"call_count":1}],
 "start_date":"2026-07-01","end_date":"2026-07-28"}

Measured from the Postgres statement log rather than asserted from the source. The slice is bounded by the log line count before and after the call, with a positive control so the absence is not vacuous

liveness: +6 log lines during the call
SpendLogs refs in slice          : 0
SpendLogToolIndex refs in slice  : 0
DailyToolSpend refs in slice     : 2      <- the group_by and the find_many
control (SpendLogs in FULL log)  : 24     <- the pattern does match when the table is touched

Tests, judged as deltas against the line tip

Targeted suite 369 passed / 0 failed at the tip and 419 passed / 0 failed here, so the picks add 50 tests and break none. Full mirrored suite shows zero new failures; eight looked new until matched-set isolation re-runs showed the same result on both trees, leaving one failure (test_s3_cache_concurrent_async_operations) that fails at the tip too. Vitest over the cost-optimization and shared-chart components is 14 files and 70 tests green. ruff format --check and ruff check clean on all 11 changed litellm/ files. tsc and eslint deltas are zero; both are red at the tip in files this PR does not touch

A deep adversarial review over eight lenses returned two findings, both reproduced and both verified byte-identical to staging rather than introduced here. They are written up in a comment on this PR

Type

🆕 New Feature
🐛 Bug Fix

Changes

The read path is the point. Before this chain rc/1.94.0 has no /v1/tool/spend at all; after it, the endpoint exists and serves from LiteLLM_DailyToolSpend, a per (date, tool_name) rollup written at request time, so a Cost Optimization load never touches LiteLLM_SpendLogs

Tool attribution counts invoked tools rather than declared ones. The index writer previously mined the request body, so a request declaring five tools while the model called one attributed its full spend to all five. Names now come from the response's tool_calls through the shared get_tool_calls_from_response normalizer, which also means non-MCP coverage no longer depends on store_prompts_in_spend_logs

Spend writers retry only ConnectError, through DB_RETRY_SAFE_ERROR_TYPES, because that is the failure that proves the statements never reached the database. Ambiguous post-send timeouts drop the batch with an error log rather than risk applying an increment twice

QA runbook

  1. python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml --detailed_debug --use_v2_migration_resolver on this branch; the startup log should show Applying migration 20260724000000_add_spend_log_tool_index_start_time_idx and Applying migration 20260725000000_add_daily_tool_spend
  2. Send a chat completion declaring several tools with a prompt that invokes exactly one, then wait for the spend-log flush
  3. SELECT * FROM "LiteLLM_DailyToolSpend" should hold one row for the invoked tool only, with spend equal to the request's cost
  4. Send an Anthropic-format /v1/messages request with tools and no store_prompts_in_spend_logs set anywhere; its invoked tool should also land in the rollup
  5. Open http://localhost:4000/ui/?page=cost-optimization and pick the Usage tab; the Spend by tool card should render both tools, and a range wider than 30 days should return data with no clamp note
  6. Click a tool on the Tools page; Recent invocations should list only requests where the model actually called it

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

devin-ai-integration Bot and others added 6 commits July 27, 2026 16:47
* feat(ui): add configuration tabs to Cost Optimization page

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

* feat(ui): reuse AutoRouter v2 and Router Settings prompt-caching panel in Cost Optimization; clarify Headroom compression

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

* feat(ui): add experimental dashboard banner with feedback discussion link to Cost Optimization

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

* feat(ui): add savings methodology note and per-key/team compression enterprise callout to Cost Optimization

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

* test(ui): assert active tab state in Cost Optimization tab-switch test

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

---------

Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
(cherry picked from commit 3456148)
feat(cost-optimization): add spend-by-tool and cache leakage views

(cherry picked from commit 43e7b96)
fix(proxy): cap /v1/tool/spend window at 30 days and bound every SpendLogs read

(cherry picked from commit b9b27c2)
fix(proxy): roll up tool spend daily instead of scanning SpendLogs

(cherry picked from commit 9bb75d6)
Ships the 20260724000000_add_spend_log_tool_index_start_time_idx and
20260725000000_add_daily_tool_spend migrations, which live in that package
@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review. (630 files found, 100 file limit)

@CLAassistant

CLAassistant commented Jul 28, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
2 out of 3 committers have signed the CLA.

✅ tin-berri
✅ yuneng-berri
❌ devin-ai-integration[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

@yuneng-berri
yuneng-berri marked this pull request as draft July 28, 2026 01:42
@yuneng-berri

yuneng-berri commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

Two verification findings, both checked against litellm_internal_staging and both carried faithfully. Decision taken: keep this branch matching staging. Recording them so the choice is explicit rather than implied.

1. #34582's window bound does not survive the stack. #34582 adds TOOL_SPEND_MAX_WINDOW_DAYS and the window_floor clamp; #34675 deletes both. Traced across the four picks: the endpoint is absent at the line tip, gains no clamp in #33978, gains the clamp in #34582, and loses it again in #34675.

Scoping this correctly, because my first pass overstated it. The endpoint reads only LiteLLM_DailyToolSpend, one row per (date, tool_name), and never touches LiteLLM_SpendLogs; that is verified live below. The default window is still 30 days (start_day = _parse_day_start(start_date) or end_day - timedelta(days=30)), the tool dimension is capped at TOOL_SPEND_TOP_TOOLS (100), and the route is admin-gated. So there is no full-table-scan exposure here, which was the concern that motivated the backport in the first place.

What genuinely remains is narrower: get_tool_spend's daily query is the only Daily*Spend reader without pagination. Its siblings in common_daily_activity.py page with skip/take=page_size and return total_pages/has_more; this one bounds tools but not days, so an explicit wide range returns days x <=100 rows in one response. That is a divergence from the house pattern rather than a scan risk, it exists identically on staging, and matching staging is the right call for a backport. Worth fixing upstream if it is worth fixing at all, so GA and this line stay converged.

#34582 still earns its place in the stack: its tool-index retention half survives, and #34675's diff is written against a tree where it landed.

2. The retry narrowing changes spend-write durability on upgrade. #34675 moves eight retry arms from DB_CONNECTION_ERROR_TYPES (ConnectError, ReadError, ReadTimeout) to DB_RETRY_SAFE_ERROR_TYPES (ConnectError only); a ninth, the idempotent update_spend_logs, is deliberately left broad. Seven of those arms already exist on this line, so this is a behavior change for anyone moving from rc.3.

A transient ReadTimeout or ReadError on a spend commit previously retried and usually succeeded. It now raises on the first attempt, and the outer handler pops every key of transactions_to_process before re-raising, so the tick is neither written nor retained. Measured against the real writers on this tree: _update_daily_spend goes from 2 attempts with the batch written to 1 attempt with 0 rows written; _commit_spend_updates_to_db goes from 9 attempts reaching all 8 tables to 1 attempt reaching none, taking that tick's key, user, team, team-member, org, tag and agent aggregates with it. ConnectError behaves identically in both arms.

This is deliberate upstream and the tradeoff is documented in _types.py: a post-send timeout is ambiguous, the statements may have landed, and a retry can stack a second increment set into the same commit, so the design prefers losing a tick to double-counting spend. Verified byte-identical to staging, definition and all eight call sites, so v1.95.0 carries it regardless. Kept as-is here so the line does not diverge.

@yuneng-berri
yuneng-berri marked this pull request as ready for review July 28, 2026 02:10
@yuneng-berri
yuneng-berri merged commit 47eae0d into rc/1.94.0 Jul 28, 2026
8 checks passed
@yuneng-berri
yuneng-berri deleted the litellm_backport_rc194_toolspend_0727 branch July 28, 2026 02:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants