Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/desktop-tauri/src/i18n/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,12 @@ export const ALL_LOCALE_KEYS = [
"UsageSpendShare",
"UsageSpendCopyJson",
"UsageSpendSaveJson",
"UsageSpendDailySpend",
"UsageSpendTrendDaily",
"UsageSpendTrendHourly",
"UsageSpendClearSelectedDay",
"UsageSpendSelectedDayScope",
"UsageSpendPartialModelBreakdown",
"AgentSessionsTitle",
"AgentSessionsEnableLabel",
"AgentSessionsEnableHelper",
Expand Down
101 changes: 101 additions & 0 deletions apps/desktop-tauri/src/lib/usageSpendBreakdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { describe, expect, it } from "vitest";

import {
buildSpendProviderGroups,
compareSpendProviderGroups,
findSpendDay,
formatSpendDay,
type SpendProviderGroup,
} from "./usageSpendBreakdown";
import type { SpendContract, SpendModelRow } from "../types/bridge";

const model = (name: string, costUsd: number | null, totalTokens: number): SpendModelRow => ({
model: name,
costUsd,
inputTokens: totalTokens,
outputTokens: 0,
cacheReadTokens: 0,
totalTokens,
customPricing: false,
});

const contract = (overrides: Partial<SpendContract> = {}): SpendContract =>
({
providerId: "codex",
knownCostUsd: 3.5,
historyCoverageEstablished: true,
priceCoverage: { priced: 2, unpriced: 0, unmetered: 0, estimated: 0 },
models: [model("gpt-5", 3, 1000), model("gpt-5-mini", 0.5, 500)],
daily: [
{ day: "2026-09-18", costUsd: 1, totalTokens: 100 },
{ day: "2026-09-19", costUsd: null, totalTokens: null },
],
...overrides,
}) as SpendContract;

const group = (displayName: string, costUsd: number | null, totalTokens: number | null): SpendProviderGroup => ({
providerId: displayName.toLowerCase(),
displayName,
costUsd,
totalTokens,
costIsPartial: false,
hasPartialModelHistory: false,
models: [],
});

describe("usage spend provider breakdown", () => {
it("groups the contract models under their provider with summed tokens", () => {
const [codex] = buildSpendProviderGroups(contract(), "Codex");
expect(codex).toMatchObject({
providerId: "codex",
displayName: "Codex",
costUsd: 3.5,
totalTokens: 1500,
costIsPartial: false,
hasPartialModelHistory: false,
});
expect(codex.models.map((row) => row.model)).toEqual(["gpt-5", "gpt-5-mini"]);
});

it("returns no groups without model-level history", () => {
expect(buildSpendProviderGroups(contract({ models: [] }), "Codex")).toEqual([]);
});

it("flags unpriced models and incomplete history as partial", () => {
const [codex] = buildSpendProviderGroups(
contract({
priceCoverage: { priced: 1, unpriced: 1, unmetered: 0, estimated: 0 },
historyCoverageEstablished: false,
}),
"Codex",
);
expect(codex.costIsPartial).toBe(true);
expect(codex.hasPartialModelHistory).toBe(true);
});

it("orders groups by cost, then tokens, then name with unknown values last", () => {
const ordered = [
group("Zed", null, null),
group("Beta", 2, 10),
group("Alpha", 2, 10),
group("Gamma", 5, 1),
group("Tokens", null, 99),
]
.sort(compareSpendProviderGroups)
.map((entry) => entry.displayName);
expect(ordered).toEqual(["Gamma", "Alpha", "Beta", "Tokens", "Zed"]);
});

it("finds a selected day only while it is inside the current range", () => {
const { daily } = contract();
expect(findSpendDay(daily, "2026-09-18")?.costUsd).toBe(1);
expect(findSpendDay(daily, "2026-01-01")).toBeNull();
expect(findSpendDay(daily, null)).toBeNull();
});

it("formats dashboard days and falls back for invalid dates", () => {
expect(formatSpendDay("2026-09-19")).toContain("2026");
expect(formatSpendDay("2026-02-31")).toBe("2026-02-31");
expect(formatSpendDay("not-a-day")).toBe("not-a-day");
});
});
78 changes: 78 additions & 0 deletions apps/desktop-tauri/src/lib/usageSpendBreakdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { SpendContract, SpendDailyPoint, SpendModelRow } from "../types/bridge";

/** Upstream 0.64.0 (#3353) `spendProviderModelDisplayLimit`. */
export const PROVIDER_MODEL_DISPLAY_LIMIT = 6;

/** One provider group in the Usage & Spend breakdown: its spend plus its models. */
export interface SpendProviderGroup {
providerId: string;
displayName: string;
costUsd: number | null;
totalTokens: number | null;
/** True when the cost is a lower bound (unpriced models) and renders with "~". */
costIsPartial: boolean;
/** True when model history is incomplete, so the model list is a partial view. */
hasPartialModelHistory: boolean;
models: SpendModelRow[];
}

/**
* Upstream `spendDashboardProviderBreakdowns` ordering: highest known cost
* first, then highest tokens, unknown values last, then display name.
*/
export function compareSpendProviderGroups(a: SpendProviderGroup, b: SpendProviderGroup): number {
const byValue = (left: number | null, right: number | null): number => {
if (left != null && right != null) return left === right ? 0 : right - left;
if (left != null) return -1;
if (right != null) return 1;
return 0;
};
return (
byValue(a.costUsd, b.costUsd) ||
byValue(a.totalTokens, b.totalTokens) ||
a.displayName.localeCompare(b.displayName, undefined, { numeric: true, sensitivity: "base" })
);
}

/**
* Group the contract's model rows by the provider that produced them.
*
* The desktop shell builds model-level history for one provider contract
* (`contract.providerId`), so that is the only group with models; providers
* without model-level history stay in the provider table.
*/
export function buildSpendProviderGroups(
contract: SpendContract,
providerName: string,
): SpendProviderGroup[] {
if (contract.models.length === 0) return [];
const totalTokens = contract.models.reduce((sum, model) => sum + model.totalTokens, 0);
const group: SpendProviderGroup = {
providerId: contract.providerId,
displayName: providerName,
costUsd: contract.knownCostUsd,
totalTokens: Number.isSafeInteger(totalTokens) ? totalTokens : null,
costIsPartial: contract.priceCoverage.unpriced > 0,
hasPartialModelHistory: !contract.historyCoverageEstablished,
models: contract.models,
};
return [group].sort(compareSpendProviderGroups);
}

/** The daily point for `day`, or null when that day is not in the current range. */
export function findSpendDay(daily: SpendDailyPoint[], day: string | null): SpendDailyPoint | null {
if (!day) return null;
return daily.find((point) => point.day === day) ?? null;
}

/** Medium local date for a `YYYY-MM-DD` dashboard day; the raw value when it does not parse. */
export function formatSpendDay(day: string): string {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(day);
if (!match) return day;
const [year, month, date] = [Number(match[1]), Number(match[2]), Number(match[3])];
const parsed = new Date(year, month - 1, date);
if (parsed.getFullYear() !== year || parsed.getMonth() !== month - 1 || parsed.getDate() !== date) {
return day;
}
return parsed.toLocaleDateString(undefined, { dateStyle: "medium" });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { useState } from "react";
import { describe, expect, it } from "vitest";

import type { SpendContract, SpendModelRow } from "../../../types/bridge";
import type { LocaleKey } from "../../../i18n/keys";
import { buildSpendProviderGroups } from "../../../lib/usageSpendBreakdown";
import { UsageSpendProviderGroups } from "./UsageSpendProviderGroups";
import { UsageSpendTrendPanel } from "./UsageSpendTrendPanel";

const t = (key: LocaleKey) => key;

const models: SpendModelRow[] = Array.from({ length: 8 }, (_, index) => ({
model: `model-${index + 1}`,
costUsd: 8 - index,
inputTokens: 10,
outputTokens: 0,
cacheReadTokens: 0,
totalTokens: 10,
customPricing: false,
}));

const contract = {
providerId: "codex",
historyDays: 30,
knownCostUsd: 36,
historyCoverageEstablished: false,
priceCoverage: { priced: 8, unpriced: 0, unmetered: 0, estimated: 0 },
models,
daily: [
{ day: "2026-09-18", costUsd: 4, totalTokens: 40 },
{ day: "2026-09-19", costUsd: 2, totalTokens: 20 },
],
hourlyActivity: [{ weekday: 0, hour: 9, conversations: 3 }],
} as unknown as SpendContract;

describe("UsageSpendProviderGroups", () => {
const groups = buildSpendProviderGroups(contract, "Codex");

it("shows six models per provider, expands, and collapses the group", () => {
render(<UsageSpendProviderGroups groups={groups} historyDays={30} t={t} />);
expect(screen.getByText("Codex")).toBeTruthy();
expect(screen.getByText("UsageSpendPartialModelBreakdown")).toBeTruthy();
expect(screen.queryByText("model-7")).toBeNull();
expect(screen.getByText("model-6")).toBeTruthy();

fireEvent.click(screen.getByText(/UsageSpendShowAll/));
expect(screen.getByText("model-8")).toBeTruthy();
fireEvent.click(screen.getByText("UsageSpendShowLess"));
expect(screen.queryByText("model-8")).toBeNull();

const header = screen.getByRole("button", { expanded: true });
fireEvent.click(header);
expect(screen.queryByText("model-1")).toBeNull();
expect(header.getAttribute("aria-expanded")).toBe("false");
});

it("shows the empty state without groups", () => {
render(<UsageSpendProviderGroups groups={[]} historyDays={30} t={t} />);
expect(screen.getByText("UsageSpendNoModels")).toBeTruthy();
});
});

describe("UsageSpendTrendPanel", () => {
function Harness() {
const [day, setDay] = useState<string | null>(null);
return <UsageSpendTrendPanel contract={contract} selectedDay={day} onSelectDay={setDay} t={t} />;
}

it("selects a day from the daily chart and clears it with one control", () => {
render(<Harness />);
expect(screen.queryByTestId("usage-spend-selected-day")).toBeNull();
const bars = screen.getAllByRole("button", { pressed: false }).filter((node) => node.getAttribute("title"));
expect(bars).toHaveLength(2);

fireEvent.click(bars[0]);
expect(screen.getByTestId("usage-spend-selected-day")).toBeTruthy();
expect(screen.getByText("UsageSpendSelectedDayScope")).toBeTruthy();

fireEvent.click(screen.getByLabelText("UsageSpendClearSelectedDay"));
expect(screen.queryByTestId("usage-spend-selected-day")).toBeNull();
});

it("switches between the daily and hourly views with one selector", () => {
render(<Harness />);
expect(screen.getByText("UsageSpendDailySpend")).toBeTruthy();
fireEvent.click(screen.getByText("UsageSpendTrendHourly"));
expect(screen.getByLabelText("UsageSpendHourlyActivity")).toBeTruthy();
expect(screen.queryByText("UsageSpendDailySpend")).toBeNull();
});

it("drops the selector when only one view has data", () => {
const hourlyOnly = { ...contract, daily: [] } as unknown as SpendContract;
render(<UsageSpendTrendPanel contract={hourlyOnly} selectedDay={null} onSelectDay={() => {}} t={t} />);
expect(screen.queryByText("UsageSpendTrendDaily")).toBeNull();
expect(screen.getByLabelText("UsageSpendHourlyActivity")).toBeTruthy();
});
});
Loading