feat(ui): show auto-router classification rate (#40192)

This commit is contained in:
tin-berri 2026-09-07 23:37:42 -07:00 committed by GitHub
parent 1a6aa98230
commit 9a9b4c4c25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 52 additions and 13 deletions

View file

@ -187,10 +187,10 @@ describe("AutoRouterBenchmarksTab", () => {
});
it.each([
{ spend: 20665.28, classifier_cost: 342.18, turns: 140815, llm: "$20,323.10", cost: "$342.18" },
{ spend: 0, classifier_cost: 0, turns: 0, llm: "$0.00", cost: "$0.00" },
{ spend: 0.002, classifier_cost: 0.0004, turns: 100, llm: "$0.0016", cost: "$0.0004" },
])("shows total classification cost across $turns turns without a per-turn rate", ({ llm, cost, ...values }) => {
{ spend: 20665.28, classifier_cost: 342.18, turns: 140815, llm: "$20,323.10", cost: "$342.18", rate: "$2.43" },
{ spend: 0, classifier_cost: 0, turns: 0, llm: "$0.00", cost: "$0.00", rate: "$0.00" },
{ spend: 0.002, classifier_cost: 0.0004, turns: 100, llm: "$0.0016", cost: "$0.0004", rate: "$0.0040" },
])("shows total classification cost and its rate across $turns turns", ({ llm, cost, rate, ...values }) => {
const stats = totals({ ...values, saved_spend: 10126.28, baseline_spend: values.spend + 10126.28 });
mockHook({ data: response([group(stats)], stats) });
renderTab();
@ -201,7 +201,7 @@ describe("AutoRouterBenchmarksTab", () => {
.map((node) => node.textContent)
.slice(1, 3),
).toEqual([llm, cost]);
expect(screen.queryByText(/1K turns/)).not.toBeInTheDocument();
expect(screen.getByText(`(${rate} / 1K turns)`)).toBeInTheDocument();
expect(screen.getAllByText("$10,126.28").length).toBeGreaterThan(0);
});
@ -237,7 +237,7 @@ describe("AutoRouterBenchmarksTab", () => {
expect(terms).toEqual([
"Actual auto-router spend",
"LLM spend",
"Classification cost",
"Classification cost($2.00 / 1K turns)",
"Estimated spend at highest-tier model",
]);
expect(values).toEqual(["$359.86", "$353.71", "$6.15", "$2,534.45"]);

View file

@ -30,7 +30,7 @@ import {
type BenchmarkView,
type BucketRow,
} from "./autoRouterBenchmarks";
import { formatRangeLabel, usd } from "./costOptimizationUtils";
import { classificationRatePer1kTurns, formatRangeLabel, usd } from "./costOptimizationUtils";
import ShadowEvalSection from "./ShadowEvalSection";
import TierTurnsChart from "./TierTurnsChart";
import { useAutoRouterBenchmarks } from "./useAutoRouterBenchmarks";
@ -52,9 +52,17 @@ const Metric: React.FC<{ label: string; value: string; hint?: string }> = ({ lab
</Card>
);
const SpendRow: React.FC<{ label: string; value: string; subdued?: boolean }> = ({ label, value, subdued }) => (
const SpendRow: React.FC<{ label: string; value: string; hint?: string; subdued?: boolean }> = ({
label,
value,
hint,
subdued,
}) => (
<dl className="flex flex-wrap items-baseline justify-between gap-x-6 gap-y-1 py-2">
<dt className="min-w-0 text-sm text-muted-foreground">{label}</dt>
<dt className="flex min-w-0 flex-wrap items-baseline gap-x-2 text-sm text-muted-foreground">
{label}
{hint && <span className="text-xs">{hint}</span>}
</dt>
<dd
className={`min-w-0 break-all tabular-nums ${subdued ? "text-sm font-normal text-muted-foreground" : "text-base font-semibold text-foreground"}`}
>
@ -99,6 +107,11 @@ const HeroCard: React.FC<{ view: BenchmarkView }> = ({ view }) => {
subdued
label="Classification cost"
value={stats.classifier_cost == null ? "Unavailable" : usd(stats.classifier_cost)}
hint={
stats.classifier_cost == null
? undefined
: classificationRatePer1kTurns(stats.classifier_cost, stats.turns)
}
/>
</div>
{stats.classifier_cost == null && (
@ -277,9 +290,10 @@ const BenchmarksBody: React.FC<BenchmarksBodyProps> = ({ isPending, error, data,
<p className="text-xs text-muted-foreground">
Compares your actual routed spend with the estimated cost of using only the most expensive model configured in
the auto-router. It accounts for both the cache savings from staying on one model and the added cache costs from
switching models. Savings are net of recorded LLM classification cost, which is included in actual spend. The
range counts whole sessions that overlap it, so totals can differ slightly from the Overall tab, which buckets
savings by UTC day.
switching models. Savings are net of recorded LLM classification cost, which is included in actual spend.
Classification cost per 1K turns is averaged over all auto-router turns, including those that skip
classification. The range counts whole sessions that overlap it, so totals can differ slightly from the Overall
tab, which buckets savings by UTC day.
</p>
<div className="space-y-4">

View file

@ -7,6 +7,7 @@ import {
SAVINGS_DRIVERS,
SAVINGS_SERIES,
buildDailyToolSeries,
classificationRatePer1kTurns,
computeCacheLeakage,
formatRangeLabel,
isAnthropicModel,
@ -401,6 +402,23 @@ describe("usd", () => {
});
});
describe("classificationRatePer1kTurns", () => {
it("normalizes total classification cost to one thousand turns", () => {
expect(classificationRatePer1kTurns(342.18, 140815)).toBe("($2.43 / 1K turns)");
expect(classificationRatePer1kTurns(0.0004, 100)).toBe("($0.0040 / 1K turns)");
});
it("shows a floor instead of rounding a real cost down to zero", () => {
expect(classificationRatePer1kTurns(0.00001, 1000)).toBe("(<$0.0001 / 1K turns)");
expect(classificationRatePer1kTurns(0.0001, 1000)).toBe("($0.0001 / 1K turns)");
});
it("reports zero when there are no turns or no classification cost", () => {
expect(classificationRatePer1kTurns(0, 0)).toBe("($0.00 / 1K turns)");
expect(classificationRatePer1kTurns(0, 100)).toBe("($0.00 / 1K turns)");
});
});
describe("savings driver colours", () => {
it("keeps a driver's colour when a driver above it is filtered out", () => {
// Charts colour by position in the data they are given, and the donut is given

View file

@ -10,6 +10,13 @@ export const usd = (value: number): string => {
return `${value < 0 ? "-" : ""}$${formatNumberWithCommas(magnitude, decimals)}`;
};
export const classificationRatePer1kTurns = (classifierCost: number, turns: number): string => {
if (turns <= 0) return `(${usd(0)} / 1K turns)`;
const rate = (classifierCost * 1000) / turns;
if (rate > 0 && rate < 0.0001) return "(<$0.0001 / 1K turns)";
return `(${usd(rate)} / 1K turns)`;
};
export const pct = (ratio: number): string => `${formatNumberWithCommas(ratio * 100, 1)}%`;
export const shortDate = (iso: string): string =>

View file

@ -89,7 +89,7 @@ describe("KeyAutoRouterUsageTab", () => {
expect(screen.getByText("$1.00")).toBeInTheDocument();
expect(screen.getByText("Classification cost")).toBeInTheDocument();
expect(screen.getByText("$0.2500")).toBeInTheDocument();
expect(screen.queryByText(/1K turns/)).not.toBeInTheDocument();
expect(screen.getByText("($62.50 / 1K turns)")).toBeInTheDocument();
expect(screen.getByText("Estimated spend at highest-tier model")).toBeInTheDocument();
expect(screen.getByText("$10.00")).toBeInTheDocument();
expect(screen.getByText("Auto-router prompt caching")).toBeInTheDocument();