diff --git a/apps/web/src/components/usage/UsagePage.tsx b/apps/web/src/components/usage/UsagePage.tsx index 8bc8a93a43d69..aa2fee8f1de22 100644 --- a/apps/web/src/components/usage/UsagePage.tsx +++ b/apps/web/src/components/usage/UsagePage.tsx @@ -87,6 +87,7 @@ import { modelShare, sortModelsByTokens, speedCostSegments, + timeBreakdownMetric, tokenTypeSegments, } from "./usageBreakdown"; import { @@ -638,6 +639,7 @@ function UsageOverview({ const breakdownPeriods: readonly (DailyTotals | HourlyTotals)[] = ( shownHourly ? merged.hourly : merged.daily ).toReversed(); + const timeMetric = timeBreakdownMetric(metric); const breakdownModels = breakdown === "model" && metric === "tokens" ? sortModelsByTokens(merged.models) @@ -961,7 +963,9 @@ function UsageOverview({ ))} Total - Tokens + + {timeMetric.secondaryLabel} + @@ -993,7 +997,7 @@ function UsageOverview({ figureClass(loadingProviders.has(provider)), )} > - {formatUsd(period.byProvider.get(provider)?.costUsd ?? 0)} + {timeMetric.formatValue(period.byProvider.get(provider))} ))} - {formatUsd(period.costUsd)} + {timeMetric.formatValue(period)} - {formatTokens(period.totalTokens)} + {timeMetric.formatSecondaryValue(period)} )) diff --git a/apps/web/src/components/usage/usageBreakdown.test.ts b/apps/web/src/components/usage/usageBreakdown.test.ts index 14fe70017fcbc..15a34c5adb582 100644 --- a/apps/web/src/components/usage/usageBreakdown.test.ts +++ b/apps/web/src/components/usage/usageBreakdown.test.ts @@ -1,7 +1,34 @@ -import type { ModelTotals } from "@t3tools/shared/usageMerge"; +import type { DailyTotals, ModelTotals } from "@t3tools/shared/usageMerge"; import { describe, expect, it } from "vite-plus/test"; -import { cacheHitRate, costPerMillionTokens } from "./usageBreakdown"; +import { cacheHitRate, costPerMillionTokens, timeBreakdownMetric } from "./usageBreakdown"; + +describe("time breakdown metric", () => { + const period: DailyTotals = { + day: "2026-10-22", + costUsd: 3.5, + totalTokens: 2_500_000, + byProvider: new Map([["codex", { costUsd: 3.5, totalTokens: 2_500_000 }]]), + }; + + it.each([ + { + metric: "cost", + value: "$3.50", + missing: "$0.00", + secondaryLabel: "Tokens", + secondary: "2.50M", + }, + { metric: "tokens", value: "2.50M", missing: "0", secondaryLabel: "Cost", secondary: "$3.50" }, + ] as const)("shows $metric figures with the other metric trailing", (row) => { + const figures = timeBreakdownMetric(row.metric); + expect(figures.formatValue(period.byProvider.get("codex"))).toBe(row.value); + expect(figures.formatValue(period.byProvider.get("claude"))).toBe(row.missing); + expect(figures.formatValue(period)).toBe(row.value); + expect(figures.secondaryLabel).toBe(row.secondaryLabel); + expect(figures.formatSecondaryValue(period)).toBe(row.secondary); + }); +}); const model = ( name: string, diff --git a/apps/web/src/components/usage/usageBreakdown.ts b/apps/web/src/components/usage/usageBreakdown.ts index 7fecab508c223..0d5de08d508bb 100644 --- a/apps/web/src/components/usage/usageBreakdown.ts +++ b/apps/web/src/components/usage/usageBreakdown.ts @@ -1,13 +1,34 @@ import type { UsageTokenTotals } from "@t3tools/contracts"; +import { formatTokens, formatUsd } from "@t3tools/shared/usageFormat"; import { isModelCostUnknown, type CategoryCost, + type DailyTotals, type ModelTotals, type SpeedCost, } from "@t3tools/shared/usageMerge"; import type { ShareSegment } from "./UsageShareBar"; +export function timeBreakdownMetric(metric: "cost" | "tokens") { + const formatCost = (totals: Pick | undefined) => + formatUsd(totals?.costUsd ?? 0); + const formatTokenCount = (totals: Pick | undefined) => + formatTokens(totals?.totalTokens ?? 0); + + return metric === "tokens" + ? { + formatValue: formatTokenCount, + secondaryLabel: "Cost", + formatSecondaryValue: formatCost, + } + : { + formatValue: formatCost, + secondaryLabel: "Tokens", + formatSecondaryValue: formatTokenCount, + }; +} + export function sortModelsByTokens(models: readonly ModelTotals[]) { return models.toSorted( (left, right) => right.totalTokens - left.totalTokens || right.costUsd - left.costUsd,