Skip to content

feat(proxy): auto-router session benchmarks endpoint and UI - #35190

Open
tin-berri wants to merge 1 commit into
litellm_internal_stagingfrom
litellm_lit4712_autorouter_benchmarks
Open

feat(proxy): auto-router session benchmarks endpoint and UI#35190
tin-berri wants to merge 1 commit into
litellm_internal_stagingfrom
litellm_lit4712_autorouter_benchmarks

Conversation

@tin-berri

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Customers can't see what the auto-router is saving them
  • No turns, session length, tokens, or dollar savings per router
  • Top adoption blocker for auto-router

How it solves it:

  • New admin /auto_router/benchmarks endpoint over spend logs
  • Four session metrics plus routed-vs-baseline dollar savings
  • Surfaced as stat tiles on the Cost Optimization autorouter tab

Relevant issues

Customers evaluating the auto-router keep asking for evidence that it saves money before they roll it out. The Logs and Session views already show which tier a request landed on (#34434), but there was nothing that rolls that up into "your routed sessions cost X, the same traffic on your flagship model would have cost Y". This adds that view.

Linear ticket

Resolves LIT-4712

Pre-Submission checklist

  • 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)

Screenshots / Proof of Fix

Captured against a local proxy on port 4712 running this branch, reading a Postgres with real auto-router traffic (611 sessions of Claude Code usage across haiku, sonnet, and opus tiers). Commit 703fde9f1e.

Admin call over a 30-day window returns one entry per configured auto-router, each with the four session metrics and the savings estimate:

$ curl -s "http://localhost:4712/auto_router/benchmarks?start_date=2026-06-29&end_date=2026-07-29" \
    -H "Authorization: Bearer $LITELLM_MASTER_KEY" | python -m json.tool
{
    "start_date": "2026-06-29",
    "end_date": "2026-07-29",
    "groups": [
        {
            "model_group": "auto",
            "router_kind": "complexity",
            "baseline_model": "anthropic/claude-opus-4-8",
            "sessions": 59,
            "avg_turns_per_session": 8.17,
            "avg_session_length_seconds": 2968.44,
            "avg_tokens_per_session": 844577.54,
            "actual_spend": 34.65,
            "baseline_spend": 257.93,
            "savings": 223.28,
            "savings_pct": 86.57
        },
        ... (claude-auto: 89.0% saved, claude-router-2: 84.2% saved)
    ]
}

The window is clamped to 30 days; asking for a year still returns a 30-day window and says so:

$ curl -s ".../auto_router/benchmarks?start_date=2025-07-29&end_date=2026-07-29" -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
    | python -c "import sys,json; d=json.load(sys.stdin); print(d['start_date'],'->',d['end_date'])"
2026-06-29 -> 2026-07-29

Non-admin / unauthenticated is rejected:

$ curl -s -o /dev/null -w "%{http_code}\n" ".../auto_router/benchmarks?start_date=2026-06-29&end_date=2026-07-29"
401

UI: go to http://localhost:4000/ui/?page=cost-optimization, open the Autorouter tab. The benchmarks panel renders above the creation form, one card per auto-router, each with four stat tiles (turns per session, avg session length, tokens per session, estimated savings) and a caption naming the baseline it compared against.

Type

🆕 New Feature

Changes

The one primitive that was missing is a session-level rollup of auto-router traffic. The four metrics the customers ask for are all session-scoped, and session_id lives only on LiteLLM_SpendLogs, never on the daily rollups, because a session does not close on a day boundary and so can't be pre-aggregated the way user or team spend is. This reads spend logs directly over a window clamped to 30 days, the same discipline the tool-spend endpoint uses (#34582). A durable per-session rollup table is the natural follow-up if the 30-day cap ever becomes a product limit.

GET /auto_router/benchmarks enumerates every configured auto-router by its public model_group, then runs one pair of aggregate queries per group: sessions grouped by session_id, and spend grouped by resolved model. Filtering spend logs by the auto-router alias is what keeps the turn count honest: the LLM classifier's own judge calls land in the same session but carry the judge model's group, not the alias, so grouping by the alias yields one row per routed turn with no classifier noise.

The savings figure compares the routed model mix against sending every request to a single baseline model. The baseline defaults to the priciest model the router actually routed to in the window, which is uniform across all four router types and is always a model the router really used. A per-router benchmark_baseline_model param pins it to a fixed flagship instead. The estimate uses list prices on the tokens actually spent; it does not model the caching a single-model baseline would have had, and the UI says so.

Admin-only, matching /adaptive_router/state. The endpoint carries no new database migration; the new param rides existing router config.

QA runbook

Point a proxy at a database with auto-router traffic that carries session_id (Claude Code sets this automatically). Call GET /auto_router/benchmarks?start_date=<iso>&end_date=<iso> with the master key and confirm one entry per configured auto-router with non-zero metrics. Confirm a non-admin key gets 403 and no key gets 401. Confirm a window wider than 30 days comes back clamped with start_date reflecting the served window. In the UI, open the Autorouter tab under Cost Optimization and confirm the benchmarks panel renders above the creation form. To check the fixed-baseline path, set benchmark_baseline_model: claude-opus-5 on an auto-router deployment and confirm baseline_model in the response reflects 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

Adds GET /auto_router/benchmarks, an admin-only read over spend logs that
reports, per configured auto-router, the four session metrics customers ask
for (turns per session, average session length, tokens per session) plus a
routed-vs-baseline dollar savings estimate.

Session-scoped metrics can't come from the daily rollups because session_id
lives only on LiteLLM_SpendLogs, so this reads spend logs directly over a
window clamped to 30 days, matching the tool-spend endpoint. Spend is
filtered by the auto-router alias, which keeps the turn count honest: the
LLM classifier's own judge calls share the session but carry the judge
model's group, not the alias.

The savings figure compares the routed model mix against sending every
request to a single baseline model, defaulting to the priciest model the
router actually routed to in the window. A per-router benchmark_baseline_model
param pins it to a fixed flagship instead. The estimate uses list prices on
the tokens actually spent and does not model the caching a single-model
baseline would have had; the UI says so.

Surfaced as stat tiles on the Cost Optimization autorouter tab.
COALESCE(SUM(spend), 0.0) AS actual_spend
FROM "LiteLLM_SpendLogs"
WHERE model_group = $1
AND session_id IS NOT NULL

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Synthetic IDs become sessions

When an auto-router request lacks an explicit trace or session identifier, spend logging assigns that request a fresh UUID and this query accepts it as a session. This reports each request as a separate one-turn, near-zero-length session, understating turns and tokens per actual session while overstating the session count.

Knowledge Base Used: Cost Tracking and Budget Enforcement

Comment on lines +84 to +92
return tuple(
(
str(entry["model_name"]),
kind,
_configured_baseline(entry.get("litellm_params")),
)
for entry in (router.model_list or [])
if (model := _entry_model(entry)) is not None and (kind := classify_strategy_router_model(model)) is not None
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Duplicate aliases duplicate benchmarks

When multiple auto-router deployments share a model_name, this enumeration emits each deployment independently even though every subsequent query filters by the same public model group. The response and dashboard therefore repeat identical traffic in multiple cards, potentially labeling it with different router kinds or baselines.

Knowledge Base Used: Cost Tracking and Budget Enforcement

Comment on lines +125 to +131
model=model,
input_cost_per_token=float(info.get("input_cost_per_token") or 0.0),
output_cost_per_token=float(info.get("output_cost_per_token") or 0.0),
)
verbose_proxy_logger.warning(
"auto_router_benchmarks: baseline model %s is not priced; savings will read zero", model
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing prices become zero savings

When benchmark_baseline_model names an unmapped custom, aliased, or newly released model, failed pricing lookup becomes zero rates and the comparison is still returned. This produces a negative dollar saving with savings_pct forced to zero, so the dashboard presents missing price data as a meaningful 0% vs <model> benchmark.

Knowledge Base Used: Cost Tracking and Budget Enforcement

Comment on lines +149 to +150
floor = (end - timedelta(days=BENCHMARKS_MAX_WINDOW_DAYS)).replace(hour=0, minute=0, second=0, microsecond=0)
clamped_start = max(start, floor)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Reversed ranges silently return empty

When start_date is later than end_date, the clamped start remains after the end and the SQL receives an impossible range. The endpoint returns an empty benchmark response instead of rejecting the invalid input, making a malformed request indistinguishable from a window with no routed sessions.

Knowledge Base Used: Cost Tracking and Budget Enforcement

Comment on lines +67 to +69
if (error) {
return null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Benchmark errors render as absence

When the endpoint returns an authorization, validation, or database error, the hook records the failure and this component renders nothing. Users receive the same blank panel as an empty successful response, with no diagnostic or retry path to distinguish unavailable benchmarks from no routed traffic.

Knowledge Base Used: Admin dashboard (ui/litellm-dashboard)

@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds an admin-only auto-router benchmark API and dashboard panel.

  • Aggregates session, token, spend, and estimated baseline savings from spend logs.
  • Adds an optional per-router benchmark baseline configuration field.
  • Fetches and renders benchmark cards in the Cost Optimization auto-router tab.
  • Adds backend and frontend tests for aggregation and presentation.

Confidence Score: 2/5

This PR is not safe to merge until session qualification, duplicate router aliases, invalid date ranges, and unavailable baseline pricing are handled without publishing incorrect benchmark results.

The endpoint can classify individual requests as sessions, duplicate identical traffic under repeated public aliases, silently accept reversed date ranges, and present missing baseline prices as meaningful savings; the dashboard also hides endpoint failures.

Files Needing Attention: litellm/proxy/spend_tracking/auto_router_benchmarks.py and ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksPanel.tsx

Important Files Changed

Filename Overview
litellm/proxy/spend_tracking/auto_router_benchmarks.py Implements benchmark aggregation, but synthetic session IDs, duplicate aliases, invalid ranges, and unpriceable configured baselines produce incorrect or misleading results.
litellm/proxy/proxy_server.py Adds the authenticated, admin-view-gated endpoint and validates router and database availability.
ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksPanel.tsx Renders the new metrics but hides endpoint failures and presents unpriceable baselines as zero-percent comparisons.
ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useAutoRouterBenchmarks.ts Fetches a rolling 30-day benchmark window with guarded asynchronous state updates.
tests/test_litellm/proxy/spend_tracking/test_auto_router_benchmarks.py Covers core calculations but codifies zero pricing for unknown configured baselines and omits generated session IDs, duplicate aliases, and reversed ranges.

Reviews (1): Last reviewed commit: "feat(proxy): add auto-router session ben..." | Re-trigger Greptile

AND session_id IS NOT NULL
AND "startTime" >= ($2::timestamptz AT TIME ZONE 'UTC')
AND "startTime" < (($3::timestamptz + INTERVAL '1 day') AT TIME ZONE 'UTC')
GROUP BY session_id

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low: Unbounded session aggregation

An authenticated user can generate high-cardinality session data by making auto-router requests without reusing a session ID; spend logging assigns each such request a unique UUID. This query returns one row per ID and _fetch_sessions materializes the entire result, so loading the dashboard over a busy 30-day window can exhaust database or worker resources. Aggregate the per-session rows again in SQL to return only the final count, sums, and average session duration needed by the response.

@veria-ai

veria-ai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

PR overview

This PR adds an auto-router session benchmarks endpoint and corresponding proxy UI for viewing session-level performance and spend metrics.

One resource-exhaustion concern remains open in the session aggregation path. An authenticated user can create many unique sessions, causing the benchmarks query and dashboard request to materialize an unbounded result set over the selected window, potentially exhausting database or worker resources. No issues have yet been addressed.

Open issues (1)

Fixed/addressed: 0 · PR risk: 5/10

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.78014% with 13 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/proxy_server.py 15.38% 11 Missing ⚠️
...llm/proxy/spend_tracking/auto_router_benchmarks.py 98.42% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_lit4712_autorouter_benchmarks (703fde9) with litellm_internal_staging (47f1fb3)

Open in CodSpeed

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.

1 participant