feat(ui): lead the benchmarks tab with savings and the sessions it was earned over - #35741
Conversation
The benchmarks dashboard answered every question by scanning LiteLLM_SpendLogs at read time: four aggregate queries per auto-router, two of them window functions over the response JSONB, re-deriving on every page load which model the previous turn used, how long a tier had been idle, and how big the prefix was last time. Those are sequential facts and the request that produces them already knows all of them, so they are now computed once, when the turn happens. A new LiteLLM_AutoRouterSession row per (session, auto-router) carries both the counters and the state that classifies the next turn. fold_turn is pure, so every rate and dollar formula is unit-testable without a database, and the in-memory queue plus background flusher follow AdaptiveRouterUpdateQueue: atomic increment upserts, so two pods writing one session compose instead of overwriting. A pod that has never seen a session loads its row once and classifies from memory after, which is what keeps a session correct across a restart or a pod move. The counters are declared once, on TurnDelta. COUNTER_FIELDS derives from that declaration and the merge, the flush payload and the read query all build off it, so a metric added there reaches the database and the dashboard without a second edit. A test asserts the read query aggregates every declared counter; it caught two that were being written on every request and read by nothing. The read path is a single aggregate over pre-folded rows covering every auto-router at once, and touches no per-request table at all. Rollup rows expire on the existing spend-log retention cutoff, keyed on last activity so a live conversation is not pruned out from under itself. Two behaviour fixes came with the move. The turn buckets are now exhaustive: a session's opening turn used to land in the headline turn count and in none of the three buckets, so the bucket totals silently disagreed with the headline. And a turn with no ephemeral cache-creation evidence now reads as the five minute tier rather than the one hour tier, which had been the default purely because zero is not less than zero. Savings come from compute_savings_spend, the same primitive the usage tab uses, so the two surfaces cannot report different numbers for the same traffic. The baseline recorded on each row is the one that priced its turns, so the tab names what the numbers were computed against rather than whatever the config says by the time someone opens it.
…s earned over Renders the per-session rollup as a tab on the cost-optimization page. Savings is the single hero figure on the view, paired in one card with the session count and turn count that number was earned over, because a dollar figure with no denominator beside it is not something an operator can size. That pairing makes two derived numbers worth showing, both computed from the response the tab already fetches: savings per session, and how many auto-routers are in scope. Neither needs a backend change, and both move with the router selector. Beneath it the three session-shape metrics, then the prompt-caching section: hit rate over a stacked share-of-turns bar with a three-bucket breakdown, and a warming estimate showing rescued writes and replay cost separately against the break-even for the TTL in use. Every rate is recomputed from summed counts whenever more than one router is in view. Averaging the routers' own rates would weight a router with three turns the same as one with three thousand, which lands the blended figure nowhere near either. Savings reads "Not measured" rather than $0.00 when no baseline is configured, since autorouter_savings_baseline_model is what the driver needs and a confident zero against real spend is worse than saying nothing. Built on the base shadcn primitives (Card, CardHeader, CardAction, Separator, Table) so padding, header layout and table density come from the design system rather than being restated per block. The tab test renders a real component tree and stubs only the network, so it is named for the integration tier; the fold it exercises is unit-tested separately against its own module.
Greptile SummaryThe PR adds an Auto-Router Benchmarks tab that folds per-router session and cache rollups into savings, session-shape, cache-hit, and warming views.
Confidence Score: 4/5The mixed-baseline and mixed-TTL aggregation defects should be fixed before merging because they present incorrect benchmark economics to operators The aggregate fold can label partially unmeasured spend as measured and can synthesize a 9% one-hour warming threshold that is inconsistent with the backend’s TTL-specific economics Files Needing Attention: ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/autoRouterBenchmarks.ts and AutoRouterBenchmarksTab.tsx
|
| Filename | Overview |
|---|---|
| ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/autoRouterBenchmarks.ts | Adds the aggregate view fold, but mixed baseline availability and mixed TTL regimes produce misleading combined values |
| ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.tsx | Adds the complete benchmarks UI; its warming card exposes the invalid aggregate TTL and threshold pair |
| ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useAutoRouterBenchmarks.ts | Adds a stable 30-day TanStack Query hook with no accepted blocking defect |
| ui/litellm-dashboard/src/components/networking.tsx | Adds the benchmark endpoint wrapper through the existing authenticated API client |
| ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/autoRouterBenchmarks.test.ts | Covers weighted aggregation and homogeneous baseline states but omits the two mixed-state cases implicated by the findings |
| ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.integration.test.tsx | Provides substantial rendering coverage but does not exercise mixed baseline availability or mixed TTL warming output |
| ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx | Registers the new benchmarks tab in the cost-optimization view |
Reviews (1): Last reviewed commit: "feat(ui): lead the benchmarks tab with s..." | Re-trigger Greptile
| const baselines = new Set(groups.map((g) => g.baseline_model).filter((m): m is string => Boolean(m))); | ||
| if (baselines.size === 0) return null; | ||
| if (baselines.size === 1) return [...baselines][0]; | ||
| return "each router's own baseline"; | ||
| }; |
There was a problem hiding this comment.
Mixed baselines appear fully measured
When the all-routers selection contains both measured and unmeasured routers, combineBaselineLabel discards the null baselines while toView still includes those routers' spend. This names the remaining baseline as though it covered the entire selection and dilutes the displayed savings percentage with spend that has no counterfactual.
Knowledge Base Used: Admin dashboard (ui/litellm-dashboard)
| return_hit_rate_pct: rate(returnHits, returnTurns), | ||
| stale_miss_share_pct: rate(staleMisses, returnTurns - returnHits), | ||
| warming_savable_miss_pct: rate(savableMisses, turns - hits), | ||
| warming_break_even_pct: Math.max(...caches.map((c) => c.warming_break_even_pct)), |
There was a problem hiding this comment.
Mixed TTLs create invalid threshold
When five-minute and one-hour cache regimes are combined, the fold independently selects the maximum TTL and maximum break-even percentage, producing the nonexistent pair 1h and 9%. The warming card then displays that pair and compares the aggregate savable-miss rate against it, giving operators an incorrect break-even assessment.
Knowledge Base Used: Admin dashboard (ui/litellm-dashboard)
4022c43 to
71f2e85
Compare
TLDR
Problem this solves:
$0.00for any router that declares no counterfactual baseline, which reads as "the router saved nothing" rather than "nothing was measured"How it solves it:
Relevant issues
litellm_internal_staging; replaces feat(ui): rebuild the auto-router benchmarks tab around savings and cache behaviour #35488, which could not be retargeted because GitHub blocks base changes on a stacked PRlitellm.autorouter_savings_baseline_modelis configured, which is what feat(ui): show auto-router savings on the cost-optimization dashboard #35522 made the driver depend on; a confident$0.00against real spend would be worse than saying nothingLinear ticket
Resolves LIT-4712
Pre-Submission checklist
Screenshots / Proof of Fix
Rendered against a live proxy holding 30 days of real auto-routed traffic (99 sessions, 3,145 turns across three routers). Savings reads "Not measured" because all three are complexity routers, which declare no baseline; that is the honest state rather than a zero.
The three buckets sum to the headline turn count: 2,560 + 146 + 439 = 3,145.
To reproduce:
python litellm/proxy/proxy_cli.py --config <a config with at least one auto-router> --port 4715curl -X POST "http://localhost:4715/auto_router/benchmarks/backfill?start_date=<30d ago>&end_date=<today>" -H "Authorization: Bearer $LITELLM_MASTER_KEY"to populate the rollup from existing trafficcd ui/litellm-dashboard && NEXT_PUBLIC_BASE_URL=http://localhost:4715 npm run devhttp://localhost:3000/cost-optimization/and click the Auto-Router Benchmarks tabType
🆕 New Feature
Changes
autoRouterBenchmarks.tsholds the API types andtoView, the pure fold that combines however many routers are in view into the one shape the tab renders. Every rate is recomputed there from summed counts rather than averaged across routers, because averaging weights a router with three turns the same as one with three thousand; the tests pin that a 1000-turn router at 90% and a 10-turn router at 0% read as 89.1%, not 45%.The baseline label is deliberately three-valued. One shared baseline names it, several name none of them ("each router's own baseline"), and no baseline at all returns null, which is what drives the "Not measured" tile.
Marks follow the house chart specs: a 2px surface gap separates the stacked segments rather than a stroke, the fills are three steps of one neutral ramp, and every bucket carries its own swatch and label in the table below so identity never rests on colour alone. Values and labels wear text tokens, never the mark colour. The bucket table doubles as the table view for the bar.
useAutoRouterBenchmarkscomputes its window once per mount so the query key stays stable and the request is not refired on every parent render.QA runbook
__all__$0.00+N%badge in the destructive toneFinal Attestation