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
5 changes: 5 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/provider_detail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ pub struct ProviderDetail {
// Cost / pace.
pub cost: Option<CostSnapshotBridge>,
pub pace: Option<PaceSnapshot>,
/// Per-day OpenAI Admin API history for the Settings daily usage chart.
#[serde(skip_serializing_if = "Option::is_none")]
pub open_ai_api_usage: Option<OpenAiApiUsageSnapshot>,

// Error / state.
pub last_error: Option<String>,
Expand Down Expand Up @@ -107,6 +110,7 @@ pub(crate) fn build_provider_detail(
display_details: Vec::new(),
cost: None,
pace: None,
open_ai_api_usage: None,
last_error: None,
error_state: None,
dashboard_url: dashboard_url.clone(),
Expand Down Expand Up @@ -172,6 +176,7 @@ pub fn get_provider_detail(
detail.display_details = snapshot.display_details.clone();
detail.cost = snapshot.cost.clone();
detail.pace = snapshot.pace.clone();
detail.open_ai_api_usage = snapshot.open_ai_api_usage.clone();
}
detail.last_error = snapshot.error.clone();
detail.error_state = Some(snapshot.error_state);
Expand Down
17 changes: 17 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,23 @@ fn provider_detail_roundtrips_through_serde() {
assert!(json.contains("\"statusPageUrl\""));
}

#[test]
fn provider_detail_carries_openai_daily_usage_only_when_present() {
let (mut detail, _settings, _id) =
super::build_provider_detail("openaiapi").expect("known provider");
assert!(detail.open_ai_api_usage.is_none());
let json = serde_json::to_string(&detail).expect("serialize");
assert!(!json.contains("openAiApiUsage"));

detail.open_ai_api_usage = Some(super::OpenAiApiUsageSnapshot {
history_days: 30,
project_id: None,
daily: Vec::new(),
});
let json = serde_json::to_string(&detail).expect("serialize");
assert!(json.contains("\"openAiApiUsage\":{\"historyDays\":30"));
}

#[test]
fn usage_item_descriptors_keep_raw_ids_and_redact_titles() {
let metadata = instantiate_provider(ProviderId::Codex).metadata().clone();
Expand Down
13 changes: 10 additions & 3 deletions apps/desktop-tauri/src/components/MenuCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useLocale } from "../hooks/useLocale";
import { formatRelativeUpdated } from "../lib/relativeTime";
import type { LocaleKey } from "../i18n/keys";
import { providerSupportsChartData } from "../lib/providerCharts";
import { hideOpenAiApiProjectId } from "../lib/openAiApiIdentity";
import MenuCardDetails, { describeCard, type MetricEntry } from "./MenuCardDetails";
import CodexAccountsMenu from "./CodexAccountsMenu";
import ClaudeAccountsMenu from "./ClaudeAccountsMenu";
Expand Down Expand Up @@ -194,7 +195,13 @@ export default function MenuCard({
? maskEmail(provider.accountEmail)
: provider.accountEmail
: null;
const planName = !isWayfinder ? displayPlanName(provider.planName, t) : null;
const planName = !isWayfinder
? displayPlanName(provider.planName, t)
: null;
const displayedPlanName = hideOpenAiApiProjectId(
planName,
provider.providerId === "openaiapi" && hideEmail,
);

const metrics: MetricEntry[] = [
...(isWayfinder
Expand Down Expand Up @@ -285,8 +292,8 @@ export default function MenuCard({
? provider.updatedAt
: formatRelativeUpdated(Date.parse(provider.updatedAt), t)}
</span>
{planName && (
<span className="menu-card__plan-badge">{planName}</span>
{displayedPlanName && (
<span className="menu-card__plan-badge">{displayedPlanName}</span>
)}
</div>
)}
Expand Down
30 changes: 29 additions & 1 deletion apps/desktop-tauri/src/components/MenuCardDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
ProviderDisplayDetail,
PaceSnapshot,
ProviderInventoryItem,
OpenAiApiUsageSnapshot,
ProviderChartData,
ProviderLocalUsageSummary,
ProviderUsageSnapshot,
Expand All @@ -22,6 +23,7 @@ import type { LocaleKey } from "../i18n/keys";
import { paceCategory } from "../surfaces/tray/paceCategory";
import { SimpleBarChart, StackedBarChart } from "./MiniBarChart";
import { InventoryItemRow } from "./InventoryRows";
import { OpenAIApiUsageChart } from "./OpenAIApiUsageChart";
import { QuotaWindowHistory } from "./QuotaWindowHistory";
import { getPaceBudget, type PaceBudget } from "../lib/paceBudget";
import PaceDetailsChart from "./PaceDetailsChart";
Expand Down Expand Up @@ -439,6 +441,8 @@ export interface MenuCardPresence {
hasQuotaWindowHistory: boolean;
localUsage: ProviderChartData["localUsage"] | null;
wayfinderUsage: ProviderUsageSnapshot["wayfinderUsage"] | null;
/** Per-day OpenAI Admin API history; `openaiapi` only, and only with data. */
openAiApiUsage: OpenAiApiUsageSnapshot | null;
hasDetails: boolean;
}

Expand Down Expand Up @@ -477,6 +481,12 @@ export function describeCard(
const isWayfinder = provider.providerId === "wayfinder";
const localUsage = provider.error ? null : chartData?.localUsage ?? null;
const wayfinderUsage = isWayfinder ? provider.wayfinderUsage : null;
const openAiApiUsage =
provider.providerId === "openaiapi" &&
!provider.error &&
(provider.openAiApiUsage?.daily.length ?? 0) > 0
? (provider.openAiApiUsage ?? null)
: null;
const hasMetrics = visibleMetrics.length > 0;
const hasInventory = !provider.error && (provider.inventory?.length ?? 0) > 0;
const hasDisplayDetails = !provider.error && (provider.displayDetails?.length ?? 0) > 0;
Expand All @@ -496,7 +506,8 @@ export function describeCard(
hasPace ||
hasCharts ||
!!localUsage ||
!!wayfinderUsage) &&
!!wayfinderUsage ||
!!openAiApiUsage) &&
// Compact Overview suppresses supplemental sections entirely; a card
// whose only content would be suppressed renders header-only so no empty
// divider or details container appears.
Expand All @@ -514,6 +525,7 @@ export function describeCard(
hasQuotaWindowHistory,
localUsage,
wayfinderUsage,
openAiApiUsage,
hasDetails,
};
}
Expand Down Expand Up @@ -555,6 +567,7 @@ export default function MenuCardDetails({
hasQuotaWindowHistory,
localUsage,
wayfinderUsage,
openAiApiUsage,
} = presence;

return (
Expand Down Expand Up @@ -689,6 +702,21 @@ export default function MenuCardDetails({
</section>
)}

{!compactOverview && openAiApiUsage && (
<details className="menu-card__more menu-card__daily-usage" onToggle={onLayoutChange}>
<summary>{t("OpenAIChartTitle")}</summary>
<div className="menu-card__more-content">
{/* Tray cards open often; skip the bar entrance animation there. */}
<OpenAIApiUsageChart
usage={openAiApiUsage}
animations={false}
t={t}
onLayoutChange={onLayoutChange}
/>
</div>
</details>
)}

{!compactOverview && (localUsage || hasPace || hasCharts) && (
<details className="menu-card__more" onToggle={onLayoutChange}>
<summary>{t("PanelUsageDetails")}</summary>
Expand Down
187 changes: 187 additions & 0 deletions apps/desktop-tauri/src/components/MenuCardOpenAIUsage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { render, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";

const tauriMocks = vi.hoisted(() => ({
getProviderChartData: vi.fn(),
getDeepSeekPricingStatus: vi.fn(),
getLocaleStrings: vi.fn(),
setUiLanguage: vi.fn(),
claudeAccountsList: vi.fn(),
}));

const eventMocks = vi.hoisted(() => ({
listen: vi.fn(),
}));

vi.mock("../lib/tauri", async (importOriginal) => ({
...(await importOriginal<typeof import("../lib/tauri")>()),
...tauriMocks,
}));
vi.mock("@tauri-apps/api/event", () => eventMocks);

import { LocaleProvider } from "../i18n/LocaleProvider";
import { buildBundle } from "../test/localeHarness";
import type { OpenAiApiUsageSnapshot, ProviderUsageSnapshot } from "../types/bridge";
import MenuCard from "./MenuCard";
import { describeCard } from "./MenuCardDetails";

const START = 1_788_220_800;

function usage(days = 2): OpenAiApiUsageSnapshot {
return {
historyDays: 30,
projectId: null,
daily: Array.from({ length: days }, (_, i) => ({
startTime: START + i * 86_400,
endTime: START + (i + 1) * 86_400,
costUsd: 1 + i,
requests: 10,
inputTokens: 1000,
cachedInputTokens: 100,
outputTokens: 400,
totalTokens: 1400,
lineItems: [{ name: "Text tokens", costUsd: 1 + i }],
models: [],
})),
};
}

function rateWindow() {
return {
usedPercent: 10,
remainingPercent: 90,
windowMinutes: null,
resetsAt: null,
resetDescription: null,
isExhausted: false,
reservePercent: null,
reserveDescription: null,
reserveWillLastToReset: false,
reserveEtaSeconds: null,
};
}

function snapshot(
providerId: string,
openAiApiUsage: OpenAiApiUsageSnapshot | null,
error: string | null = null,
): ProviderUsageSnapshot {
return {
providerId,
displayName: providerId,
primary: rateWindow(),
selectedMetric: rateWindow(),
primaryLabel: "Session",
secondary: null,
modelSpecific: null,
tertiary: null,
extraRateWindows: [],
cost: null,
planName: null,
accountEmail: null,
sourceLabel: "api",
updatedAt: "2026-05-24T00:00:00Z",
error,
errorState: "unknown",
pace: null,
accountOrganization: null,
trayStatusLabel: null,
fetchDurationMs: null,
openAiApiUsage,
};
}

function renderCard(
snap: ProviderUsageSnapshot,
compactOverview = false,
hidePersonalInfo = false,
) {
return render(
<LocaleProvider>
<MenuCard
provider={snap}
display={{ hideEmail: hidePersonalInfo, resetTimeRelative: true, compactOverview }}
/>
</LocaleProvider>,
);
}

describe("MenuCard OpenAI daily usage section", () => {
beforeEach(() => {
vi.clearAllMocks();
tauriMocks.claudeAccountsList.mockResolvedValue([]);
tauriMocks.getLocaleStrings.mockResolvedValue(buildBundle({}));
tauriMocks.getDeepSeekPricingStatus.mockResolvedValue(null);
tauriMocks.getProviderChartData.mockResolvedValue({
providerId: "openaiapi",
costHistory: [],
creditsHistory: [],
usageBreakdown: [],
localUsage: null,
});
eventMocks.listen.mockResolvedValue(() => {});
});

it("renders a collapsed Daily usage section for openaiapi", async () => {
const { container } = renderCard(snapshot("openaiapi", usage()));
await waitFor(() => {
expect(container.querySelector(".menu-card__daily-usage")).not.toBeNull();
});
const section = container.querySelector(".menu-card__daily-usage") as HTMLDetailsElement;
expect(section.tagName).toBe("DETAILS");
expect(section.open).toBe(false);
expect(section.querySelector("summary")).toHaveTextContent("OpenAIChartTitle");
expect(section.querySelectorAll('[role="option"]')).toHaveLength(2);
});

it("does not show another provider's OpenAI payload (provider siloing)", async () => {
const { container } = renderCard(snapshot("claude", usage()));
await waitFor(() => {
expect(tauriMocks.getProviderChartData).toHaveBeenCalled();
});
expect(container.querySelector(".menu-card__daily-usage")).toBeNull();
expect(container.querySelector('[role="option"]')).toBeNull();
});

it("masks the Admin project id in the card identity when privacy is enabled", async () => {
const provider = snapshot("openaiapi", usage());
provider.planName = "Admin API: proj-private";
const { container } = renderCard(provider, false, true);
await waitFor(() => expect(container.querySelector(".menu-card__plan-badge")).not.toBeNull());
expect(container.querySelector(".menu-card__plan-badge")).toHaveTextContent("Admin API: ••••");
expect(container).not.toHaveTextContent("proj-private");
});

it("hides the section on error, in compact overview and with an empty window", async () => {
const errored = renderCard(snapshot("openaiapi", usage(), "boom"));
await waitFor(() => expect(tauriMocks.getLocaleStrings).toHaveBeenCalled());
expect(errored.container.querySelector(".menu-card__daily-usage")).toBeNull();
errored.unmount();

const compact = renderCard(snapshot("openaiapi", usage()), true);
expect(compact.container.querySelector(".menu-card__daily-usage")).toBeNull();
compact.unmount();

const empty = renderCard(snapshot("openaiapi", usage(0)));
expect(empty.container.querySelector(".menu-card__daily-usage")).toBeNull();
});
});

describe("describeCard OpenAI usage presence", () => {
it("is present only for openaiapi with days and no error", () => {
expect(describeCard(snapshot("openaiapi", usage()), null, []).openAiApiUsage).not.toBeNull();
expect(describeCard(snapshot("claude", usage()), null, []).openAiApiUsage).toBeNull();
expect(describeCard(snapshot("openaiapi", usage(0)), null, []).openAiApiUsage).toBeNull();
expect(describeCard(snapshot("openaiapi", null), null, []).openAiApiUsage).toBeNull();
expect(describeCard(snapshot("openaiapi", usage(), "boom"), null, []).openAiApiUsage).toBeNull();
});

it("makes a card that only has daily usage count as having details", () => {
expect(describeCard(snapshot("openaiapi", usage()), null, []).hasDetails).toBe(true);
expect(describeCard(snapshot("openaiapi", usage(0)), null, []).hasDetails).toBe(false);
});

it("keeps compact overview header-only when daily usage is the only content", () => {
expect(describeCard(snapshot("openaiapi", usage()), null, [], "detailed", true, true).hasDetails).toBe(false);
});
});
Loading