mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
refactor(ui): derive every savings chart and total from the driver list
Four call sites each named the metric their caching series plots, and they drifted: the tiles and donut moved to the injected share while the key page's timeline kept the total. SAVINGS_DRIVERS now carries each driver's accessor, savingsSeriesOf builds the one series both timelines render, the donut and the Total saved tile sum the same list, so a driver's source of truth cannot vary by surface. Also ratchets the inline-object budget the extracted fixtures freed
This commit is contained in:
parent
5851327a6e
commit
94d6ff942a
7 changed files with 90 additions and 83 deletions
|
|
@ -3,6 +3,6 @@
|
|||
"no-console": { "max": 12, "target": 0 },
|
||||
"complexity": { "max": 140, "target": 80 },
|
||||
"max-depth": { "max": 70, "target": 30 },
|
||||
"local/no-large-inline-object-arg": { "max": 560, "target": 300 },
|
||||
"local/no-large-inline-object-arg": { "max": 559, "target": 300 },
|
||||
"local/no-long-condition-chain": { "max": 265, "target": 120 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,20 +140,19 @@ describe("UsageTab", () => {
|
|||
// Total caching and the LiteLLM-injected share deliberately differ so these
|
||||
// assertions pin which one each figure uses: the caching headline and the
|
||||
// Total-saved tile take the injected share, the secondary keeps the total.
|
||||
const { getByText } = renderWith([
|
||||
day("2026-07-12", {
|
||||
compression_savings_spend: 0.04,
|
||||
prompt_caching_savings_spend: 0.006,
|
||||
gateway_injected_caching_savings_spend: 0.004,
|
||||
compression_saved_tokens: 40000,
|
||||
}),
|
||||
day("2026-07-13", {
|
||||
compression_savings_spend: 0.1,
|
||||
prompt_caching_savings_spend: 0.01,
|
||||
gateway_injected_caching_savings_spend: 0.006,
|
||||
compression_saved_tokens: 100000,
|
||||
}),
|
||||
]);
|
||||
const firstDay: Partial<SpendMetrics> = {
|
||||
compression_savings_spend: 0.04,
|
||||
prompt_caching_savings_spend: 0.006,
|
||||
gateway_injected_caching_savings_spend: 0.004,
|
||||
compression_saved_tokens: 40000,
|
||||
};
|
||||
const secondDay: Partial<SpendMetrics> = {
|
||||
compression_savings_spend: 0.1,
|
||||
prompt_caching_savings_spend: 0.01,
|
||||
gateway_injected_caching_savings_spend: 0.006,
|
||||
compression_saved_tokens: 100000,
|
||||
};
|
||||
const { getByText } = renderWith([day("2026-07-12", firstDay), day("2026-07-13", secondDay)]);
|
||||
|
||||
expect(getByText("$0.1500")).toBeInTheDocument();
|
||||
expect(getByText("$0.1400")).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -9,11 +9,8 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||
import useCan from "@/app/(dashboard)/hooks/useCan";
|
||||
import { getToolSpend, ToolSpendResponse } from "@/components/networking";
|
||||
import {
|
||||
autorouterOf,
|
||||
buildDailyToolSeries,
|
||||
compressionOf,
|
||||
formatRangeLabel,
|
||||
gatewayAttributedCachingOf,
|
||||
localIsoDay,
|
||||
MAX_POINTS_WITH_DOTS,
|
||||
SAVINGS_COLORS,
|
||||
|
|
@ -21,13 +18,15 @@ import {
|
|||
SAVINGS_SERIES,
|
||||
SavingsAccumulation,
|
||||
SavingsPoint,
|
||||
savingsSeriesOf,
|
||||
shortDate,
|
||||
sumOverDays,
|
||||
toCumulative,
|
||||
topToolsBySpend,
|
||||
usd,
|
||||
withStartAnchor,
|
||||
} from "./costOptimizationUtils";
|
||||
import SavingsTiles, { useSavingsTotals } from "@/components/shared/SavingsTiles";
|
||||
import SavingsTiles from "@/components/shared/SavingsTiles";
|
||||
import { DailyActivityRange } from "./useDailyActivityRange";
|
||||
|
||||
interface UsageTabProps {
|
||||
|
|
@ -73,26 +72,9 @@ const UsageTab: React.FC<UsageTabProps> = ({ accessToken, activity }) => {
|
|||
const toolSpend = toolSpendState?.key === rangeKey ? toolSpendState.data : null;
|
||||
const toolSpendLoading = toolSpendEnabled && toolSpend === null;
|
||||
|
||||
const totals = useSavingsTotals(results);
|
||||
|
||||
const [accumulation, setAccumulation] = useState<SavingsAccumulation>("cumulative");
|
||||
|
||||
// The daily rollup arrives newest first; sort on the raw ISO date so the axis
|
||||
// reads oldest to newest and the running total accumulates forward in time
|
||||
// rather than backward. Sort here, before shortDate() drops the year and makes
|
||||
// the labels unsortable.
|
||||
const perInterval = useMemo<SavingsPoint[]>(
|
||||
() =>
|
||||
[...results]
|
||||
.sort((a, b) => a.date.localeCompare(b.date))
|
||||
.map((d) => ({
|
||||
date: shortDate(d.date),
|
||||
Compression: compressionOf(d.metrics),
|
||||
"Prompt caching": gatewayAttributedCachingOf(d.metrics),
|
||||
"Auto-router": autorouterOf(d.metrics),
|
||||
})),
|
||||
[results],
|
||||
);
|
||||
const perInterval = useMemo<SavingsPoint[]>(() => savingsSeriesOf(results), [results]);
|
||||
|
||||
// Cumulative anchors on a synthetic $0 point at the range start so a short
|
||||
// range (down to a single day) rises from zero instead of floating as one dot.
|
||||
|
|
@ -116,16 +98,12 @@ const UsageTab: React.FC<UsageTabProps> = ({ accessToken, activity }) => {
|
|||
// that actually saved are plotted; the range total keeps the signed truth.
|
||||
const byDriver = useMemo(
|
||||
() =>
|
||||
SAVINGS_DRIVERS.map(({ name, color }) => ({
|
||||
SAVINGS_DRIVERS.map(({ name, color, of }) => ({
|
||||
driver: name,
|
||||
color,
|
||||
usd: {
|
||||
Compression: totals.compression,
|
||||
"Prompt caching": totals.gatewayAttributedCaching,
|
||||
"Auto-router": totals.autorouter,
|
||||
}[name],
|
||||
usd: sumOverDays(results, of),
|
||||
})).filter((d) => d.usd > 0),
|
||||
[totals],
|
||||
[results],
|
||||
);
|
||||
const plottedDriverTotal = useMemo(() => byDriver.reduce((sum, d) => sum + d.usd, 0), [byDriver]);
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
formatRangeLabel,
|
||||
isAnthropicModel,
|
||||
localIsoDay,
|
||||
savingsSeriesOf,
|
||||
toCumulative,
|
||||
topToolsBySpend,
|
||||
usd,
|
||||
|
|
@ -66,6 +67,28 @@ const modelDay = (date: string, models: Record<string, Partial<SpendMetrics>>):
|
|||
},
|
||||
});
|
||||
|
||||
describe("savingsSeriesOf", () => {
|
||||
it("plots the LiteLLM-injected caching share, sorted oldest first", () => {
|
||||
// Total and injected caching deliberately differ: every chart derives from
|
||||
// SAVINGS_DRIVERS, so the caching series must follow the injected figure.
|
||||
const sharedSavings: Partial<SpendMetrics> = {
|
||||
compression_savings_spend: 0.1,
|
||||
prompt_caching_savings_spend: 0.5,
|
||||
autorouter_savings_spend: 0.05,
|
||||
};
|
||||
const newestFirst = [day("2026-07-02", {}), day("2026-07-01", {})].map((d, i) => ({
|
||||
...d,
|
||||
metrics: metrics({ ...sharedSavings, gateway_injected_caching_savings_spend: i === 0 ? 0.2 : 0.3 }),
|
||||
}));
|
||||
|
||||
const series = savingsSeriesOf(newestFirst);
|
||||
|
||||
expect(series.map((p) => p.date)).toEqual(["Jul 1", "Jul 2"]);
|
||||
expect(series[0]).toMatchObject({ Compression: 0.1, "Prompt caching": 0.3, "Auto-router": 0.05 });
|
||||
expect(series[1]).toMatchObject({ Compression: 0.1, "Prompt caching": 0.2, "Auto-router": 0.05 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeCacheLeakage", () => {
|
||||
it("aggregates a key's tokens and savings across multiple days", () => {
|
||||
const results = [
|
||||
|
|
|
|||
|
|
@ -193,14 +193,38 @@ export type SavingsPoint = {
|
|||
* mapping. Colour travels with the driver so filtering cannot separate them.
|
||||
*/
|
||||
export const SAVINGS_DRIVERS = [
|
||||
{ name: "Compression", color: "emerald" },
|
||||
{ name: "Prompt caching", color: "blue" },
|
||||
{ name: "Auto-router", color: "amber" },
|
||||
{ name: "Compression", color: "emerald", of: compressionOf },
|
||||
{ name: "Prompt caching", color: "blue", of: gatewayAttributedCachingOf },
|
||||
{ name: "Auto-router", color: "amber", of: autorouterOf },
|
||||
] as const;
|
||||
|
||||
export const SAVINGS_SERIES = SAVINGS_DRIVERS.map((d) => d.name);
|
||||
export const SAVINGS_COLORS = SAVINGS_DRIVERS.map((d) => d.color);
|
||||
|
||||
type SavingsDriverName = (typeof SAVINGS_DRIVERS)[number]["name"];
|
||||
|
||||
export const sumOverDays = (results: readonly DailyData[], of: (m: SpendMetrics) => number): number =>
|
||||
results.reduce((sum, d) => sum + of(d.metrics), 0);
|
||||
|
||||
/**
|
||||
* One point per day, each driver plotting the metric its SAVINGS_DRIVERS entry
|
||||
* names. The rollup arrives newest first, so sort on the raw ISO date before
|
||||
* shortDate() drops the year and makes the labels unsortable; the running total
|
||||
* then accumulates forward in time. Deriving every chart's series and every
|
||||
* total from the same driver list is what keeps a tile, a timeline and the
|
||||
* donut from quietly plotting different metrics for the same driver name.
|
||||
*/
|
||||
export const savingsSeriesOf = (results: readonly DailyData[]): SavingsPoint[] =>
|
||||
[...results]
|
||||
.sort((a, b) => a.date.localeCompare(b.date))
|
||||
.map((d) => ({
|
||||
date: shortDate(d.date),
|
||||
...(Object.fromEntries(SAVINGS_DRIVERS.map(({ name, of }) => [name, of(d.metrics)])) as Record<
|
||||
SavingsDriverName,
|
||||
number
|
||||
>), // fromEntries widens keys to string; the entries are exactly the driver names
|
||||
}));
|
||||
|
||||
/**
|
||||
* Running total of each series across the selected window. The total restarts
|
||||
* at the beginning of the range rather than carrying in earlier spend, which is
|
||||
|
|
|
|||
|
|
@ -6,32 +6,30 @@ import SummaryCard from "@/components/shared/SummaryCard";
|
|||
import {
|
||||
autorouterOf,
|
||||
cachingOf,
|
||||
gatewayAttributedCachingOf,
|
||||
compressionOf,
|
||||
gatewayAttributedCachingOf,
|
||||
SAVINGS_DRIVERS,
|
||||
savedTokensOf,
|
||||
sumOverDays,
|
||||
usd,
|
||||
} from "@/app/(dashboard)/cost-optimization/_components/costOptimizationUtils";
|
||||
import { DailyData } from "@/components/UsagePage/types";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
|
||||
// Exported because the by-driver donut has to slice the same numbers the tiles print, and two
|
||||
// totalling paths over the same rows is how a chart and the tile above it end up disagreeing.
|
||||
export const useSavingsTotals = (results: DailyData[]) =>
|
||||
useMemo(() => {
|
||||
const sumOf = (of: (metrics: DailyData["metrics"]) => number) => results.reduce((sum, d) => sum + of(d.metrics), 0);
|
||||
const compression = sumOf(compressionOf);
|
||||
const caching = sumOf(cachingOf);
|
||||
const autorouter = sumOf(autorouterOf);
|
||||
const gatewayAttributedCaching = sumOf(gatewayAttributedCachingOf);
|
||||
return {
|
||||
compression,
|
||||
caching,
|
||||
autorouter,
|
||||
gatewayAttributedCaching,
|
||||
savedTokens: sumOf(savedTokensOf),
|
||||
total: compression + gatewayAttributedCaching + autorouter,
|
||||
};
|
||||
}, [results]);
|
||||
// The total sums SAVINGS_DRIVERS, so it is by construction the sum of what the
|
||||
// charts plot; the donut and timelines derive from the same list in costOptimizationUtils.
|
||||
const useSavingsTotals = (results: DailyData[]) =>
|
||||
useMemo(
|
||||
() => ({
|
||||
compression: sumOverDays(results, compressionOf),
|
||||
caching: sumOverDays(results, cachingOf),
|
||||
autorouter: sumOverDays(results, autorouterOf),
|
||||
gatewayAttributedCaching: sumOverDays(results, gatewayAttributedCachingOf),
|
||||
savedTokens: sumOverDays(results, savedTokensOf),
|
||||
total: SAVINGS_DRIVERS.reduce((sum, { of }) => sum + sumOverDays(results, of), 0),
|
||||
}),
|
||||
[results],
|
||||
);
|
||||
|
||||
const SavingsTiles = ({ results, isLoading }: { results: DailyData[]; isLoading: boolean }) => {
|
||||
const totals = useSavingsTotals(results);
|
||||
|
|
|
|||
|
|
@ -9,9 +9,6 @@ import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle }
|
|||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { hasProxyWideSpendView, spendScopeUserId } from "@/utils/roles";
|
||||
import {
|
||||
autorouterOf,
|
||||
cachingOf,
|
||||
compressionOf,
|
||||
formatRangeLabel,
|
||||
localIsoDay,
|
||||
MAX_POINTS_WITH_DOTS,
|
||||
|
|
@ -19,6 +16,7 @@ import {
|
|||
SAVINGS_SERIES,
|
||||
SavingsAccumulation,
|
||||
SavingsPoint,
|
||||
savingsSeriesOf,
|
||||
shortDate,
|
||||
toCumulative,
|
||||
usd,
|
||||
|
|
@ -50,20 +48,7 @@ const KeySavingsTab: React.FC<KeySavingsTabProps> = ({ accessToken, keyToken, us
|
|||
|
||||
const [accumulation, setAccumulation] = useState<SavingsAccumulation>("cumulative");
|
||||
|
||||
// Sort on the raw ISO date before shortDate() drops the year: the rollup arrives newest
|
||||
// first, and the running total has to accumulate forward in time.
|
||||
const perInterval = useMemo<SavingsPoint[]>(
|
||||
() =>
|
||||
[...results]
|
||||
.sort((a, b) => a.date.localeCompare(b.date))
|
||||
.map((d) => ({
|
||||
date: shortDate(d.date),
|
||||
Compression: compressionOf(d.metrics),
|
||||
"Prompt caching": cachingOf(d.metrics),
|
||||
"Auto-router": autorouterOf(d.metrics),
|
||||
})),
|
||||
[results],
|
||||
);
|
||||
const perInterval = useMemo<SavingsPoint[]>(() => savingsSeriesOf(results), [results]);
|
||||
|
||||
const overTime = useMemo(() => {
|
||||
if (accumulation !== "cumulative") return perInterval;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue