From e87b4207d4582503a06c451daa10aea695ae3cdc Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Fri, 25 Sep 2026 12:47:22 -0700 Subject: [PATCH] fix(autorouter): compare matching estimates and reuse daily savings totals --- .../auto_router_endpoints.py | 39 ++---- .../auto_router_endpoints.py | 8 +- .../test_auto_router_endpoints.py | 128 +++++++++--------- .../_components/AutoRouterBenchmarksTab.tsx | 11 +- ...KeyAutoRouterUsageTab.integration.test.tsx | 44 ++++++ ui/litellm-dashboard/src/lib/http/schema.d.ts | 10 +- 6 files changed, 138 insertions(+), 102 deletions(-) diff --git a/litellm/proxy/management_endpoints/auto_router_endpoints.py b/litellm/proxy/management_endpoints/auto_router_endpoints.py index 82019267145..c3381924811 100644 --- a/litellm/proxy/management_endpoints/auto_router_endpoints.py +++ b/litellm/proxy/management_endpoints/auto_router_endpoints.py @@ -688,7 +688,9 @@ def _savings_cohort( return saved_spend, actual_spend + saved_spend -def _recorded_baseline_spend(estimated_turns: int, actual_spend: float, saved_spend: float) -> float | None: +def _recorded_baseline_spend(turns: int, estimated_turns: int, actual_spend: float, saved_spend: float) -> float | None: + if 0 < estimated_turns < turns: + return None if estimated_turns == 0 and saved_spend == 0: return None return actual_spend + saved_spend @@ -696,23 +698,27 @@ def _recorded_baseline_spend(estimated_turns: int, actual_spend: float, saved_sp async def _daily_router_costs( prisma_client: "PrismaClient", start_date: str, end_date: str, api_key: str | None, user_id: str | None -) -> AutoRouterDailyCosts: +) -> tuple[float, AutoRouterDailyCosts]: rows: Final = await _query_raw(prisma_client, AUTOROUTER_DAILY_COSTS_SQL, start_date, end_date, api_key, user_id) recorded: Final = AutoRouterDailyCosts.model_validate(rows[0]) if rows else AutoRouterDailyCosts() if recorded.complete: - return recorded + return recorded.saved_spend, recorded try: recovered: Final = await recover_daily_router_costs(prisma_client, start_date, end_date, api_key, user_id) - return recovered if recovered is not None else recorded + return recorded.saved_spend, recovered if recovered is not None else recorded except Exception: # noqa: BLE001 # optional recovery must preserve durable daily savings and costs verbose_proxy_logger.warning("Auto-router historical cost recovery unavailable; using recorded daily costs") - return recorded + return recorded.saved_spend, recorded def _benchmark_totals(row: _SessionAggRow) -> AutoRouterBenchmarkTotals: return_misses: Final = row.return_turns - row.return_hits + comparison_savings: Final = row.savings_estimated_saved_spend if row.savings_estimated_turns else row.saved_spend + comparison_actual: Final = row.savings_estimated_actual_spend if row.savings_estimated_turns else row.spend baseline_spend: Final = ( - _recorded_baseline_spend(row.savings_estimated_turns, row.spend, row.saved_spend) if row.turns else 0.0 + comparison_actual + comparison_savings + if row.savings_estimated_turns or not row.turns + else _recorded_baseline_spend(row.turns, 0, row.spend, row.saved_spend) ) sessions: Final = row.sessions return AutoRouterBenchmarkTotals( @@ -730,7 +736,7 @@ def _benchmark_totals(row: _SessionAggRow) -> AutoRouterBenchmarkTotals: saved_spend=row.saved_spend, classifier_cost=row.classifier_cost if row.classifier_cost_recorded_turns == row.turns else None, baseline_spend=baseline_spend, - saved_pct=_pct(row.saved_spend, baseline_spend) if baseline_spend is not None else None, + saved_pct=_pct(comparison_savings, baseline_spend) if baseline_spend is not None else None, saved_per_session=row.saved_spend / sessions if sessions else 0.0, cache=AutoRouterCacheStats( coverage_pct=_pct(row.covered_turns, row.turns), @@ -901,20 +907,6 @@ async def get_auto_router_benchmarks( if end_day < start_day: raise HTTPException(status_code=400, detail="end_date must not be earlier than start_date") - from litellm.proxy.management_endpoints.common_daily_activity import get_daily_activity_aggregated - - daily: Final = await get_daily_activity_aggregated( - prisma_client=prisma_client, - table_name="litellm_dailyuserspend", - entity_id_field="user_id", - entity_id=user_id, - entity_metadata_field=None, - start_date=start_day.strftime("%Y-%m-%d"), - end_date=end_day.strftime("%Y-%m-%d"), - model=None, - api_key=api_key, - ) - raw_rows: Final = await _query_raw( prisma_client, AUTOROUTER_BENCHMARKS_SQL, @@ -929,14 +921,13 @@ async def get_auto_router_benchmarks( *_idle_router_groups(llm_router, frozenset((row.router_name, row.router_type) for row in rows)), ) session_totals: Final = _benchmark_totals(_summed_agg_row(rows)) - costs: Final = await _daily_router_costs( + saved_spend, costs = await _daily_router_costs( prisma_client, start_day.strftime("%Y-%m-%d"), end_day.strftime("%Y-%m-%d"), api_key, user_id, ) - saved_spend: Final = daily.metadata.total_autorouter_savings_spend baseline_spend: Final = costs.baseline_spend(saved_spend) return AutoRouterBenchmarksResponse( start_date=start_day.strftime("%Y-%m-%d"), @@ -1006,7 +997,7 @@ async def get_auto_router_session( savings_estimated_turns=row.savings_estimated_turns, savings_estimated_actual_spend=row.savings_estimated_actual_spend, saved_spend=row.saved_spend, - baseline_spend=_recorded_baseline_spend(row.savings_estimated_turns, row.spend, row.saved_spend), + baseline_spend=_recorded_baseline_spend(row.turns, row.savings_estimated_turns, row.spend, row.saved_spend), savings_estimated_baseline_spend=estimated_baseline_spend, baseline_model=row.baseline_model, baseline_models=row.baseline_models, diff --git a/litellm/types/management_endpoints/auto_router_endpoints.py b/litellm/types/management_endpoints/auto_router_endpoints.py index 357ee546537..743da7fff56 100644 --- a/litellm/types/management_endpoints/auto_router_endpoints.py +++ b/litellm/types/management_endpoints/auto_router_endpoints.py @@ -246,11 +246,12 @@ class AutoRouterBenchmarkTotals(BaseModel): "groups cover whole overlapping sessions. Turns without an estimate add no savings" ) baseline_spend: float | None = Field( - description="Matching actual spend plus recorded savings. Daily totals compare estimated requests only; " + description="Matching actual spend plus savings for estimated requests. Historical-only groups retain " + "their recorded comparison; " "null when historical cost coverage or the savings estimate is unavailable" ) saved_pct: float | None = Field( - description="Recorded savings divided by the matching estimated baseline; null when that baseline is unavailable" + description="Savings on compared requests divided by their estimated baseline; null when unavailable" ) saved_per_session: float | None = Field( description="Recorded savings in whole overlapping sessions divided by their session count, independent " @@ -296,7 +297,8 @@ class AutoRouterSessionResponse(BaseModel): description="Recorded signed savings, retaining historical estimates and adding current estimates once" ) baseline_spend: float | None = Field( - description="Actual session spend plus recorded savings; turns without an estimate add no savings" + description="Actual session spend plus recorded savings; null for mixed estimated and unestimated turns. " + "Historical-only sessions retain their recorded comparison" ) savings_estimated_baseline_spend: float | None = Field( description="Estimated single-model cost for covered turns only" diff --git a/tests/test_litellm/proxy/management_endpoints/test_auto_router_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_auto_router_endpoints.py index eb4ba238617..c26fc460127 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_auto_router_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_auto_router_endpoints.py @@ -4,7 +4,6 @@ Unit tests for auto router management endpoints import json from collections.abc import Mapping, Sequence -from datetime import date from functools import partial from pathlib import Path from types import SimpleNamespace @@ -44,12 +43,6 @@ from litellm.types.management_endpoints.auto_router_endpoints import ( AutoRouterBenchmarksResponse, AutoRouterRoutingTestRequest, ) -from litellm.types.proxy.management_endpoints.common_daily_activity import ( - DailySpendData, - DailySpendMetadata, - SpendAnalyticsPaginatedResponse, - SpendMetrics, -) from litellm.types.router import Deployment from litellm.types.utils import Choices, Message, ModelResponse @@ -639,24 +632,6 @@ class TestAutoRouterBenchmarks: monkeypatch.setattr(proxy_server, "llm_router", None) - @pytest.fixture(autouse=True) - def _daily_savings(self, monkeypatch: pytest.MonkeyPatch) -> None: - from litellm.proxy.management_endpoints import common_daily_activity - - monkeypatch.setattr( - common_daily_activity, - "get_daily_activity_aggregated", - AsyncMock(return_value=self._daily_result(30.0)), - ) - - @staticmethod - def _daily_result(saved: float, *, has_rows: bool = True) -> SpendAnalyticsPaginatedResponse: - return SpendAnalyticsPaginatedResponse( - results=[DailySpendData(date=date(2026, 7, 15), metrics=SpendMetrics(autorouter_savings_spend=saved))] - if has_rows else [], - metadata=DailySpendMetadata(total_autorouter_savings_spend=saved), - ) - @staticmethod async def _benchmarks( monkeypatch: pytest.MonkeyPatch, @@ -671,7 +646,7 @@ class TestAutoRouterBenchmarks: class _DB: async def query_raw(self, sql: str, *params: object): if sql in (AUTOROUTER_DAILY_COSTS_SQL, AUTOROUTER_HISTORICAL_COSTS_SQL): - return [(costs or AutoRouterDailyCosts(complete=False)).model_dump()] + return [(costs or AutoRouterDailyCosts(complete=False, saved_spend=30.0)).model_dump()] return rows monkeypatch.setattr(proxy_server, "prisma_client", type("P", (), {"db": _DB()})()) @@ -752,8 +727,8 @@ class TestAutoRouterBenchmarks: @pytest.mark.asyncio @pytest.mark.parametrize("estimated_turns,saved_spend,baseline_spend,saved_pct", [ - (0, 30.0, 40.0, 75.0), (4, 30.0, 40.0, 75.0), - (0, 0.0, None, None), (4, 0.0, 10.0, 0.0), (0, -2.0, 8.0, -25.0), + (0, 30.0, 40.0, 75.0), (4, 30.0, 1.5, -33.3), + (0, 0.0, None, None), (4, 0.0, 1.5, -33.3), (0, -2.0, 8.0, -25.0), ]) async def test_session_groups_preserve_history_and_only_compare_known_savings( self, monkeypatch: pytest.MonkeyPatch, estimated_turns: int, saved_spend: float, @@ -775,6 +750,18 @@ class TestAutoRouterBenchmarks: assert group.saved_pct == saved_pct assert group.saved_per_session == saved_spend / row.sessions + @pytest.mark.asyncio + async def test_selected_router_compares_only_estimated_requests(self, monkeypatch: pytest.MonkeyPatch) -> None: + row: Final = self.ROW.model_copy(update={ + "turns": 2, "spend": 100.0, "saved_spend": 1.0, + "savings_estimated_turns": 1, "savings_estimated_actual_spend": 1.0, + "savings_estimated_saved_spend": 1.0, + }) + response: Final = await self._benchmarks(monkeypatch, rows=[row.model_dump()], model_list=[]) + group: Final = response.groups[0] + assert (group.spend, group.saved_spend, group.baseline_spend, group.saved_pct) == (100.0, 1.0, 2.0, 50.0) + assert (group.savings_estimated_turns, group.cost_requests, group.savings_estimated_actual_spend) == (1, 2, 1.0) + @pytest.mark.asyncio async def test_tracked_session_groups_keep_historical_and_new_estimates_once( self, monkeypatch: pytest.MonkeyPatch @@ -821,7 +808,7 @@ class TestAutoRouterBenchmarks: from litellm.proxy.management_endpoints import common_daily_activity from litellm.proxy.management_endpoints.auto_router_endpoints import get_auto_router_benchmarks - daily: Final = AsyncMock(return_value=self._daily_result(daily_saved, has_rows=has_daily_rows)) + daily: Final = AsyncMock(side_effect=AssertionError("Full usage breakdowns must not be loaded")) query: Final = AsyncMock(side_effect=[ [self.ROW.model_dump()] if has_sessions else [], [AutoRouterDailyCosts(complete=not has_daily_rows, saved_spend=daily_saved).model_dump()], @@ -835,11 +822,7 @@ class TestAutoRouterBenchmarks: api_key=api_key, user_id=user_id, ) - daily.assert_awaited_once_with( - prisma_client=prisma, table_name="litellm_dailyuserspend", entity_id_field="user_id", - entity_id=user_id, entity_metadata_field=None, start_date="2026-07-01", end_date="2026-08-01", - model=None, api_key=api_key, - ) + daily.assert_not_awaited() assert query.call_args_list[0].args[1:] == ("2026-07-01T00:00:00", "2026-08-02T00:00:00", api_key, user_id) assert query.call_args_list[1].args[1:] == ("2026-07-01", "2026-08-01", api_key, user_id) assert response.totals.saved_spend == daily_saved @@ -853,30 +836,25 @@ class TestAutoRouterBenchmarks: @pytest.mark.asyncio @pytest.mark.parametrize( - "complete, classifier_requests, estimated, saved, recorded_saved, expected_spend, baseline, pct, coverage", + "complete, classifier_requests, estimated, saved, expected_spend, baseline, pct, coverage", [ - (True, 3, 2, 30.0, 30.0, 10.0, 32.5, 92.3, "complete"), - (True, 3, 0, 0.0, 0.0, 10.0, None, None, "complete"), - (True, 2, 2, 30.0, 30.0, 10.0, None, None, "partial"), - (False, 3, 2, 30.0, 30.0, 10.0, None, None, "partial"), - (True, 3, 2, 30.0, 29.0, 10.0, None, None, "complete"), - (True, 3, 2, -0.5, -0.5, 10.0, 2.0, -25.0, "complete"), + (True, 3, 2, 30.0, 10.0, 32.5, 92.3, "complete"), + (True, 3, 0, 0.0, 10.0, None, None, "complete"), + (True, 2, 2, 30.0, 10.0, None, None, "partial"), + (False, 3, 2, 30.0, 10.0, None, None, "partial"), + (True, 3, 2, -0.5, 10.0, 2.0, -25.0, "complete"), ], - ids=["covered-subset", "unknown-only", "missing-classifier", "partial-history", "pending-savings", "loss"], + ids=["covered-subset", "unknown-only", "missing-classifier", "partial-history", "loss"], ) async def test_daily_costs_preserve_actual_charges_and_compare_only_matching_estimates( self, monkeypatch: pytest.MonkeyPatch, complete: bool, classifier_requests: int, - estimated: int, saved: float, recorded_saved: float, expected_spend: float | None, + estimated: int, saved: float, expected_spend: float | None, baseline: float | None, pct: float | None, coverage: str, ) -> None: - from litellm.proxy.management_endpoints import common_daily_activity - - monkeypatch.setattr(common_daily_activity, "get_daily_activity_aggregated", - AsyncMock(return_value=self._daily_result(saved))) costs: Final = AutoRouterDailyCosts( requests=3, llm_spend=9.7, classifier_cost=0.3, classifier_requests=classifier_requests, estimated_requests=estimated, estimated_actual_spend=2.5 if estimated else 0.0, - saved_spend=recorded_saved, complete=complete, + saved_spend=saved, complete=complete, ) response: Final = await self._benchmarks(monkeypatch, rows=[], model_list=[], costs=costs) totals: Final = response.totals @@ -893,21 +871,20 @@ class TestAutoRouterBenchmarks: assert totals.sessions == totals.turns == 0 @pytest.mark.asyncio - @pytest.mark.parametrize("recovery", ["complete", "partial", "empty", "failure", "invalid", "not-needed"]) + @pytest.mark.parametrize("recovery", ["complete", "partial", "empty", "failure", "invalid", "not-needed", "mismatched"]) async def test_historical_cost_recovery_preserves_durable_savings_and_request_scope( self, monkeypatch: pytest.MonkeyPatch, recovery: str, ) -> None: - from litellm.proxy.management_endpoints import common_daily_activity from litellm.proxy.route_llm_request import ROUTE_ENDPOINT_MAPPING recovered: Final = AutoRouterDailyCosts( requests=3, llm_spend=4.5, classifier_cost=0.5, classifier_requests=3, - estimated_requests=2, estimated_actual_spend=3.0, saved_spend=6.5, + estimated_requests=2, estimated_actual_spend=3.0, saved_spend=7.5 if recovery == "mismatched" else 6.5, complete=recovery != "partial", ) recorded: Final = recovered if recovery == "not-needed" else AutoRouterDailyCosts( requests=1, llm_spend=2.0, classifier_cost=0.25, classifier_requests=1, - estimated_requests=1, estimated_actual_spend=2.25, saved_spend=1.5, complete=False, + estimated_requests=1, estimated_actual_spend=2.25, saved_spend=6.5, complete=False, ) recovery_result: Final = { "empty": [], "failure": RuntimeError("retained logs unavailable"), "invalid": [{"unexpected": True}], @@ -919,8 +896,6 @@ class TestAutoRouterBenchmarks: monkeypatch.setattr(proxy_server, "prisma_client", SimpleNamespace( db=SimpleNamespace(query_raw=query), read_db=reader, )) - monkeypatch.setattr(common_daily_activity, "get_daily_activity_aggregated", - AsyncMock(return_value=self._daily_result(6.5))) response: Final = await auto_router_endpoints.get_auto_router_benchmarks( user_api_key_dict=ADMIN, start_date="2026-07-01", end_date="2026-08-01", api_key="selected-key", user_id="selected-user", @@ -934,7 +909,7 @@ class TestAutoRouterBenchmarks: ) comparable: Final = recovery in ("complete", "not-needed") assert (totals.baseline_spend, totals.saved_pct) == ((9.5, 68.4) if comparable else (None, None)) - assert totals.cost_coverage == ("complete" if comparable else "partial") + assert totals.cost_coverage == ("complete" if comparable or recovery == "mismatched" else "partial") assert query.await_count == (2 if recovery == "not-needed" else 3) if recovery != "not-needed": sql, start, end, key, user, endpoints = query.call_args.args @@ -944,14 +919,10 @@ class TestAutoRouterBenchmarks: @pytest.mark.asyncio async def test_daily_savings_failure_never_falls_back_to_session_savings(self, monkeypatch: pytest.MonkeyPatch) -> None: - from litellm.proxy.management_endpoints import common_daily_activity - - monkeypatch.setattr( - common_daily_activity, "get_daily_activity_aggregated", - AsyncMock(side_effect=HTTPException(status_code=500, detail="Daily savings unavailable")), - ) - with pytest.raises(HTTPException, match="Daily savings unavailable"): - await self._benchmarks(monkeypatch, rows=[self.ROW.model_dump()], model_list=[]) + query: Final = AsyncMock(side_effect=[[self.ROW.model_dump()], RuntimeError("Daily savings unavailable")]) + monkeypatch.setattr(proxy_server, "prisma_client", SimpleNamespace(db=SimpleNamespace(query_raw=query))) + with pytest.raises(RuntimeError, match="Daily savings unavailable"): + await auto_router_endpoints.get_auto_router_benchmarks(user_api_key_dict=ADMIN) def test_an_empty_window_folds_to_zeros(self): from litellm.proxy.management_endpoints.auto_router_endpoints import ( @@ -1070,7 +1041,7 @@ class TestAutoRouterBenchmarks: class _DB: async def query_raw(self, sql: str, *params: object): if sql in (AUTOROUTER_DAILY_COSTS_SQL, AUTOROUTER_HISTORICAL_COSTS_SQL): - return [AutoRouterDailyCosts(complete=False).model_dump()] + return [AutoRouterDailyCosts(complete=False, saved_spend=30.0).model_dump()] captured["sql"] = sql captured["params"] = params return [TestAutoRouterBenchmarks.ROW.model_dump()] @@ -1359,13 +1330,40 @@ class TestAutoRouterSession: "saved_spend": saved_spend, "savings_estimated_turns": 3 if estimated else 0, "savings_estimated_actual_spend": 0.14 if estimated else 0.0, - "baseline_spend": pytest.approx(spend + saved_spend) if estimated or saved_spend != 0 else None, + "baseline_spend": pytest.approx(spend + saved_spend) + if (estimated and turns == 3) or (not estimated and saved_spend != 0) else None, "savings_estimated_baseline_spend": pytest.approx(spend + saved_spend if turns == 3 else 0.1) if estimated else None, "baseline_model": "anthropic/claude-opus-5", "baseline_models": {"anthropic/claude-opus-5": 3}, } + @pytest.mark.asyncio + async def test_mixed_session_compares_estimated_turns_in_the_cli( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, + ) -> None: + from litellm.proxy.client.cli.commands.statusline_script import _session_from_payload, render + from litellm.proxy.management_endpoints.auto_router_endpoints import get_auto_router_session + + caller: Final = UserAPIKeyAuth(api_key="sk-caller") + self._rig(monkeypatch, [{ + **self.ROW, "api_key": caller.api_key, "session_id": "mixed-estimates", + "turns": 2, "spend": 100.0, "saved_spend": 1.0, + "savings_estimated_turns": 1, "savings_estimated_actual_spend": 1.0, + "savings_estimated_saved_spend": 1.0, + }]) + + response: Final = await get_auto_router_session(user_api_key_dict=caller, session_id="mixed-estimates") + assert (response.spend, response.saved_spend, response.baseline_spend) == (100.0, 1.0, None) + assert response.savings_estimated_baseline_spend == 2.0 + session: Final = _session_from_payload(response.model_dump()) + assert session is not None + rendered: Final = render("claude-sonnet-5", session, tmp_path, use_color=False) + assert "-50%" in rendered + assert "1 of 2 turns estimated" in rendered + assert "$1.00" in rendered and "$2.00" in rendered + assert "$100.00" not in rendered + @pytest.mark.asyncio async def test_unknown_only_session_keeps_savings_unavailable_in_the_cli( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.tsx index ced592c073e..6842af45c75 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.tsx @@ -116,10 +116,11 @@ const CostBreakdown: React.FC<{ view: BenchmarkView; partialComparison: boolean const HeroCard: React.FC<{ view: BenchmarkView }> = ({ view }) => { const stats = view.stats; - const cheaper = stats.saved_spend != null && stats.saved_spend >= 0; - const dailyCosts = viewGroup(view) === null; + const cheaper = stats.saved_pct != null && stats.saved_pct >= 0; + const comparisonRequests = viewGroup(view) ? stats.turns : stats.cost_requests; + const trackedComparison = stats.savings_estimated_turns > 0 || stats.baseline_spend == null; const partialComparison = - dailyCosts && stats.cost_requests != null && stats.savings_estimated_turns < stats.cost_requests; + trackedComparison && comparisonRequests != null && stats.savings_estimated_turns < comparisonRequests; return (
@@ -136,14 +137,14 @@ const HeroCard: React.FC<{ view: BenchmarkView }> = ({ view }) => { variant="secondary" className={`h-6 px-2.5 text-sm ${cheaper ? "bg-success/10 text-success" : "bg-destructive/10 text-destructive"}`} > - {stats.saved_spend !== 0 && (cheaper ? "-" : "+")} + {stats.saved_pct !== 0 && (cheaper ? "-" : "+")} {Math.abs(stats.saved_pct).toFixed(0)}% )}
{partialComparison && (

- {stats.savings_estimated_turns.toLocaleString()} of {stats.cost_requests?.toLocaleString()} requests have + {stats.savings_estimated_turns.toLocaleString()} of {comparisonRequests?.toLocaleString()} requests have savings estimates

)} diff --git a/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.integration.test.tsx b/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.integration.test.tsx index f10b27e4b7d..b20a9ed2098 100644 --- a/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.integration.test.tsx @@ -136,4 +136,48 @@ describe("KeyAutoRouterUsageTab", () => { expect(screen.getByText("Auto-router prompt caching")).toBeInTheDocument(); expect(screen.getAllByText("50.0%").length).toBeGreaterThan(0); }); + + it("shows matching estimated costs and coverage when selecting a mixed-coverage router", async () => { + const user = userEvent.setup(); + const mixed = { + ...stats, + turns: 2, + spend: 100, + llm_spend: 100, + classifier_cost: 0, + savings_estimated_turns: 1, + savings_estimated_actual_spend: 1, + saved_spend: 1, + baseline_spend: 2, + saved_pct: 50, + }; + vi.stubGlobal("fetch", async (request: Request | string) => { + const url = typeof request === "string" ? request : request.url; + return jsonResponse( + url.includes("/auto_router/benchmarks") + ? { ...benchmarks, groups: [{ ...benchmarks.groups[0], ...mixed }] } + : noDeployments, + ); + }); + const activity = { + dateValue: { from: new Date(2025, 0, 1), to: new Date(2025, 0, 31) }, + onDateChange: vi.fn(), + }; + renderWithProviders(); + expect(await screen.findByText("$12.75")).toBeInTheDocument(); + + await user.click(screen.getByRole("combobox")); + await user.click(await screen.findByRole("option", { name: "router-one" })); + + expect(screen.getByText("-50%")).toBeInTheDocument(); + expect(screen.getByText("1 of 2 requests have savings estimates")).toBeInTheDocument(); + expect(screen.getByText("Actual spend on estimated requests")).toBeInTheDocument(); + expect(screen.getAllByRole("definition").map((node) => node.textContent)).toEqual([ + "$100.00", + "$100.00", + "$0.00", + "$1.00", + "$2.00", + ]); + }); }); diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index beb712154c9..98d66f6470f 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -24890,7 +24890,7 @@ export interface components { avg_turns_per_session: number; /** * Baseline Spend - * @description Matching actual spend plus recorded savings. Daily totals compare estimated requests only; null when historical cost coverage or the savings estimate is unavailable + * @description Matching actual spend plus savings for estimated requests. Historical-only groups retain their recorded comparison; null when historical cost coverage or the savings estimate is unavailable */ baseline_spend: number | null; cache: components["schemas"]["AutoRouterCacheStats"]; @@ -24928,7 +24928,7 @@ export interface components { router_type: string; /** * Saved Pct - * @description Recorded savings divided by the matching estimated baseline; null when that baseline is unavailable + * @description Savings on compared requests divided by their estimated baseline; null when unavailable */ saved_pct: number | null; /** @@ -24981,7 +24981,7 @@ export interface components { avg_turns_per_session: number; /** * Baseline Spend - * @description Matching actual spend plus recorded savings. Daily totals compare estimated requests only; null when historical cost coverage or the savings estimate is unavailable + * @description Matching actual spend plus savings for estimated requests. Historical-only groups retain their recorded comparison; null when historical cost coverage or the savings estimate is unavailable */ baseline_spend: number | null; cache: components["schemas"]["AutoRouterCacheStats"]; @@ -25009,7 +25009,7 @@ export interface components { llm_spend?: number | null; /** * Saved Pct - * @description Recorded savings divided by the matching estimated baseline; null when that baseline is unavailable + * @description Savings on compared requests divided by their estimated baseline; null when unavailable */ saved_pct: number | null; /** @@ -25318,7 +25318,7 @@ export interface components { }; /** * Baseline Spend - * @description Actual session spend plus recorded savings; turns without an estimate add no savings + * @description Actual session spend plus recorded savings; null for mixed estimated and unestimated turns. Historical-only sessions retain their recorded comparison */ baseline_spend: number | null; /**