= {
"pws.metricTokens": "令牌",
"pws.usageUnavailable": "尚无用量记录。",
"pws.rateLimits": "速率限制",
+ "pws.quota.unavailableTitle": "配额不可用",
+ "pws.quota.signInRequired": "需要登录",
+ "pws.quota.loginNeedsRefresh": "需要刷新登录",
+ "pws.quota.upstreamUnavailable": "暂时不可用",
+ "pws.quota.retry": "重试",
"pws.quotaUnavailable": "此提供商暂无配额数据。",
"pws.planLimits": "套餐上限与本地观测",
"pws.reference.intro": "OpenCode Go 不提供实时剩余额度 API。这里将公开上限与通过此 CodexCommander 代理观测到的流量并列显示。",
diff --git a/gui/src/intl-formatters.ts b/gui/src/intl-formatters.ts
index e096931b79..d9d65e953a 100644
--- a/gui/src/intl-formatters.ts
+++ b/gui/src/intl-formatters.ts
@@ -68,3 +68,17 @@ export function formatEstimatedUsdValue(value: number, locale?: string): string
}).format(value);
return `~${formatted}`;
}
+
+/**
+ * Format a DEFINED zero USD estimate for display (locale-aware "$0.00"-equivalent, 2
+ * fraction digits). A defined zero must read as a zeroed amount — never "Unavailable"
+ * and never the ~$0.0000 estimate rendering.
+ */
+export function formatEstimatedUsdZero(locale?: string): string {
+ return cachedNumberFormat(locale, {
+ style: "currency",
+ currency: "USD",
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }).format(0);
+}
diff --git a/gui/src/pages/ClaudeCode.tsx b/gui/src/pages/ClaudeCode.tsx
index 92447180ef..643567755d 100644
--- a/gui/src/pages/ClaudeCode.tsx
+++ b/gui/src/pages/ClaudeCode.tsx
@@ -272,7 +272,7 @@ export default function ClaudeCode({ apiBase, active = true }: { apiBase: string
onClick={() => setSelectedSection(s.id)}
aria-current={selectedSection === s.id ? "true" : undefined}
>
- {s.label}
+ {s.label}
))}
diff --git a/gui/src/pages/Models.tsx b/gui/src/pages/Models.tsx
index 4d7d7e0621..dc6259012d 100644
--- a/gui/src/pages/Models.tsx
+++ b/gui/src/pages/Models.tsx
@@ -1640,7 +1640,7 @@ export default function Models({ apiBase }: { apiBase: string }) {
onClick={() => setSelectedProvider(null)}
aria-current={selectedProvider === null ? "true" : undefined}
>
- {t("models.workspace.allProviders")}
+ {t("models.workspace.allProviders")}
{t("models.active", { active: effectiveVisibleCount, total: models.length })}
{groups.map(group => {
@@ -1661,7 +1661,7 @@ export default function Models({ apiBase }: { apiBase: string }) {
onClick={() => setSelectedProvider(provider)}
aria-current={selectedProvider === provider ? "true" : undefined}
>
- {formatProviderDisplayName(provider, t)}
+ {formatProviderDisplayName(provider, t)}
{t("models.active", { active: activeCount, total: rows.length })}
);
diff --git a/gui/src/pages/Providers.tsx b/gui/src/pages/Providers.tsx
index b7f179d7bc..70b9aa7519 100644
--- a/gui/src/pages/Providers.tsx
+++ b/gui/src/pages/Providers.tsx
@@ -403,6 +403,9 @@ export default function Providers({ apiBase }: { apiBase: string }) {
usageTotals={data.usageTotals}
modelUsage={data.modelUsage}
quotaReport={data.quotaReport}
+ quotaUnavailableReason={data.quotaUnavailableReason}
+ onRetryQuota={data.onRetryQuota}
+ accountNeedsReauth={data.accountNeedsReauth}
availableModels={data.availableModels}
hasLiveModels={data.hasLiveModels}
selectedModels={data.selectedModels}
diff --git a/gui/src/pages/Usage.tsx b/gui/src/pages/Usage.tsx
index 345a419b7a..f0b2a538a6 100644
--- a/gui/src/pages/Usage.tsx
+++ b/gui/src/pages/Usage.tsx
@@ -1,95 +1,26 @@
-import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
+import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useI18n, type TFn, type Locale } from "../i18n/shared";
import { formatProviderDisplayName } from "../provider-icons";
import { formatTokens } from "../format-tokens";
-import { formatEstimatedUsdValue as formatUsdEstimate } from "../intl-formatters";
-import { readSessionListCache, writeSessionListCache } from "../session-list-cache";
+import { formatEstimatedUsdValue as formatUsdEstimate, formatEstimatedUsdZero } from "../intl-formatters";
import { EmptyState, Notice } from "../ui";
import { modelLabel } from "../model-display";
-import { useDataSurface } from "../data-surface";
+import { classifyDataSurface } from "../data-surface";
import { DataSurfaceSkeleton } from "../components/data-surface";
import { SectionTabs } from "../components/section-tabs";
import { sectionAnchorId } from "../section-anchors";
-
-type Range = "all" | "30d" | "7d";
-type UsageSurface = "all" | "codex" | "claude" | "grok";
-
-interface UsageSummaryTotals {
- requests: number;
- measuredRequests: number;
- reportedRequests: number;
- unreportedRequests: number;
- unsupportedRequests: number;
- estimatedRequests: number;
- inputTokens: number;
- outputTokens: number;
- cachedInputTokens: number;
- cacheReadInputTokens?: number;
- cacheCreationInputTokens?: number;
- reasoningOutputTokens: number;
- totalTokens: number;
- coverageRatio: number;
- estimatedCostUsd?: number;
- pricedRequests?: number;
- unpricedRequests?: number;
- unmeteredRequests?: number;
-}
-
-interface UsageDay {
- date: string;
- requests: number;
- measuredRequests: number;
- reportedRequests: number;
- totalTokens: number;
- models: UsageDayModel[];
-}
-
-interface UsageDayModel {
- model: string;
- provider: string;
- requests: number;
- totalTokens: number;
-}
-
-interface UsageModel {
- provider: string;
- model: string;
- resolvedModel?: string;
- requests: number;
- measuredRequests: number;
- reportedRequests: number;
- estimatedRequests: number;
- totalTokens: number;
- inputTokens: number;
- outputTokens: number;
- shareRatio: number;
-}
-
-interface UsageProvider {
- provider: string;
- requests: number;
- measuredRequests: number;
- reportedRequests: number;
- estimatedRequests: number;
- totalTokens: number;
- shareRatio: number;
-}
-
-interface UsageResponse {
- range: Range;
- surface: UsageSurface;
- since: number | null;
- generatedAt: number;
- summary: UsageSummaryTotals;
- days: UsageDay[];
- models: UsageModel[];
- providers: UsageProvider[];
- historyTruncated: boolean;
- truncatedPrefixBytes: number;
- entriesTruncated: boolean;
- entriesDropped: number;
- error?: string;
-}
+import { useUsageReport } from "../usage-report-store";
+import {
+ type UsageDay,
+ type UsageModel,
+ type UsageProvider,
+ type UsageRange,
+ type UsageReport,
+ type UsageSummaryTotals,
+ type UsageSurface,
+} from "../usage-report-validation";
+
+type Range = UsageRange;
function formatPct(ratio: number): string {
return `${Math.round(ratio * 100)}%`;
@@ -294,11 +225,21 @@ function UsageSummaryCards({
{t("usage.card.coverage")}
{formatPct(summary.coverageRatio)}
{t("usage.card.activeDays")}
{activeDays}
- {summary.estimatedCostUsd !== undefined && (
+ {summary.estimatedCostUsd === undefined ? (
+ // "Unavailable" is reachable ONLY through legacy (unvalidated) seeds: the DTO
+ // requires the cost fields and every fetched report is validated before caching,
+ // so a validated report can never take this branch.
+
+ {t("usage.cost.total")}
+ {t("usage.cost.unavailable")}
+
+ ) : (
{t("usage.cost.total")}
- {formatUsdEstimate(summary.estimatedCostUsd, locale)}
+ {summary.estimatedCostUsd === 0
+ ? formatEstimatedUsdZero(locale)
+ : formatUsdEstimate(summary.estimatedCostUsd, locale)}
{t("usage.cost.disclaimer")}
{((summary.unpricedRequests ?? 0) + (summary.unmeteredRequests ?? 0)) > 0 && (
@@ -658,7 +599,7 @@ function UsageWorkspaceBody({
locale,
t,
}: {
- data: UsageResponse | null;
+ data: UsageReport | null;
heatmap: ReturnType;
weekBars: UsageDay[];
activeDays: number;
@@ -731,50 +672,19 @@ function UsageWorkspaceBody({
);
}
-/** Held usage payloads so provider/surface tab switches skip a cold ~5s refetch. */
-const usageMemoryCache = new Map();
-
-function usageCacheKey(apiBase: string, range: Range, surface: UsageSurface): string {
- return `ccx.usage.v1:${apiBase}:${range}:${surface}`;
-}
-
-function readHeldUsage(apiBase: string, range: Range, surface: UsageSurface): UsageResponse | null {
- const key = usageCacheKey(apiBase, range, surface);
- return usageMemoryCache.get(key) ?? readSessionListCache(key);
-}
-
-function writeHeldUsage(apiBase: string, range: Range, surface: UsageSurface, value: UsageResponse) {
- const key = usageCacheKey(apiBase, range, surface);
- usageMemoryCache.set(key, value);
- writeSessionListCache(key, value);
-}
-
export default function Usage({ apiBase }: { apiBase: string }) {
const { t, locale } = useI18n();
const [range, setRange] = useState("30d");
const [surface, setSurface] = useState("all");
const [modelQuery, setModelQuery] = useState("");
- const loadUsage = useCallback(async (signal: AbortSignal): Promise => {
- const response = await fetch(`${apiBase}/api/usage?range=${range}&surface=${surface}`, { signal });
- if (!response.ok) throw new Error(`${response.status} ${response.statusText}`.trim());
- const next = await response.json() as UsageResponse;
- writeHeldUsage(apiBase, range, surface, next);
- return next;
- }, [apiBase, range, surface]);
-
- const resourceKey = usageCacheKey(apiBase, range, surface);
- const cached = readHeldUsage(apiBase, range, surface);
- // Range and surface identify different reports, so the key changes with both. That prevents
- // a force-loading dependency revalidation from ever showing a previous report as this one.
- const resource = useDataSurface(
- resourceKey,
- [apiBase, range, surface],
- loadUsage,
- { isEmpty: () => false, initialData: cached ?? undefined },
- );
- const { state } = resource;
- const data = state.data ?? cached ?? null;
+ // The usage-report domain store owns fetching, singleflight dedupe, and persistence.
+ // Range and surface identify different reports, so the key changes with both and a
+ // previous report is never shown as this one. The Dashboard's 30d/all selector shares
+ // this exact entry, collapsing what used to be three independent fetches into one.
+ const report = useUsageReport(apiBase, range, surface);
+ const state = classifyDataSurface(report, () => false, true);
+ const data = state.data ?? null;
const heatmap = useMemo(() => buildHeatmap(data?.days ?? []), [data?.days]);
const weekBars = useMemo(() => lastSevenDays(data?.days ?? []), [data?.days]);
@@ -808,8 +718,13 @@ export default function Usage({ apiBase }: { apiBase: string }) {
) : state.kind === "failed-cold" ? (
- {state.error instanceof Error ? `${t("usage.loadError")} ${state.error.message}` : t("usage.loadError")}{" "}
- resource.refresh()}>
+ {t("usage.loadError")}{" "}
+ report.refresh()}
+ >
{t("common.retry")}
diff --git a/gui/src/pages/dashboard-core-poll.ts b/gui/src/pages/dashboard-core-poll.ts
index d2314030f8..e4db864d1b 100644
--- a/gui/src/pages/dashboard-core-poll.ts
+++ b/gui/src/pages/dashboard-core-poll.ts
@@ -14,7 +14,6 @@ import {
type SettingsData,
type ShadowCallData,
type SidecarData,
- type UsageSummary30d,
} from "./dashboard-shared";
import { parseShadowCallData } from "./shadow-call-source";
@@ -134,14 +133,6 @@ export async function fetchDashboardModels(apiBase: string, signal: AbortSignal)
return requireJson(response);
}
-export async function fetchDashboardUsage(apiBase: string, signal: AbortSignal): Promise {
- const response = await fetch(`${apiBase}/api/usage?range=30d`, { signal });
- // Usage can be expensive on an older server. Keeping it in its own resource means
- // it cannot delay health/provider/settings commits, and a failed refresh retains
- // the last good usage snapshot.
- return requireJson(response);
-}
-
/** Web-search / vision sidecar + shadow-call — config reads, typically sub-10ms. */
export async function fetchDashboardSidecars(
apiBase: string,
diff --git a/gui/src/pages/dashboard-overview-head.tsx b/gui/src/pages/dashboard-overview-head.tsx
index 31e85eb1a1..9fa0f5b314 100644
--- a/gui/src/pages/dashboard-overview-head.tsx
+++ b/gui/src/pages/dashboard-overview-head.tsx
@@ -1,6 +1,7 @@
import { IconAlert, IconInfo } from "../icons";
import { type TKey, useT } from "../i18n/shared";
import { formatTokens } from "../format-tokens";
+import { formatEstimatedUsdValue, formatEstimatedUsdZero } from "../intl-formatters";
import { formatUptime } from "../formatUptime";
import type { useDashboardData } from "./use-dashboard-data";
@@ -81,6 +82,28 @@ export function DashboardOverviewHead({
: "\u00a0"}
+
+
{t("dash.cost30d")}
+
+ {usage30d && usage30d.summary.estimatedCostUsd !== undefined
+ ? usage30d.summary.estimatedCostUsd === 0
+ ? formatEstimatedUsdZero(locale)
+ : formatEstimatedUsdValue(usage30d.summary.estimatedCostUsd, locale)
+ : "—"}
+
+
+ {usage30d && usage30d.summary.estimatedCostUsd !== undefined
+ && (usage30d.summary.pricedRequests
+ + usage30d.summary.unpricedRequests
+ + usage30d.summary.unmeteredRequests) > 0
+ ? t("dash.cost30dCoverage", {
+ priced: usage30d.summary.pricedRequests,
+ unpriced: usage30d.summary.unpricedRequests,
+ unmetered: usage30d.summary.unmeteredRequests,
+ })
+ : "\u00a0"}
+
+
diff --git a/gui/src/pages/dashboard-overview-panels.tsx b/gui/src/pages/dashboard-overview-panels.tsx
index 8009d04423..0518c2030a 100644
--- a/gui/src/pages/dashboard-overview-panels.tsx
+++ b/gui/src/pages/dashboard-overview-panels.tsx
@@ -1,5 +1,6 @@
import MemoryObservabilityCard from "../components/MemoryObservabilityCard";
import type { useDashboardData } from "./use-dashboard-data";
+import { DashboardPlanQuotaSection } from "./dashboard-plan-quota-section";
import {
DashboardEffortCapPanel,
DashboardInjectionPanel,
@@ -18,6 +19,7 @@ export function DashboardOverviewPanels(props: Dash) {
+
>
);
diff --git a/gui/src/pages/dashboard-plan-quota-section.tsx b/gui/src/pages/dashboard-plan-quota-section.tsx
new file mode 100644
index 0000000000..33a7bd92bd
--- /dev/null
+++ b/gui/src/pages/dashboard-plan-quota-section.tsx
@@ -0,0 +1,162 @@
+/**
+ * Dashboard "Plan & quota" section.
+ *
+ * Fed by the shared provider-quota store (keyed by apiBase) — the same entry the
+ * Providers workspace shell selects, so both surfaces dedupe into one fetch and share
+ * last-known-good data. Reuses the workspace parsing (accountQuotaFromReport /
+ * capacityAggregationFromReport / referenceQuotaFromReport) and the
+ * ProviderCapacityQuota presentation so both surfaces share semantics: per-provider
+ * plan, 5h/week/month windows, and observed reference spend vs published caps —
+ * always labeled provider-reported / estimates, never billed spend.
+ *
+ * Styling reuses existing dashboard tokens/classes (panel, dash-sidecar-grid,
+ * pws-capacity-*, muted/text-caption/mono); no new stylesheet is introduced.
+ */
+import { useEffect } from "react";
+import { useI18n, useT, type TFn, type TKey } from "../i18n/shared";
+import { useProviderQuota } from "../provider-quota-store";
+import { formatProviderDisplayName } from "../provider-icons";
+import { quotaUnavailableReasonKey } from "../quota-unavailable";
+import { providerRouteHash } from "../provider-route";
+import {
+ formatQuotaSourceLabel,
+ referenceQuotaFromReport,
+ type ProviderQuotaReferenceWindowView,
+ type ProviderQuotaReportView,
+} from "../provider-workspace/report";
+import { formatRequestCount, formatTokenCount } from "../provider-workspace/usage";
+import { ProviderCapacityQuota } from "../components/provider-workspace/ProviderCapacityQuota";
+
+const REFERENCE_WINDOW_KEYS: Record = {
+ five_hour: "pws.reference.fiveHour",
+ weekly: "pws.reference.weekly",
+ monthly: "pws.reference.monthly",
+};
+
+function referenceObservedLabel(
+ window: ProviderQuotaReferenceWindowView,
+ locale: string,
+ t: TFn,
+): string {
+ if (window.observedRequests === 0) return t("pws.reference.noTraffic");
+ if (window.observedSpendUsd !== undefined) {
+ const amount = new Intl.NumberFormat(locale, {
+ style: "currency",
+ currency: "USD",
+ minimumFractionDigits: 2,
+ maximumFractionDigits: window.observedSpendUsd < 0.01 ? 4 : 2,
+ }).format(window.observedSpendUsd);
+ return t("pws.reference.spendObserved", { amount });
+ }
+ if (window.observedTokens > 0) {
+ return t("pws.reference.tokensObserved", { tokens: formatTokenCount(window.observedTokens, locale) });
+ }
+ return t("pws.reference.requestsObserved", { requests: formatRequestCount(window.observedRequests, locale) });
+}
+
+function PlanQuotaReference({ report, locale }: { report: ProviderQuotaReportView; locale: string }) {
+ const t = useT();
+ const reference = referenceQuotaFromReport(report);
+ if (!reference) return null;
+ const money = (amount: number) => new Intl.NumberFormat(locale, {
+ style: "currency",
+ currency: "USD",
+ maximumFractionDigits: amount >= 10 ? 0 : 2,
+ }).format(amount);
+ return (
+
+
{t("dash.planQuota.referenceIntro")}
+ {reference.windows.map(window => (
+
+ {t(REFERENCE_WINDOW_KEYS[window.id])}
+
+ {t("pws.reference.publishedCap", { amount: money(window.publishedLimitUsd) })}
+
+ {referenceObservedLabel(window, locale, t)}
+
+ ))}
+
+ );
+}
+
+export function DashboardPlanQuotaSection({ apiBase }: { apiBase: string }) {
+ const t = useT();
+ const { locale } = useI18n();
+ const quota = useProviderQuota(apiBase);
+ // First subscriber on this surface: fetch (singleflight dedupes against the
+ // Providers workspace shell; a rehydrated session seed quiet-revalidates).
+ const ensure = quota.ensure;
+ useEffect(() => {
+ ensure();
+ }, [ensure]);
+
+ const entries = Object.entries(quota.reports);
+ const unavailable = quota.unavailableProviders;
+ const hasRetryableUnavailable = unavailable.some(
+ ({ reason }) => reason !== "reauth_required",
+ );
+ return (
+
+
+
{t("dash.planQuota.title")}
+ {t("dash.planQuota.hint")}
+
+ {entries.length === 0 && unavailable.length === 0 ? (
+
+ {quota.loading ? t("dash.planQuota.loading") : t("dash.planQuota.empty")}
+
+ ) : (
+ entries.length > 0 && (
+
+ {entries.map(([provider, report]) => (
+
+
+ {formatProviderDisplayName(provider, t)}
+ {report.source?.trim() && (
+ {formatQuotaSourceLabel(report.source)}
+ )}
+
+
+
+
+ ))}
+
+ )
+ )}
+ {unavailable.length > 0 && (
+
+
{t("dash.planQuota.unavailable")}
+ {unavailable.map(({ provider, reason }) => {
+ const display = formatProviderDisplayName(provider, t);
+ return (
+
+
+ {display}
+ {" — "}
+ {t(quotaUnavailableReasonKey(reason))}
+
+ {reason === "reauth_required" && (
+
+ ·
+
+ {t("dash.planQuota.manageProvider", { provider: display })}
+
+
+ )}
+
+ );
+ })}
+ {hasRetryableUnavailable && (
+
quota.refresh({ force: true })}>
+ {t("dash.planQuota.retry")}
+
+ )}
+
+ )}
+ {t("dash.planQuota.disclaimer")}
+
+ );
+}
diff --git a/gui/src/pages/dashboard-shared.ts b/gui/src/pages/dashboard-shared.ts
index bfa44cbc21..b77f883767 100644
--- a/gui/src/pages/dashboard-shared.ts
+++ b/gui/src/pages/dashboard-shared.ts
@@ -50,7 +50,17 @@ export interface SidecarPatch {
vision?: { backend?: SidecarBackend | null; model?: string };
}
export type { ShadowCallData } from "./shadow-call-source";
-export interface UsageSummary30d { summary: { requests: number; totalTokens: number; coverageRatio: number } }
+export interface UsageSummary30d {
+ summary: {
+ requests: number;
+ totalTokens: number;
+ coverageRatio: number;
+ estimatedCostUsd: number;
+ pricedRequests: number;
+ unpricedRequests: number;
+ unmeteredRequests: number;
+ };
+}
export interface SyncResult {
ok: boolean;
added: number;
diff --git a/gui/src/pages/integrations/OpenCodeIntegrationPage.tsx b/gui/src/pages/integrations/OpenCodeIntegrationPage.tsx
index e784e640da..8290b28ea0 100644
--- a/gui/src/pages/integrations/OpenCodeIntegrationPage.tsx
+++ b/gui/src/pages/integrations/OpenCodeIntegrationPage.tsx
@@ -252,7 +252,7 @@ export default function OpenCodeIntegrationPage({
{t("integrations.destination")}
- {homeDisplayPath(integration.targetPath)}
+ {homeDisplayPath(integration.targetPath)}
{t("integrations.models")}
diff --git a/gui/src/pages/use-dashboard-data.ts b/gui/src/pages/use-dashboard-data.ts
index 8203403218..941b4fd261 100644
--- a/gui/src/pages/use-dashboard-data.ts
+++ b/gui/src/pages/use-dashboard-data.ts
@@ -16,12 +16,12 @@ import {
fetchDashboardOverview,
fetchDashboardSettings,
fetchDashboardSidecars,
- fetchDashboardUsage,
fetchProjectConfigDiagnostics,
fetchStartupHealth,
normalizeInjectionSelection,
type DashboardEpochRefs,
} from "./dashboard-core-poll";
+import { useUsageReport } from "../usage-report-store";
import {
type DashboardSection,
type HealthData,
@@ -43,7 +43,6 @@ import {
const CONTROLS_CACHE_PREFIX = "ccx.dash.controls.v1:";
const OVERVIEW_CACHE_PREFIX = "ccx.dash.overview.v1:";
-const USAGE_CACHE_PREFIX = "ccx.dash.usage30d.v1:";
const STARTUP_CACHE_PREFIX = "ccx.dash.startup.v1:";
const MA_MODE_CACHE_PREFIX = "ccx.dash.maMode.v1:";
@@ -78,10 +77,6 @@ export function useDashboardData(apiBase: string) {
() => readSessionListCache
(`${OVERVIEW_CACHE_PREFIX}${apiBase}`),
[apiBase],
);
- const cachedUsage = useMemo(
- () => readSessionListCache(`${USAGE_CACHE_PREFIX}${apiBase}`),
- [apiBase],
- );
const cachedStartup = useMemo(() => {
const cached = readSessionListCache(`${STARTUP_CACHE_PREFIX}${apiBase}`);
return cached === "error" ? null : cached;
@@ -97,7 +92,6 @@ export function useDashboardData(apiBase: string) {
const [settings, setSettings] = useState(() => cachedControls?.settings ?? null);
const [sidecar, setSidecar] = useState(() => cachedControls?.sidecar ?? null);
const [shadowCall, setShadowCall] = useState(() => cachedControls?.shadowCall ?? null);
- const [usage30d, setUsage30d] = useState(() => cachedUsage);
const [sidecarSaving, setSidecarSaving] = useState(false);
const [shadowCallSaving, setShadowCallSaving] = useState(false);
const [modelsLoading, setModelsLoading] = useState(false);
@@ -221,12 +215,41 @@ export function useDashboardData(apiBase: string) {
{ pollMs: 5000, enabled: overviewReady },
);
- const usagePoll = useKeyedClientResource(
- `dashboard-usage:${apiBase}`,
- [apiBase],
- (signal) => fetchDashboardUsage(apiBase, signal),
- { pollMs: 60_000, enabled: overviewReady },
- );
+ // Usage is owned by the shared usage-report store (key `${apiBase}:30d:all`). The Usage
+ // page selects the same entry, so both surfaces dedupe into one in-flight fetch. Keeping
+ // it out of the client-resource polls means usage can never delay health/settings commits.
+ const usageReport = useUsageReport(apiBase, "30d", "all");
+ const usage30d = useMemo(() => {
+ const report = usageReport.data;
+ if (!report) return null;
+ return {
+ summary: {
+ requests: report.summary.requests,
+ totalTokens: report.summary.totalTokens,
+ coverageRatio: report.summary.coverageRatio,
+ estimatedCostUsd: report.summary.estimatedCostUsd,
+ pricedRequests: report.summary.pricedRequests,
+ unpricedRequests: report.summary.unpricedRequests,
+ unmeteredRequests: report.summary.unmeteredRequests,
+ },
+ };
+ }, [usageReport.data]);
+
+ // The dashboard used to poll /api/usage?range=30d every 60s. Usage is now owned by the
+ // usage-report store, so while the Dashboard is mounted we quiet-revalidate the 30d/all
+ // entry on the same cadence. replace:false keeps singleflight semantics: a poll skips
+ // when another fetch is in flight and never aborts it, and quiet refreshes retain the
+ // last-good data (no skeleton flash).
+ const refreshUsage30d = usageReport.refresh;
+ useEffect(() => {
+ const timer = window.setInterval(() => {
+ // Same pause-when-hidden behavior the old client-resource poll had: a background
+ // tab has nobody reading the paint, so skip the quiet revalidation until visible.
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
+ refreshUsage30d({ replace: false });
+ }, 60_000);
+ return () => window.clearInterval(timer);
+ }, [refreshUsage30d]);
const diagnosticsPoll = useKeyedClientResource(
`dashboard-diagnostics:${apiBase}`,
@@ -336,13 +359,6 @@ export function useDashboardData(apiBase: string) {
}
}, [settingsPoll.data, apiBase]);
- useEffect(() => {
- if (usagePoll.data !== undefined) {
- setUsage30d(usagePoll.data);
- writeSessionListCache(`${USAGE_CACHE_PREFIX}${apiBase}`, usagePoll.data);
- }
- }, [usagePoll.data, apiBase]);
-
useEffect(() => {
if (diagnosticsPoll.data) setProjectConfigWarnings(diagnosticsPoll.data);
}, [diagnosticsPoll.data]);
@@ -544,7 +560,7 @@ export function useDashboardData(apiBase: string) {
modelQuery, setModelQuery,
expandedProviders, setExpandedProviders,
health, startupHealth, providers, models, settings, sidecar, shadowCall, usage30d,
- usageLoading: usagePoll.loading && !usage30d,
+ usageLoading: usageReport.loading && !usage30d,
healthLoading: overviewPoll.loading && !health,
sidecarSaving, shadowCallSaving, modelsLoading, settingsSaving, syncing,
maMode, maModeResolved, maBusy, setMaHelpOpen, maHelpOpen,
diff --git a/gui/src/provider-quota-store.ts b/gui/src/provider-quota-store.ts
new file mode 100644
index 0000000000..5bb108e845
--- /dev/null
+++ b/gui/src/provider-quota-store.ts
@@ -0,0 +1,615 @@
+/**
+ * Domain store for GET /api/provider-quotas.
+ *
+ * Keyed by apiBase and shared by the Providers workspace shell and the Dashboard
+ * "Plan & quota" section, so both surfaces dedupe into one in-flight fetch and
+ * share the same last-known-good data. The shell's quotaRefreshEpoch /
+ * quotaForceRefresh semantics map to a refresh({ force }) action (force adds the
+ * server-side ?refresh=1 TTL bypass).
+ *
+ * Privacy invariant: only quota reports (provider/label/source/quota/updatedAt +
+ * aggregation) and a timestamp are persisted — never account emails or ids. The
+ * wire shape from src/providers/quota.ts already avoids identities; this store
+ * does not add any.
+ */
+
+import { create } from "zustand";
+import { persist, type PersistStorage, type StorageValue } from "zustand/middleware";
+import { useCallback, useMemo } from "react";
+import {
+ capacityAggregationFromReport,
+ type ProviderQuotaReportView,
+} from "./provider-workspace/report";
+
+export const PROVIDER_QUOTA_STORAGE_NAME = "ccx.provider-quotas.v1";
+/** Same freshness bound the workspace shell applied to its session cache. */
+const QUOTA_REPORT_MAX_AGE_MS = 30 * 60_000;
+
+export interface ProviderQuotaData {
+ reports: Record;
+ authAttention: Record;
+ /**
+ * In-memory quota availability per provider (status/reason/checkedAt), projected
+ * from the wire. Deliberately NOT persisted: the persisted slice stays reports +
+ * timestamp, so a hostile availability row can never reach sessionStorage.
+ */
+ availability: Record;
+ updatedAt?: number;
+}
+
+export interface ProviderQuotaAvailability {
+ status: string;
+ reason?: string;
+ checkedAt: number;
+}
+
+export interface ProviderQuotaEntry extends ProviderQuotaData {
+ loading: boolean;
+ refreshing: boolean;
+ hasSucceeded: boolean;
+ lastAttemptOk: boolean;
+ error?: unknown;
+ /** True for a rehydrated seed: the first subscriber must quiet-revalidate. */
+ seedNeedsRevalidate: boolean;
+}
+
+export type ProviderQuotaResource = ProviderQuotaData & {
+ key: string;
+ error: unknown;
+ loading: boolean;
+ refreshing: boolean;
+ hasSucceeded: boolean;
+ lastAttemptOk: boolean;
+ /** Providers whose quota is unavailable and have no report, sorted by name. */
+ unavailableProviders: Array<{ provider: string; reason?: string }>;
+ ensure: (opts?: { force?: boolean }) => void;
+ refresh: (opts?: { force?: boolean }) => void;
+};
+
+/** The persisted slice — quota reports + timestamp only. */
+interface PersistedQuotaSlice {
+ entries: Record; updatedAt: number }>;
+}
+
+interface ProviderQuotaStoreState {
+ entries: Record;
+ /** One in-flight controller per apiBase; singleflight + cancellation. */
+ inflight: Record;
+ ensure: (apiBase: string, opts?: { force?: boolean }) => void;
+ refresh: (apiBase: string, opts?: { force?: boolean }) => void;
+ clearForTests: () => void;
+}
+
+/**
+ * Per-row ingest validation: keep a report row when it is fresh (updatedAt within
+ * QUOTA_REPORT_MAX_AGE_MS), has a quota object, and carries no malformed optional
+ * fields. Deep shape validation is left to the consumers' parsers
+ * (capacityAggregationFromReport / accountQuotaFromReport / referenceQuotaFromReport),
+ * which return null for unusable payloads.
+ */
+function quotaReportFromRow(value: unknown, now: number): ProviderQuotaReportView | null {
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
+ const row = value as Record;
+ if (typeof row.updatedAt !== "number" || !Number.isFinite(row.updatedAt)) return null;
+ if (now - row.updatedAt >= QUOTA_REPORT_MAX_AGE_MS) return null;
+ const quota = projectQuota(row.quota);
+ if (!quota) return null;
+ if (row.label !== undefined && typeof row.label !== "string") return null;
+ if (row.source !== undefined && typeof row.source !== "string") return null;
+ const aggregation = "aggregation" in row ? projectAggregation(row.aggregation) : null;
+ return {
+ ...(typeof row.label === "string" ? { label: row.label } : {}),
+ ...(typeof row.source === "string" ? { source: row.source } : {}),
+ updatedAt: row.updatedAt,
+ quota,
+ // ProviderQuotaReportView declares aggregation as required; consumers treat
+ // undefined as "no capacity aggregation" (capacityAggregationFromReport returns
+ // null), so reference-window-only reports stay representable.
+ aggregation: aggregation ?? undefined,
+ };
+}
+
+/** Strict display filter matching the workspace shell's prior session-cache behavior. */
+export function freshQuotaReport(value: unknown, now: number): ProviderQuotaReportView | null {
+ const report = quotaReportFromRow(value, now);
+ if (!report || report.aggregation === undefined) return null;
+ return capacityAggregationFromReport(report) ? report : null;
+}
+
+export function freshQuotaReportRecord(value: unknown, now = Date.now()): Record | null {
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
+ const out: Record = {};
+ for (const [provider, raw] of Object.entries(value)) {
+ const report = freshQuotaReport(raw, now);
+ if (provider.trim() && report) out[provider] = report;
+ }
+ return out;
+}
+
+function freshQuotaReportsFromResponse(value: unknown, now = Date.now()): Record {
+ if (!Array.isArray(value)) return {};
+ const out: Record = {};
+ for (const raw of value) {
+ const row = raw as Record | null;
+ const provider = row?.provider;
+ const report = row ? quotaReportFromRow(row, now) : null;
+ if (typeof provider === "string" && provider.trim() && report) out[provider] = report;
+ }
+ return out;
+}
+
+/**
+ * The quota endpoint may discover an auth problem after the account list was read.
+ * Project only fixed, privacy-safe reason codes so an open surface cannot keep
+ * saying Connected until its next account refresh.
+ */
+export function quotaAuthAttentionFromResponse(value: unknown): Record {
+ if (!Array.isArray(value)) return {};
+ const out: Record = {};
+ for (const raw of value) {
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
+ const row = raw as Record;
+ if (typeof row.provider !== "string" || !row.provider.trim()) continue;
+ if (row.reason === "reauth_required" || row.reason === "local_cli_refresh_required") {
+ out[row.provider] = true;
+ }
+ }
+ return out;
+}
+
+/** Known, privacy-safe reason codes the UI maps to copy. Anything else is dropped. */
+const KNOWN_QUOTA_UNAVAILABLE_REASONS = new Set([
+ "reauth_required",
+ "local_cli_refresh_required",
+ "upstream_unavailable",
+]);
+/** Wire availability statuses; anything else is dropped at ingest. */
+const KNOWN_QUOTA_AVAILABILITY_STATUSES = new Set(["available", "stale", "unavailable"]);
+
+/**
+ * Project availability rows onto { provider, status, reason?, checkedAt } only —
+ * provider + status + reason + checkedAt, nothing else (no identities, no raw
+ * provider errors). Mirrors the Mac app's ProviderQuotaAvailability decoding.
+ */
+export function quotaAvailabilityFromResponse(value: unknown): Record {
+ if (!Array.isArray(value)) return {};
+ const out: Record = {};
+ for (const raw of value) {
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
+ const row = raw as Record;
+ if (typeof row.provider !== "string" || !row.provider.trim()) continue;
+ if (typeof row.status !== "string" || !KNOWN_QUOTA_AVAILABILITY_STATUSES.has(row.status)) continue;
+ const checkedAt = finiteNumber(row.checkedAt) ?? Date.now();
+ const reason =
+ typeof row.reason === "string" && KNOWN_QUOTA_UNAVAILABLE_REASONS.has(row.reason)
+ ? row.reason
+ : undefined;
+ out[row.provider] = {
+ status: row.status,
+ ...(reason ? { reason } : {}),
+ checkedAt,
+ };
+ }
+ return out;
+}
+
+/**
+ * Providers with a non-available quota status AND no report entry, sorted by provider
+ * name. A provider with a report (even a stale last-known-good one) is not listed.
+ */
+export function unavailableQuotaProviders(
+ availability: Record,
+ reports: Record,
+): Array<{ provider: string; reason?: string }> {
+ return Object.entries(availability)
+ .filter(([provider, row]) => row.status !== "available" && !(provider in reports))
+ .map(([provider, row]) => ({ provider, ...(row.reason ? { reason: row.reason } : {}) }))
+ .sort((a, b) => a.provider.localeCompare(b.provider));
+}
+
+const sessionStorageLazy: PersistStorage = {
+ getItem: (name) => {
+ try {
+ const raw = sessionStorage.getItem(name);
+ if (!raw) return null;
+ return JSON.parse(raw) as StorageValue;
+ } catch {
+ return null;
+ }
+ },
+ setItem: (name, value) => {
+ try {
+ sessionStorage.setItem(name, JSON.stringify(value));
+ } catch {
+ /* private mode / no sessionStorage in this runtime */
+ }
+ },
+ removeItem: (name) => {
+ try {
+ sessionStorage.removeItem(name);
+ } catch {
+ /* ignore */
+ }
+ },
+};
+
+function emptyEntry(): ProviderQuotaEntry {
+ return {
+ reports: {},
+ authAttention: {},
+ availability: {},
+ loading: false,
+ refreshing: false,
+ hasSucceeded: false,
+ lastAttemptOk: false,
+ seedNeedsRevalidate: false,
+ };
+}
+
+function finiteNumber(value: unknown): number | undefined {
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
+}
+
+/**
+ * Project a ProviderQuota row onto the known keys only (percentages, resets, windows,
+ * referenceWindows, observedLimitEvent, updatedAt). A hostile or legacy server that
+ * stashes identity fields inside `quota` can never get them persisted: anything not on
+ * this allowlist is dropped at ingest.
+ */
+function projectQuota(value: unknown): Record | null {
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
+ const row = value as Record;
+ const out: Record = {};
+ for (const key of ["fiveHourPercent", "fiveHourResetAt", "weeklyPercent", "weeklyResetAt", "monthlyPercent", "monthlyResetAt"] as const) {
+ const n = finiteNumber(row[key]);
+ if (n !== undefined) out[key] = n;
+ }
+ const updatedAt = finiteNumber(row.updatedAt);
+ if (updatedAt !== undefined) out.updatedAt = updatedAt;
+ if (Array.isArray(row.customWindows)) {
+ const windows = row.customWindows.flatMap(raw => {
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return [];
+ const window = raw as Record;
+ const label = typeof window.label === "string" && window.label.trim() ? window.label : null;
+ const percent = finiteNumber(window.percent);
+ if (!label || percent === undefined) return [];
+ const resetAt = finiteNumber(window.resetAt);
+ return [{ label, percent, ...(resetAt !== undefined ? { resetAt } : {}) }];
+ });
+ if (windows.length > 0) out.customWindows = windows;
+ }
+ if (Array.isArray(row.referenceWindows)) {
+ const windows = row.referenceWindows.flatMap(raw => {
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return [];
+ const window = raw as Record;
+ const id = window.id;
+ const coverage = window.coverage;
+ const label = typeof window.label === "string" && window.label.trim() ? window.label : null;
+ const windowSeconds = finiteNumber(window.windowSeconds);
+ const publishedLimitUsd = finiteNumber(window.publishedLimitUsd);
+ const observedTokens = finiteNumber(window.observedTokens);
+ const observedRequests = finiteNumber(window.observedRequests);
+ const pricedRequests = finiteNumber(window.pricedRequests);
+ const unpricedRequests = finiteNumber(window.unpricedRequests);
+ const unmeasuredRequests = finiteNumber(window.unmeasuredRequests);
+ const validId = id === "five_hour" || id === "weekly" || id === "monthly";
+ const validCoverage = coverage === "none" || coverage === "complete" || coverage === "partial" || coverage === "unpriced";
+ if (!validId || !validCoverage || !label || windowSeconds === undefined || publishedLimitUsd === undefined
+ || observedTokens === undefined || observedRequests === undefined
+ || pricedRequests === undefined || unpricedRequests === undefined || unmeasuredRequests === undefined) return [];
+ const observedSpendUsd = finiteNumber(window.observedSpendUsd);
+ return [{
+ id,
+ label,
+ windowSeconds,
+ publishedLimitUsd,
+ observedTokens,
+ observedRequests,
+ pricedRequests,
+ unpricedRequests,
+ unmeasuredRequests,
+ coverage,
+ ...(observedSpendUsd !== undefined ? { observedSpendUsd } : {}),
+ }];
+ });
+ if (windows.length > 0) out.referenceWindows = windows;
+ }
+ if (row.observedLimitEvent && typeof row.observedLimitEvent === "object" && !Array.isArray(row.observedLimitEvent)) {
+ const event = row.observedLimitEvent as Record;
+ const limitName = event.limitName;
+ const observedAt = finiteNumber(event.observedAt);
+ if ((limitName === "5 hour" || limitName === "weekly" || limitName === "monthly") && observedAt !== undefined) {
+ const resetAt = finiteNumber(event.resetAt);
+ out.observedLimitEvent = {
+ limitName,
+ observedAt,
+ ...(resetAt !== undefined ? { resetAt } : {}),
+ };
+ }
+ }
+ return Object.keys(out).length > 0 ? out : null;
+}
+
+function projectCapacityWindow(value: unknown): Record | null {
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
+ const row = value as Record;
+ const usedPercent = finiteNumber(row.usedPercent);
+ if (usedPercent === undefined) return null;
+ const out: Record = { usedPercent };
+ for (const key of ["includedAccounts", "excludedAccounts", "nextRecoveryAt", "nextRecoveryPercent"] as const) {
+ const n = finiteNumber(row[key]);
+ if (n !== undefined) out[key] = n;
+ }
+ if (typeof row.incomplete === "boolean") out.incomplete = row.incomplete;
+ const updatedAt = finiteNumber(row.updatedAt);
+ if (updatedAt !== undefined) out.updatedAt = updatedAt;
+ return out;
+}
+
+/**
+ * Project a CodexCapacityAggregation onto its known keys so identity-like fields cannot
+ * ride along into the persisted slice (aggregation carries currentAccount with
+ * plan/quota only; anything else is dropped).
+ */
+function projectAggregation(value: unknown): Record | null {
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
+ const row = value as Record;
+ if (row.kind !== "capacity-weighted-v1" || row.scope !== "routable-known") return null;
+ const presentation = row.presentation;
+ if (presentation !== "aggregate" && presentation !== "effective-account-fallback" && presentation !== "coverage-only") return null;
+ const out: Record = { kind: row.kind, scope: row.scope, presentation };
+ for (const key of ["excludedAccounts", "unknownPlanAccounts", "partialWindowAccounts", "includedAccounts"] as const) {
+ const n = finiteNumber(row[key]);
+ if (n !== undefined) out[key] = n;
+ }
+ if (typeof row.incomplete === "boolean") out.incomplete = row.incomplete;
+ for (const key of ["fiveHour", "weekly", "monthly"] as const) {
+ const window = projectCapacityWindow(row[key]);
+ if (window) out[key] = window;
+ }
+ if (Array.isArray(row.customWindows)) {
+ const windows = row.customWindows.flatMap(raw => {
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return [];
+ const entry = raw as Record;
+ const label = typeof entry.label === "string" && entry.label.trim() ? entry.label : null;
+ const window = projectCapacityWindow(entry);
+ return label && window ? [{ label, ...window }] : [];
+ });
+ if (windows.length > 0) out.customWindows = windows;
+ }
+ if (row.currentAccount && typeof row.currentAccount === "object" && !Array.isArray(row.currentAccount)) {
+ const account = row.currentAccount as Record;
+ const projected: Record = {};
+ if (typeof account.plan === "string" || account.plan === null) projected.plan = account.plan;
+ if (typeof account.isMain === "boolean") projected.isMain = account.isMain;
+ if (account.quota === null) {
+ projected.quota = null;
+ } else {
+ const quota = projectQuota(account.quota);
+ if (quota) projected.quota = quota;
+ }
+ if (Object.keys(projected).length > 0) out.currentAccount = projected;
+ }
+ return out;
+}
+
+function fetchQuotas(
+ set: (partial: Partial | ((state: ProviderQuotaStoreState) => Partial)) => void,
+ get: () => ProviderQuotaStoreState,
+ apiBase: string,
+ options?: { force?: boolean; replace?: boolean },
+): void {
+ const key = apiBase;
+ const inflight = get().inflight[key];
+ // Singleflight: concurrent subscribers dedupe onto the in-flight request.
+ if (inflight && options?.replace !== true) return;
+ inflight?.abort();
+ const controller = new AbortController();
+ set(state => {
+ const existing = state.entries[key];
+ return {
+ inflight: { ...state.inflight, [key]: controller },
+ entries: {
+ ...state.entries,
+ [key]: {
+ ...(existing ?? emptyEntry()),
+ loading: existing?.reports === undefined || Object.keys(existing.reports).length === 0
+ ? true
+ : false,
+ refreshing: true,
+ error: undefined,
+ seedNeedsRevalidate: false,
+ },
+ },
+ };
+ });
+
+ void (async () => {
+ try {
+ const response = await fetch(`${apiBase}/api/provider-quotas${options?.force ? "?refresh=1" : ""}`, {
+ signal: controller.signal,
+ });
+ if (!response.ok) throw new Error(`${response.status} ${response.statusText}`.trim());
+ const data = (await response.json()) as { reports?: unknown; availability?: unknown } | null;
+ if (get().inflight[key] !== controller) return;
+ const reports = freshQuotaReportsFromResponse(data?.reports);
+ const authAttention = quotaAuthAttentionFromResponse(data?.availability);
+ const availability = quotaAvailabilityFromResponse(data?.availability);
+ set(state => ({
+ inflight: { ...state.inflight, [key]: null },
+ entries: {
+ ...state.entries,
+ [key]: {
+ reports,
+ authAttention,
+ availability,
+ updatedAt: Date.now(),
+ error: undefined,
+ loading: false,
+ refreshing: false,
+ hasSucceeded: true,
+ lastAttemptOk: true,
+ seedNeedsRevalidate: false,
+ },
+ },
+ }));
+ } catch (error) {
+ if (controller.signal.aborted || get().inflight[key] !== controller) return;
+ set(state => ({
+ inflight: { ...state.inflight, [key]: null },
+ entries: {
+ ...state.entries,
+ [key]: {
+ ...(state.entries[key] ?? emptyEntry()),
+ error: error === undefined ? new Error("provider quota load failed") : error,
+ loading: false,
+ refreshing: false,
+ lastAttemptOk: false,
+ },
+ },
+ }));
+ }
+ })();
+}
+
+export const useProviderQuotaStore = create()(
+ persist(
+ (set, get) => ({
+ entries: {},
+ inflight: {},
+ ensure: (apiBase, opts) => {
+ const key = apiBase;
+ // A forced ensure always re-reads (and replaces any in-flight request); it is the
+ // singleflight-visible variant of refresh for callers that only have ensure.
+ if (opts?.force === true) {
+ fetchQuotas(set, get, apiBase, { force: true, replace: true });
+ return;
+ }
+ if (get().inflight[key]) return;
+ const entry = get().entries[key];
+ // Cold start, rehydrated seed, or a previously cold-failed key: fetch
+ // (quiet for a seed, cold otherwise). Singleflight dedupes subscribers.
+ if (!entry || entry.seedNeedsRevalidate || !entry.hasSucceeded) {
+ fetchQuotas(set, get, apiBase, {});
+ return;
+ }
+ // Healthy cached data — nothing to do.
+ },
+ refresh: (apiBase, opts) => {
+ fetchQuotas(set, get, apiBase, { ...opts, replace: true });
+ },
+ clearForTests: () => {
+ for (const controller of Object.values(get().inflight)) controller?.abort();
+ set({ entries: {}, inflight: {} });
+ },
+ }),
+ {
+ name: PROVIDER_QUOTA_STORAGE_NAME,
+ storage: sessionStorageLazy,
+ partialize: (state): PersistedQuotaSlice => ({
+ entries: Object.fromEntries(
+ Object.entries(state.entries)
+ .filter(([, entry]) => entry.updatedAt !== undefined && Object.keys(entry.reports).length > 0)
+ .map(([key, entry]) => [
+ key,
+ { reports: entry.reports, updatedAt: entry.updatedAt as number },
+ ]),
+ ),
+ }),
+ merge: (persisted, current) => {
+ const persistedEntries =
+ (persisted as Partial | undefined)?.entries ?? {};
+ const now = Date.now();
+ const entries: Record = { ...current.entries };
+ for (const [key, value] of Object.entries(persistedEntries)) {
+ if (!value || !value.reports) continue;
+ const freshReports = freshQuotaReportRecord(value.reports, now) ?? {};
+ if (Object.keys(freshReports).length === 0) continue;
+ entries[key] = {
+ ...(entries[key] ?? emptyEntry()),
+ reports: freshReports,
+ updatedAt: value.updatedAt,
+ seedNeedsRevalidate: true,
+ };
+ }
+ return { ...current, entries };
+ },
+ },
+ ),
+);
+
+/**
+ * Select the provider-quota entry for an apiBase. The caller decides when to fetch:
+ * `ensure` starts a cold/quiet fetch (singleflight), `refresh` always re-fetches and
+ * replaces any in-flight request (used for quotaRefreshEpoch/quotaForceRefresh).
+ */
+export function useProviderQuota(apiBase: string): ProviderQuotaResource {
+ const key = apiBase;
+ const entry = useProviderQuotaStore(state => state.entries[key]);
+ const ensureAction = useProviderQuotaStore(state => state.ensure);
+ const refreshAction = useProviderQuotaStore(state => state.refresh);
+ // Stable fallbacks so derived memos don't recompute while the entry is absent
+ // (a fresh `{}` per render would churn unavailableProviders and trip lint).
+ const availability = entry?.availability ?? EMPTY_AVAILABILITY;
+ const reports = entry?.reports ?? EMPTY_REPORTS;
+ const unavailableProviders = useMemo(
+ () => unavailableQuotaProviders(availability, reports),
+ [availability, reports],
+ );
+
+ const ensure = useCallback(
+ (opts?: { force?: boolean }) => ensureAction(key, opts),
+ [key, ensureAction],
+ );
+ const refresh = useCallback(
+ (opts?: { force?: boolean }) => refreshAction(key, opts),
+ [key, refreshAction],
+ );
+
+ return {
+ key,
+ reports,
+ authAttention: entry?.authAttention ?? {},
+ availability,
+ unavailableProviders,
+ updatedAt: entry?.updatedAt,
+ error: entry?.error,
+ loading: entry?.loading ?? false,
+ refreshing: entry?.refreshing ?? false,
+ hasSucceeded: entry?.hasSucceeded ?? false,
+ lastAttemptOk: entry?.lastAttemptOk ?? false,
+ ensure,
+ refresh,
+ };
+}
+
+const EMPTY_AVAILABILITY: Record = {};
+const EMPTY_REPORTS: Record = {};
+
+/** Test-only: drop every entry and abort in-flight work so suite order cannot reuse data. */
+export function clearProviderQuotaStoresForTests(): void {
+ useProviderQuotaStore.getState().clearForTests();
+}
+
+/** Test-only: seed an entry as if it were rehydrated from sessionStorage. */
+export function seedProviderQuotaForTests(
+ apiBase: string,
+ data: { reports: Record; updatedAt: number },
+): void {
+ useProviderQuotaStore.setState(state => ({
+ entries: {
+ ...state.entries,
+ [apiBase]: {
+ ...emptyEntry(),
+ reports: data.reports,
+ updatedAt: data.updatedAt,
+ seedNeedsRevalidate: true,
+ },
+ },
+ }));
+}
+
+/** Test-only: re-run persist rehydration against the current sessionStorage. */
+export function rehydrateProviderQuotaForTests(): void {
+ void useProviderQuotaStore.persist.rehydrate();
+}
diff --git a/gui/src/quota-unavailable.ts b/gui/src/quota-unavailable.ts
new file mode 100644
index 0000000000..afad8d1675
--- /dev/null
+++ b/gui/src/quota-unavailable.ts
@@ -0,0 +1,12 @@
+import type { TKey } from "./i18n/shared";
+
+/**
+ * Map a quota-unavailable reason code to its localized copy, matching the Mac app's
+ * ProviderListView summary. Any unknown or missing reason falls back to the generic
+ * "Temporarily unavailable" line (raw reason strings never reach the DOM).
+ */
+export function quotaUnavailableReasonKey(reason: string | undefined): TKey {
+ if (reason === "reauth_required") return "pws.quota.signInRequired";
+ if (reason === "local_cli_refresh_required") return "pws.quota.loginNeedsRefresh";
+ return "pws.quota.upstreamUnavailable";
+}
diff --git a/gui/src/styles-apikeys-workspace.css b/gui/src/styles-apikeys-workspace.css
index f59b3c60d6..2f02725bb8 100644
--- a/gui/src/styles-apikeys-workspace.css
+++ b/gui/src/styles-apikeys-workspace.css
@@ -161,8 +161,8 @@
}
.awi-overview .api-panel {
- padding: 18px;
- gap: 10px;
+ padding: var(--space-4);
+ gap: var(--space-2);
}
.awi-overview .api-auth-list {
diff --git a/gui/src/styles-claudecode-workspace.css b/gui/src/styles-claudecode-workspace.css
index ebf34279b7..af41082377 100644
--- a/gui/src/styles-claudecode-workspace.css
+++ b/gui/src/styles-claudecode-workspace.css
@@ -29,7 +29,7 @@
padding: 0 16px 12px;
margin-top: -4px;
font-size: var(--text-caption);
- line-height: 1.45;
+ line-height: 1.45; /* deliberate: between --leading-ui and --leading-body */
color: var(--muted);
border-bottom: 1px solid var(--border-soft);
}
@@ -43,7 +43,7 @@
color: var(--faint);
text-transform: uppercase;
letter-spacing: 0.04em;
- font-size: 10.5px;
+ font-size: 10.5px; /* deliberate: micro-caption tuning */
}
/* ── Rail ─────────────────────────────────────────────── */
@@ -183,7 +183,7 @@
align-items: baseline;
gap: 6px;
margin: 0 0 5px;
- font-size: 10.5px;
+ font-size: 10.5px; /* deliberate: micro-caption tuning */
font-weight: var(--weight-semibold);
letter-spacing: 0.04em;
text-transform: uppercase;
@@ -223,7 +223,7 @@
.claude-aliases-chip-id {
font-family: var(--font-code);
- font-size: 11px;
+ font-size: var(--text-caption);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
diff --git a/gui/src/styles-combos-workspace.css b/gui/src/styles-combos-workspace.css
index f73aa3b8a0..f19a55af25 100644
--- a/gui/src/styles-combos-workspace.css
+++ b/gui/src/styles-combos-workspace.css
@@ -87,7 +87,7 @@
.combos-workspace-rail-group-head {
display: flex;
align-items: center;
- gap: 7px;
+ gap: var(--space-1-5);
padding: 8px 16px 6px;
color: var(--muted);
font-size: var(--text-caption);
@@ -232,7 +232,7 @@
font-size: var(--text-control);
font-weight: 500;
color: var(--muted);
- padding: 8px 12px;
+ padding: var(--space-2) var(--space-3);
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
@@ -295,8 +295,8 @@
.cwi-count-pill {
display: inline-flex;
align-items: baseline;
- gap: 6px;
- padding: 8px 12px;
+ gap: var(--space-1-5);
+ padding: var(--space-2) var(--space-3);
border: 1px solid var(--border-soft);
border-radius: var(--radius);
background: var(--raised);
@@ -508,7 +508,7 @@
.pwi-json-unsaved-title,
.pwi-remove-confirm-title { margin: 0 0 8px; font-size: var(--text-subtitle); font-weight: 650; color: var(--text); }
.pwi-json-unsaved-desc,
-.pwi-remove-confirm-desc { margin: 0 0 18px; font-size: var(--text-control); line-height: 1.45; }
+.pwi-remove-confirm-desc { margin: 0 0 var(--space-4); font-size: var(--text-control); line-height: 1.45; /* deliberate: between --leading-ui and --leading-body */ }
.pwi-json-unsaved-actions,
.pwi-remove-confirm-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
.pwi-remove-confirm-danger { background: var(--red) !important; color: #fff !important; border-color: transparent !important; }
diff --git a/gui/src/styles-dashboard-workspace.css b/gui/src/styles-dashboard-workspace.css
index a7a990d320..ce4511ed3f 100644
--- a/gui/src/styles-dashboard-workspace.css
+++ b/gui/src/styles-dashboard-workspace.css
@@ -97,6 +97,40 @@
min-width: 0;
}
+/* Quota-unavailable strip: one full-width row under the Plan & quota grid, never a
+ grid card — providers with no report entry get a reason line; reauth rows deep-link
+ to the provider (Manage), non-auth rows share one Retry quota check. */
+.dash-plan-quota-unavailable {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--space-2) var(--space-3);
+ margin-top: var(--space-3);
+ padding: var(--space-2) var(--space-3);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ background: var(--surface);
+}
+
+.dash-plan-quota-unavailable-label {
+ font-weight: var(--weight-semibold);
+}
+
+.dash-plan-quota-unavailable-item {
+ display: inline-flex;
+ align-items: baseline;
+ flex-wrap: wrap;
+ gap: var(--space-2);
+ color: var(--muted);
+}
+
+.dash-plan-quota-unavailable-manage {
+ display: inline-flex;
+ align-items: baseline;
+ gap: var(--space-1);
+ white-space: nowrap;
+}
+
.dash-sidecar-card__row .font-semibold {
min-width: 0;
}
diff --git a/gui/src/styles-integrations.css b/gui/src/styles-integrations.css
index 6fc1d8762c..984d6016f2 100644
--- a/gui/src/styles-integrations.css
+++ b/gui/src/styles-integrations.css
@@ -8,7 +8,7 @@
.integration-badge--danger { background: var(--red-soft); color: var(--red); }
.integration-badge--danger-outline { background: transparent; color: var(--red); border-color: var(--red); }
-.integration-summary { display: flex; flex-wrap: wrap; align-items: center; gap: 18px; padding: 14px 16px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--raised); margin-bottom: 14px; }
+.integration-summary { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4); padding: var(--space-3) var(--space-4); border: 1px solid var(--border); border-radius: var(--radius); background: var(--raised); margin-bottom: 14px; }
.integration-summary-cell { display: flex; flex-direction: column; gap: 2px; }
.integration-summary-label { font-size: var(--text-caption); color: var(--muted); }
.integration-summary .btn { margin-left: auto; }
@@ -91,8 +91,8 @@
}
.client-apps-page-head h2 { margin: 0; font-size: var(--text-display); letter-spacing: -0.025em; }
.client-apps-page-head p { margin: 5px 0 0; color: var(--muted); font-size: var(--text-body); }
-.client-apps-page-actions { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
-.client-apps-page-actions .btn { display: inline-flex; align-items: center; gap: 7px; }
+.client-apps-page-actions { display: flex; align-items: center; gap: var(--space-2); flex: 0 0 auto; }
+.client-apps-page-actions .btn { display: inline-flex; align-items: center; gap: var(--space-1-5); }
.client-apps-page-actions svg { width: 15px; height: 15px; }
.client-apps-flow {
@@ -134,15 +134,15 @@
.client-apps-flow-stage small { color: var(--green); font-size: var(--text-caption); font-weight: var(--weight-semibold); }
.client-apps-flow-stage em { flex: 1 0 100%; color: var(--muted); font-size: var(--text-caption); font-style: normal; line-height: var(--leading-body); }
.client-apps-flow-arrow { color: var(--faint); text-align: center; font-size: 19px; }
-.client-apps-flow-chips { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 12px; min-width: 0; }
+.client-apps-flow-chips { display: flex; flex-wrap: wrap; gap: var(--space-1-5); margin-top: var(--space-3); min-width: 0; }
.client-apps-flow-chips--providers { grid-column: 1; }
.client-apps-flow-chips--clients { grid-column: 5; }
.client-apps-flow-chip {
display: inline-flex;
align-items: center;
- gap: 6px;
- min-height: 28px;
- padding: 4px 9px;
+ gap: var(--space-1-5);
+ min-height: var(--control-sm);
+ padding: var(--space-1) var(--space-2);
border: 1px solid var(--border);
border-radius: var(--radius-pill);
background: var(--raised);
@@ -228,10 +228,10 @@ button.client-apps-flow-chip:hover { border-color: var(--accent-ring); color: va
.client-apps-mark svg { width: 19px; height: 19px; color: var(--muted); }
.client-apps-row-copy { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.client-apps-row-title { font-size: var(--text-control); font-weight: var(--weight-semibold); }
-.client-apps-row-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; color: var(--muted); font-size: var(--text-caption); }
+.client-apps-row-meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1-5) var(--space-2); color: var(--muted); font-size: var(--text-caption); }
.client-apps-row-meta .badge { font-size: var(--text-caption); }
.client-apps-row-actions { display: flex; align-items: center; justify-content: flex-end; }
-.client-apps-row-actions .btn { min-height: 34px; white-space: nowrap; }
+.client-apps-row-actions .btn { min-height: var(--control-md); white-space: nowrap; }
.client-apps-row > .notice,
.client-apps-row-refusal { grid-column: 1 / -1; }
.client-apps-row-refusal { margin: 0; color: var(--red); font-size: var(--text-caption); line-height: var(--leading-body); }
@@ -253,7 +253,7 @@ button.client-apps-flow-chip:hover { border-color: var(--accent-ring); color: va
.client-apps-available-main > span:last-child { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.client-apps-available-main strong { font-size: var(--text-control); }
.client-apps-available-main small { color: var(--muted); font-size: var(--text-caption); }
-.client-apps-available-row .btn { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; min-height: 34px; padding-inline: 10px; }
+.client-apps-available-row .btn { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; min-height: var(--control-md); padding-inline: var(--space-2); }
.client-apps-available-row .btn svg { width: 13px; height: 13px; }
.client-apps-detail { position: sticky; top: 20px; min-width: 0; overflow: hidden; }
diff --git a/gui/src/styles-subagents-workspace.css b/gui/src/styles-subagents-workspace.css
index 97a53f5b39..660879f879 100644
--- a/gui/src/styles-subagents-workspace.css
+++ b/gui/src/styles-subagents-workspace.css
@@ -256,13 +256,13 @@
.swi-roster-name,
.swi-library-name {
- overflow: hidden;
color: var(--text);
font-family: var(--mono);
font-size: var(--text-control);
line-height: var(--leading-ui);
- text-overflow: ellipsis;
- white-space: nowrap;
+ /* Model IDs must never be clipped mid-token: let them wrap anywhere instead. */
+ white-space: normal;
+ overflow-wrap: anywhere;
}
.swi-model-chips {
@@ -420,10 +420,9 @@
.swi-roster-matrix-name {
min-width: 0;
- overflow: hidden;
font-family: var(--mono);
- text-overflow: ellipsis;
- white-space: nowrap;
+ white-space: normal;
+ overflow-wrap: anywhere;
}
.swi-roster-matrix-surface {
@@ -480,10 +479,10 @@
.swi-library-filters {
display: flex;
align-items: center;
+ /* Wrap instead of clipping: chips must never be cut off at the right edge. */
+ flex-wrap: wrap;
gap: var(--space-2);
padding: 0 var(--space-4) var(--space-3);
- overflow-x: auto;
- scrollbar-width: thin;
}
.swi-filter {
@@ -606,9 +605,10 @@
.swi-policy-grid {
display: grid;
- grid-template-columns: minmax(150px, 0.8fr) minmax(220px, 1.2fr) minmax(200px, 1.05fr) minmax(150px, 0.72fr) auto;
+ grid-template-columns: minmax(170px, 0.8fr) minmax(220px, 1.2fr) minmax(200px, 1.05fr) minmax(170px, 0.72fr) auto;
gap: var(--space-4);
- align-items: end;
+ /* Top-align so stacked labels/helpers never form the 30px "label staircase". */
+ align-items: start;
padding: var(--space-4);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
@@ -628,7 +628,7 @@
}
.swi-policy-guidance-field {
- grid-column: span 2;
+ grid-column: span 4;
}
.swi-policy-toggle-row {
@@ -649,14 +649,16 @@
.swi-policy-help {
display: block;
color: var(--muted);
- font-size: var(--text-micro);
+ font-size: var(--text-label);
line-height: var(--leading-body);
}
.swi-policy-save-cell {
display: flex;
align-items: flex-end;
- min-height: calc(var(--control-lg) + 34px);
+ justify-content: flex-end;
+ grid-column: 5;
+ min-height: 0;
}
.swi-policy-save-cell .btn {
@@ -830,11 +832,10 @@
.swi-fallback-name {
min-width: 0;
- overflow: hidden;
font-family: var(--mono);
font-size: var(--text-label);
- text-overflow: ellipsis;
- white-space: nowrap;
+ white-space: normal;
+ overflow-wrap: anywhere;
}
.swi-fallback-actions {
diff --git a/gui/src/styles.css b/gui/src/styles.css
index 9453a1d8d6..9b74f009e8 100644
--- a/gui/src/styles.css
+++ b/gui/src/styles.css
@@ -245,8 +245,8 @@ input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); }
.sidebar {
position: sticky; top: 0; align-self: start; height: 100dvh;
z-index: var(--z-overlay);
- display: flex; flex-direction: column; gap: 4px;
- padding: 18px 14px;
+ display: flex; flex-direction: column; gap: var(--space-1);
+ padding: var(--space-4) var(--space-3);
border-right: 1px solid var(--border);
background: var(--glass-rail);
backdrop-filter: var(--glass-blur);
@@ -393,7 +393,7 @@ input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); }
/* Page-level underline tabs (Logs & Debug / Dashboard). Distinct from pill .segmented filters. */
/* Let the row take the height it needs — no horizontal scrollbar on a short tab strip. */
.page-tabs { display: flex; flex-wrap: wrap; gap: 2px; border-bottom: 1px solid var(--border); margin: 2px 0 14px; overflow: visible; }
-.page-tab { flex: 0 0 auto; white-space: nowrap; appearance: none; background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; padding: 8px 12px; color: var(--muted); cursor: pointer; font: inherit; font-size: var(--text-control); }
+.page-tab { flex: 0 0 auto; white-space: nowrap; appearance: none; background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; padding: var(--space-2) var(--space-3); color: var(--muted); cursor: pointer; font: inherit; font-size: var(--text-control); }
.page-tab:hover { color: var(--text); }
.page-tab--active { color: var(--text); border-bottom-color: var(--accent); font-weight: var(--weight-semibold); }
.page-tab:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: -2px; }
@@ -551,7 +551,7 @@ input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); }
/* ---- buttons ---- */
.btn {
- display: inline-flex; align-items: center; justify-content: center; gap: 7px;
+ display: inline-flex; align-items: center; justify-content: center; gap: var(--space-1-5);
padding: 8px 16px; border-radius: var(--radius-pill);
font: inherit; font-size: var(--text-control); font-weight: var(--weight-medium); line-height: var(--leading-ui); cursor: pointer;
border: 1px solid transparent; transition: background var(--motion-fast), border-color var(--motion-fast), opacity var(--motion-fast); white-space: nowrap;
@@ -589,7 +589,7 @@ a.btn, a.btn:hover { text-decoration: none; }
/* ---- cards / panels ---- */
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); min-width: 0; }
-.panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px; }
+.panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-4); }
/* Flat accent tint + token border (FE-GRADIENT-02: no gradient wash on opaque functional panels) */
.panel-accent { border-color: color-mix(in srgb, var(--accent) 28%, var(--border)); background: color-mix(in srgb, var(--accent) 5%, var(--surface)); }
.api-panel { display: flex; flex-direction: column; gap: 10px; overflow: hidden; }
@@ -905,7 +905,7 @@ a.btn, a.btn:hover { text-decoration: none; }
/* ---- tables ---- */
.tbl { width: 100%; border-collapse: collapse; font-size: var(--text-control); }
-.tbl thead th { text-align: left; padding: 9px 12px; color: var(--muted); font-weight: var(--weight-medium); font-size: var(--text-label); border-bottom: 1px solid var(--border); }
+.tbl thead th { text-align: left; padding: var(--space-2) var(--space-3); color: var(--muted); font-weight: var(--weight-medium); font-size: var(--text-label); border-bottom: 1px solid var(--border); }
.tbl tbody td { padding: 10px 12px; border-bottom: 1px solid var(--border-soft); }
.tbl tbody tr:last-child td { border-bottom: none; }
.tbl tbody tr:hover td { background: var(--hover); }
@@ -962,6 +962,7 @@ select.input { appearance: none; }
backdrop-filter: blur(12px) saturate(1.3);
-webkit-backdrop-filter: blur(12px) saturate(1.3);
border: 1px solid var(--border);
+ /* deliberate: pill elevation distinct from --shadow-sm */
box-shadow: 0 2px 8px rgb(0 0 0 / 0.06);
color: var(--text); font: inherit; font-size: var(--text-control); line-height: var(--leading-ui); cursor: pointer;
transition: border-color var(--motion-fast), box-shadow var(--motion-fast);
@@ -985,6 +986,14 @@ select.input { appearance: none; }
}
.select-trigger:hover:not(:disabled) { border-color: var(--faint); box-shadow: 0 2px 12px rgb(0 0 0 / 0.10); }
.select-trigger:disabled { opacity: 0.5; cursor: default; }
+/* Long select labels must ellipsize inside the pill instead of spilling past it
+ (matches the sidecar pattern in styles-dashboard-workspace.css). */
+.select-trigger > span {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
.select-dropdown {
position: absolute; top: calc(100% + 4px); left: 0; z-index: var(--z-popover);
min-width: 100%; max-height: 280px; overflow-y: auto;
@@ -1012,7 +1021,7 @@ select.input { appearance: none; }
.select-dropdown-beside { top: auto; bottom: 0; left: calc(100% + 12px); right: auto; min-width: 10rem; max-height: min(60vh, 20rem); overflow-y: auto; }
.select-option {
display: block; width: 100%; text-align: left;
- padding: 7px 12px; border: none; border-radius: var(--radius-sm);
+ padding: var(--space-1-5) var(--space-3); border: none; border-radius: var(--radius-sm);
background: transparent; color: var(--text); font: inherit; font-size: var(--text-control); line-height: var(--leading-ui);
cursor: pointer; transition: background var(--motion-fast), box-shadow var(--motion-fast);
white-space: nowrap;
@@ -1076,7 +1085,7 @@ select.input { appearance: none; }
.faint { color: var(--faint); }
.row { display: flex; align-items: center; gap: 10px; }
.spread { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
-.setting-hint { font-size: var(--text-control); line-height: var(--leading-body); margin-top: 3px; max-width: var(--prose-measure); }
+.setting-hint { font-size: var(--text-control); line-height: var(--leading-body); margin-top: var(--space-1); max-width: var(--prose-measure); }
.stack { display: flex; flex-direction: column; }
.chip { font-family: var(--font-code); font-size: var(--text-label); line-height: var(--leading-ui); background: var(--raised); border: 1px solid var(--border); padding: 1px 7px; border-radius: var(--radius-xs); color: var(--text); }
@@ -1084,7 +1093,7 @@ select.input { appearance: none; }
.empty svg { width: 30px; height: 30px; color: var(--faint); margin-bottom: 12px; }
.empty .title { color: var(--text); font-weight: var(--weight-semibold); margin-bottom: 6px; }
-.notice { font-size: var(--text-control); line-height: var(--leading-body); padding: 9px 12px; border-radius: var(--radius-sm); margin-bottom: 14px; display: flex; align-items: center; gap: 8px; max-width: var(--prose-measure); }
+.notice { font-size: var(--text-control); line-height: var(--leading-body); padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); margin-bottom: var(--space-3); display: flex; align-items: center; gap: var(--space-2); max-width: var(--prose-measure); }
.notice svg { width: 15px; height: 15px; flex-shrink: 0; }
/* Tone pairs: ink is a shade of the tint (not gray-on-color), AA on both themes. */
.notice-ok {
@@ -1660,7 +1669,7 @@ dialog.modal-overlay::backdrop {
.startup-hero-icon svg { width: 34px; height: 34px; }
.startup-hero-copy p { margin: 0; color: var(--muted); font-size: 17px; line-height: var(--leading-body); max-width: 66ch; }
.startup-primary, .startup-advanced, .startup-actions { margin-bottom: 38px; }
-.startup-primary { padding: 18px 31px; }
+.startup-primary { padding: var(--space-4) var(--space-8); }
.startup-primary-row { display: flex; align-items: center; gap: 28px; min-height: 112px; padding: 12px 0; }
.startup-primary-row + .startup-primary-row { border-top: 1px solid var(--border-soft); }
.startup-primary-row-icon { width: 58px; height: 58px; border-radius: var(--radius); display: grid; place-items: center; background: var(--raised); flex: 0 0 auto; }
@@ -1708,7 +1717,7 @@ dialog.modal-overlay::backdrop {
.startup-hero-copy p { font-size: var(--text-body); }
.startup-primary, .startup-advanced, .startup-actions { margin-bottom: 20px; }
.startup-primary { padding: 10px 20px; }
- .startup-primary-row { min-height: 0; gap: 14px; padding: 18px 0; }
+ .startup-primary-row { min-height: 0; gap: var(--space-3); padding: var(--space-4) 0; }
.startup-primary-row-icon { width: 46px; height: 46px; }
.startup-primary-row-label, .startup-primary-row-value { font-size: var(--text-body); }
.startup-primary-row-actions .btn { min-height: 40px; padding: 8px 16px; font-size: var(--text-control); }
@@ -1720,7 +1729,7 @@ dialog.modal-overlay::backdrop {
.startup-detail-row { align-items: flex-start; }
.startup-detail-row > .startup-detail-actions { flex-direction: column; align-items: flex-end; }
}
-.modal-desc { font-size: var(--text-control); line-height: var(--leading-body); color: var(--muted); margin-bottom: 14px; max-width: var(--prose-measure); }
+.modal-desc { font-size: var(--text-control); line-height: var(--leading-body); color: var(--muted); margin-bottom: var(--space-3); max-width: var(--prose-measure); }
.modal-actions { display: flex; gap: 8px; margin-top: 16px; }
.modal-actions .btn { flex: 1; }
@@ -1881,7 +1890,7 @@ table.logs-table {
overflow: auto; max-height: 40vh; white-space: pre-wrap; word-break: break-all; margin: 0;
}
-.setup-guide { font-size: var(--text-control); line-height: var(--leading-body); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 8px 12px; margin-bottom: 4px; }
+.setup-guide { font-size: var(--text-control); line-height: var(--leading-body); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--space-2) var(--space-3); margin-bottom: var(--space-1); }
.setup-guide summary { cursor: pointer; color: var(--accent-hover); font-weight: var(--weight-medium); }
.setup-guide summary:hover { text-decoration: underline; }
.setup-guide a { color: var(--accent-hover); }
@@ -2254,7 +2263,7 @@ button.prov-account-row.active { cursor: default; }
margin-bottom: 12px;
}
.claude-tabs button {
- min-width: 88px; min-height: 34px; padding: 6px 14px; border: 0; border-radius: var(--radius-pill);
+ min-width: 88px; min-height: var(--control-md); padding: var(--space-1-5) var(--space-3); border: 0; border-radius: var(--radius-pill);
background: transparent; color: var(--muted); font: inherit; font-size: 13px; font-weight: 550; cursor: pointer;
line-height: 1.2;
}
@@ -2305,12 +2314,12 @@ button.prov-account-row.active { cursor: default; }
}
.claude-lane-default {
min-width: 0; overflow: hidden; color: var(--faint);
- font-family: var(--font-code); font-size: 11px; font-weight: var(--weight-semibold);
+ font-family: var(--font-code); font-size: var(--text-caption); font-weight: var(--weight-semibold);
text-overflow: ellipsis; white-space: nowrap;
}
.claude-default-radio { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); font-size: 11.5px; cursor: pointer; }
.claude-default-needed { color: var(--amber); font-size: 11.5px; font-weight: 550; }
-.claude-effective-default { display: inline-block; margin-top: 6px; color: var(--amber); font-size: 11px; font-weight: 550; }
+.claude-effective-default { display: inline-block; margin-top: var(--space-1-5); color: var(--amber); font-size: var(--text-caption); font-weight: 550; /* deliberate: variable weight between 500/600 */ }
.claude-lane-models { display: flex; flex-direction: column; gap: 9px; min-height: 104px; padding: 10px; }
/* Lane density controls: a filter that only appears once a lane is long enough to need one,
and a pager for the tail. Same idiom as the Models page so the two dense surfaces match. */
@@ -2333,7 +2342,7 @@ button.prov-account-row.active { cursor: default; }
.grok-model-row:first-child { border-top: 0; }
.grok-model-names { display: flex; flex: 1; min-width: 0; flex-direction: column; gap: 1px; }
.grok-model-names strong { overflow: hidden; color: var(--text); font-size: 12.5px; text-overflow: ellipsis; white-space: nowrap; }
-.grok-model-names code { overflow: hidden; color: var(--muted); font-size: 10.5px; text-overflow: ellipsis; white-space: nowrap; }
+.grok-model-names code { overflow: hidden; color: var(--muted); font-size: 10.5px; /* deliberate: between --text-micro and --text-caption */ text-overflow: ellipsis; white-space: nowrap; }
.claude-lane-empty {
display: grid; place-items: center; min-height: 82px; padding: 12px; border: 1px dashed var(--border);
border-radius: var(--radius-sm); color: var(--muted); font-size: 12px; text-align: center;
@@ -2344,13 +2353,13 @@ button.prov-account-row.active { cursor: default; }
.claude-model-card[draggable="true"] { cursor: grab; }
.claude-model-card[draggable="true"]:active { cursor: grabbing; }
.claude-model-summary {
- display: flex; width: 100%; align-items: center; gap: 10px; padding: 9px 12px;
+ display: flex; width: 100%; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3);
border: 0; background: transparent; color: inherit; cursor: pointer; text-align: left;
}
.claude-model-summary:hover { background: var(--hover); }
-.claude-model-context { flex-shrink: 0; color: var(--muted); font-size: 11px; }
+.claude-model-context { flex-shrink: 0; color: var(--muted); font-size: var(--text-caption); }
.claude-model-context-unknown { color: var(--faint); font-style: italic; }
-.claude-row-default { flex-shrink: 0; color: var(--green); font-size: 10.5px; font-weight: 600; }
+.claude-row-default { flex-shrink: 0; color: var(--green); font-size: 10.5px; /* deliberate: compact badge row */ font-weight: 600; }
.claude-1m-chip {
flex-shrink: 0; padding: 1px 6px; border-radius: var(--radius-xs);
background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent);
@@ -2361,7 +2370,7 @@ button.prov-account-row.active { cursor: default; }
.claude-model-body > .claude-field:first-child { margin-top: 0; }
.claude-field > span, .claude-move-row > label { display: block; margin-bottom: 4px; color: var(--muted); font-size: 11.5px; font-weight: 550; }
.claude-alias {
- display: block; width: 100%; min-height: 34px; padding: 7px 10px; overflow: hidden;
+ display: block; width: 100%; min-height: var(--control-md); padding: var(--space-1-5) var(--space-2); overflow: hidden;
border: 1px solid var(--border); border-radius: var(--radius-xs); background: var(--raised);
color: var(--text); font-size: 11.5px; text-overflow: ellipsis; white-space: nowrap;
}
@@ -2388,7 +2397,7 @@ button.prov-account-row.active { cursor: default; }
/* ── Desktop status bar + effort badges (hardening WP3) ── */
.claude-status-bar {
- display: flex; align-items: center; gap: 10px; margin-bottom: 14px; padding: 8px 14px;
+ display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); padding: var(--space-2) var(--space-3);
border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface);
font-size: 12.5px; color: var(--muted);
}
diff --git a/gui/src/ui.tsx b/gui/src/ui.tsx
index 188d8f37be..ab297a8346 100644
--- a/gui/src/ui.tsx
+++ b/gui/src/ui.tsx
@@ -18,8 +18,10 @@ export function Notice({ tone, children }: { tone: "ok" | "err" | "warn"; childr
// `warn` is degraded-but-not-failed: the action happened, something adjacent
// did not. It must not render as the clean success the user did not get.
const toneClass = tone === "ok" ? "notice-ok" : tone === "warn" ? "notice-warn" : "notice-err";
+ // Errors must be announced immediately (alert); ok/warn stay polite status.
+ const liveRole = tone === "err" ? "alert" : "status";
return (
-
+
{tone === "ok" ? : }
{children}
diff --git a/gui/src/usage-report-store.ts b/gui/src/usage-report-store.ts
new file mode 100644
index 0000000000..9de210c2f3
--- /dev/null
+++ b/gui/src/usage-report-store.ts
@@ -0,0 +1,323 @@
+/**
+ * Domain store for GET /api/usage reports.
+ *
+ * Replaces the Usage page's private memory cache and the dashboard's usage poll
+ * with one keyed store shared by every surface: Usage page and Dashboard select
+ * the same entry for `${apiBase}:30d:all`, so concurrent subscribers dedupe into
+ * a single in-flight fetch (singleflight) and share the same AbortController
+ * cancellation semantics as `client-resource`.
+ *
+ * Persistence: zustand `persist` with sessionStorage stores ONLY validated
+ * successful reports plus a timestamp — never errors, never in-flight state.
+ * On rehydrate, seeded entries are marked `seedNeedsRevalidate` so the first
+ * subscriber quiet-revalidates instead of trusting the seed forever.
+ */
+
+import { create } from "zustand";
+import { persist, type PersistStorage, type StorageValue } from "zustand/middleware";
+import { useCallback, useEffect } from "react";
+import {
+ parseUsageReport,
+ type UsageRange,
+ type UsageReport,
+ type UsageSurface,
+} from "./usage-report-validation";
+
+export type UsageReportResource = {
+ key: string;
+ data: UsageReport | undefined;
+ error: unknown;
+ loading: boolean;
+ refreshing: boolean;
+ hasSucceeded: boolean;
+ lastAttemptOk: boolean;
+ refresh: (opts?: { forceLoading?: boolean; replace?: boolean }) => void;
+};
+
+export interface UsageReportEntry {
+ data?: UsageReport;
+ error?: unknown;
+ loading: boolean;
+ refreshing: boolean;
+ hasSucceeded: boolean;
+ lastAttemptOk: boolean;
+ /** Epoch ms when the report was persisted. */
+ persistedAt?: number;
+ /** True for a rehydrated seed: the first subscriber must quiet-revalidate. */
+ seedNeedsRevalidate: boolean;
+}
+
+/** The persisted slice — validated reports + timestamp only. */
+interface PersistedUsageSlice {
+ entries: Record
;
+}
+
+interface UsageReportStoreState {
+ entries: Record;
+ /** One in-flight controller per key; singleflight + cancellation. */
+ inflight: Record;
+ ensure: (key: string, apiBase: string, range: UsageRange, surface: UsageSurface) => void;
+ refresh: (
+ key: string,
+ apiBase: string,
+ range: UsageRange,
+ surface: UsageSurface,
+ opts?: { forceLoading?: boolean; replace?: boolean },
+ ) => void;
+ clearForTests: () => void;
+}
+
+export const USAGE_REPORT_STORAGE_NAME = "ccx.usage-reports.v1";
+
+/**
+ * sessionStorage resolved lazily at each call instead of captured at module load:
+ * GUI tests install the happy-dom sessionStorage per file/per test, and the store
+ * must read whatever storage is current when a read/write happens.
+ */
+const sessionStorageLazy: PersistStorage = {
+ getItem: (name) => {
+ try {
+ const raw = sessionStorage.getItem(name);
+ if (!raw) return null;
+ return JSON.parse(raw) as StorageValue;
+ } catch {
+ return null;
+ }
+ },
+ setItem: (name, value) => {
+ try {
+ sessionStorage.setItem(name, JSON.stringify(value));
+ } catch {
+ /* private mode / no sessionStorage in this runtime */
+ }
+ },
+ removeItem: (name) => {
+ try {
+ sessionStorage.removeItem(name);
+ } catch {
+ /* ignore */
+ }
+ },
+};
+
+export function usageReportKey(apiBase: string, range: UsageRange, surface: UsageSurface): string {
+ return `${apiBase}:${range}:${surface}`;
+}
+
+function emptyEntry(): UsageReportEntry {
+ return {
+ loading: false,
+ refreshing: false,
+ hasSucceeded: false,
+ lastAttemptOk: false,
+ seedNeedsRevalidate: false,
+ };
+}
+
+function fetchReport(
+ set: (partial: Partial | ((state: UsageReportStoreState) => Partial)) => void,
+ get: () => UsageReportStoreState,
+ key: string,
+ apiBase: string,
+ range: UsageRange,
+ surface: UsageSurface,
+ options?: { forceLoading?: boolean; replace?: boolean },
+): void {
+ const inflight = get().inflight[key];
+ // Singleflight: concurrent subscribers dedupe onto the in-flight request.
+ if (inflight && options?.replace !== true) return;
+ // Remember whether this request was a quiet revalidation of a rehydrated seed so a
+ // failed revalidation can restore the retry flag for the next subscriber.
+ const existingEntry = get().entries[key];
+ const wasSeed = existingEntry?.seedNeedsRevalidate === true && existingEntry.data !== undefined;
+ inflight?.abort();
+ const controller = new AbortController();
+ set(state => {
+ const existing = state.entries[key];
+ return {
+ inflight: { ...state.inflight, [key]: controller },
+ entries: {
+ ...state.entries,
+ [key]: {
+ ...(existing ?? emptyEntry()),
+ loading: options?.forceLoading === true || existing?.data === undefined,
+ refreshing: true,
+ error: undefined,
+ seedNeedsRevalidate: false,
+ },
+ },
+ };
+ });
+
+ void (async () => {
+ try {
+ const response = await fetch(`${apiBase}/api/usage?range=${range}&surface=${surface}`, {
+ signal: controller.signal,
+ });
+ if (!response.ok) throw new Error(`${response.status} ${response.statusText}`.trim());
+ const data = parseUsageReport(await response.json());
+ if (get().inflight[key] !== controller) return;
+ set(state => ({
+ inflight: { ...state.inflight, [key]: null },
+ entries: {
+ ...state.entries,
+ [key]: {
+ data,
+ error: undefined,
+ loading: false,
+ refreshing: false,
+ hasSucceeded: true,
+ lastAttemptOk: true,
+ persistedAt: Date.now(),
+ seedNeedsRevalidate: false,
+ },
+ },
+ }));
+ } catch (error) {
+ if (controller.signal.aborted || get().inflight[key] !== controller) return;
+ set(state => ({
+ inflight: { ...state.inflight, [key]: null },
+ entries: {
+ ...state.entries,
+ [key]: {
+ ...(state.entries[key] ?? emptyEntry()),
+ error: error === undefined ? new Error("usage report load failed") : error,
+ loading: false,
+ refreshing: false,
+ lastAttemptOk: false,
+ // A failed revalidation of a rehydrated seed keeps the seed as last-known-good
+ // (hasSucceeded) and re-arms the quiet retry for the next subscriber.
+ hasSucceeded: state.entries[key]?.hasSucceeded === true || wasSeed,
+ seedNeedsRevalidate: wasSeed,
+ },
+ },
+ }));
+ }
+ })();
+}
+
+export const useUsageReportStore = create()(
+ persist(
+ (set, get) => ({
+ entries: {},
+ inflight: {},
+ ensure: (key, apiBase, range, surface) => {
+ if (get().inflight[key]) return;
+ const entry = get().entries[key];
+ if (!entry) {
+ fetchReport(set, get, key, apiBase, range, surface, { forceLoading: true });
+ return;
+ }
+ if (entry.seedNeedsRevalidate) {
+ // Quiet revalidation of a rehydrated seed: keep the seed visible, no skeleton.
+ fetchReport(set, get, key, apiBase, range, surface, {});
+ return;
+ }
+ if (entry.data === undefined && !entry.hasSucceeded) {
+ // A previously cold-failed key retries on the next subscriber.
+ fetchReport(set, get, key, apiBase, range, surface, { forceLoading: true });
+ return;
+ }
+ // Healthy cached data — nothing to do.
+ },
+ refresh: (key, apiBase, range, surface, opts) => {
+ fetchReport(set, get, key, apiBase, range, surface, { ...opts, replace: opts?.replace !== false });
+ },
+ clearForTests: () => {
+ for (const controller of Object.values(get().inflight)) controller?.abort();
+ set({ entries: {}, inflight: {} });
+ },
+ }),
+ {
+ name: USAGE_REPORT_STORAGE_NAME,
+ storage: sessionStorageLazy,
+ partialize: (state): PersistedUsageSlice => ({
+ entries: Object.fromEntries(
+ Object.entries(state.entries)
+ .filter(([, entry]) => entry.data !== undefined)
+ .map(([key, entry]) => [
+ key,
+ { data: entry.data as UsageReport, persistedAt: entry.persistedAt ?? Date.now() },
+ ]),
+ ),
+ }),
+ merge: (persisted, current) => {
+ const persistedEntries =
+ (persisted as Partial | undefined)?.entries ?? {};
+ const entries: Record = { ...current.entries };
+ for (const [key, value] of Object.entries(persistedEntries)) {
+ if (value && value.data !== undefined) {
+ entries[key] = {
+ ...(entries[key] ?? emptyEntry()),
+ data: value.data,
+ persistedAt: value.persistedAt,
+ seedNeedsRevalidate: true,
+ // A rehydrated seed is last-known-good data: it reads as succeeded so the
+ // UI does not mistake "showing a seed" for "never succeeded".
+ hasSucceeded: true,
+ };
+ }
+ }
+ return { ...current, entries };
+ },
+ },
+ ),
+);
+
+/**
+ * Select a usage report and keep it fresh. The key derives from
+ * apiBase/range/surface, so the Usage page's 30d/all and the Dashboard's 30d/all
+ * share one store entry and one in-flight fetch.
+ */
+export function useUsageReport(
+ apiBase: string,
+ range: UsageRange,
+ surface: UsageSurface,
+): UsageReportResource {
+ const key = usageReportKey(apiBase, range, surface);
+ const entry = useUsageReportStore(state => state.entries[key]);
+ const ensure = useUsageReportStore(state => state.ensure);
+ const refreshAction = useUsageReportStore(state => state.refresh);
+
+ useEffect(() => {
+ ensure(key, apiBase, range, surface);
+ }, [key, apiBase, range, surface, ensure]);
+
+ const refresh = useCallback(
+ (opts?: { forceLoading?: boolean; replace?: boolean }) => {
+ refreshAction(key, apiBase, range, surface, opts);
+ },
+ [key, apiBase, range, surface, refreshAction],
+ );
+
+ return {
+ key,
+ data: entry?.data,
+ error: entry?.error,
+ loading: entry?.loading ?? false,
+ refreshing: entry?.refreshing ?? false,
+ hasSucceeded: entry?.hasSucceeded ?? false,
+ lastAttemptOk: entry?.lastAttemptOk ?? false,
+ refresh,
+ };
+}
+
+/** Test-only: drop every entry and abort in-flight work so suite order cannot reuse data. */
+export function clearUsageReportStoresForTests(): void {
+ useUsageReportStore.getState().clearForTests();
+}
+
+/** Test-only: seed an entry as if it were rehydrated from sessionStorage. */
+export function seedUsageReportForTests(key: string, data: UsageReport, persistedAt = Date.now()): void {
+ useUsageReportStore.setState(state => ({
+ entries: {
+ ...state.entries,
+ [key]: { ...emptyEntry(), data, persistedAt, seedNeedsRevalidate: true, hasSucceeded: true },
+ },
+ }));
+}
+
+/** Test-only: re-run persist rehydration against the current sessionStorage. */
+export function rehydrateUsageReportForTests(): void {
+ void useUsageReportStore.persist.rehydrate();
+}
diff --git a/gui/src/usage-report-validation.ts b/gui/src/usage-report-validation.ts
new file mode 100644
index 0000000000..89876aef0a
--- /dev/null
+++ b/gui/src/usage-report-validation.ts
@@ -0,0 +1,276 @@
+/**
+ * Validation for GET /api/usage reports.
+ *
+ * The Usage page and the usage-report domain store both consume this contract:
+ * only validated successful reports may be cached or persisted. Error envelopes
+ * (HTTP-level or body-level `error`) and malformed summaries are rejected here
+ * before any cache write, so a transient read failure can never shadow
+ * last-known-good data.
+ */
+
+export type UsageRange = "all" | "30d" | "7d";
+export type UsageSurface = "all" | "codex" | "claude" | "grok";
+
+export interface UsageSummaryTotals {
+ requests: number;
+ measuredRequests: number;
+ reportedRequests: number;
+ unreportedRequests: number;
+ unsupportedRequests: number;
+ estimatedRequests: number;
+ inputTokens: number;
+ outputTokens: number;
+ cachedInputTokens: number;
+ cacheReadInputTokens?: number;
+ cacheCreationInputTokens?: number;
+ reasoningOutputTokens: number;
+ totalTokens: number;
+ coverageRatio: number;
+ /** Required in the success DTO — the management API always emits these. */
+ estimatedCostUsd: number;
+ pricedRequests: number;
+ unpricedRequests: number;
+ unmeteredRequests: number;
+}
+
+export interface UsageDayModel {
+ model: string;
+ provider: string;
+ requests: number;
+ totalTokens: number;
+}
+
+export interface UsageDay {
+ date: string;
+ requests: number;
+ measuredRequests: number;
+ reportedRequests: number;
+ totalTokens: number;
+ models: UsageDayModel[];
+}
+
+export interface UsageModel {
+ provider: string;
+ model: string;
+ resolvedModel?: string;
+ requests: number;
+ measuredRequests: number;
+ reportedRequests: number;
+ estimatedRequests: number;
+ totalTokens: number;
+ inputTokens: number;
+ outputTokens: number;
+ shareRatio: number;
+}
+
+export interface UsageProvider {
+ provider: string;
+ requests: number;
+ measuredRequests: number;
+ reportedRequests: number;
+ estimatedRequests: number;
+ totalTokens: number;
+ shareRatio: number;
+}
+
+export interface UsageReport {
+ range: UsageRange;
+ surface: UsageSurface;
+ since: number | null;
+ generatedAt: number;
+ summary: UsageSummaryTotals;
+ days: UsageDay[];
+ models: UsageModel[];
+ providers: UsageProvider[];
+ historyTruncated: boolean;
+ truncatedPrefixBytes: number;
+ entriesTruncated: boolean;
+ entriesDropped: number;
+}
+
+/** Typed validation failure — callers must not persist the rejected payload. */
+export class UsageReportValidationError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "UsageReportValidationError";
+ }
+}
+
+const REQUIRED_SUMMARY_FIELDS: (keyof UsageSummaryTotals)[] = [
+ "requests",
+ "measuredRequests",
+ "reportedRequests",
+ "unreportedRequests",
+ "unsupportedRequests",
+ "estimatedRequests",
+ "inputTokens",
+ "outputTokens",
+ "cachedInputTokens",
+ "reasoningOutputTokens",
+ "totalTokens",
+ "coverageRatio",
+ "estimatedCostUsd",
+ "pricedRequests",
+ "unpricedRequests",
+ "unmeteredRequests",
+];
+
+function isFiniteNumber(value: unknown): value is number {
+ return typeof value === "number" && Number.isFinite(value);
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
+
+function parseSummary(value: unknown): UsageSummaryTotals {
+ if (!isRecord(value)) {
+ throw new UsageReportValidationError("usage report summary is not an object");
+ }
+ for (const key of REQUIRED_SUMMARY_FIELDS) {
+ if (!isFiniteNumber(value[key])) {
+ throw new UsageReportValidationError(
+ `usage report summary field "${key}" is missing or not a finite number`,
+ );
+ }
+ }
+ return {
+ ...(value as unknown as UsageSummaryTotals),
+ cacheReadInputTokens: isFiniteNumber(value.cacheReadInputTokens) ? value.cacheReadInputTokens : undefined,
+ cacheCreationInputTokens: isFiniteNumber(value.cacheCreationInputTokens) ? value.cacheCreationInputTokens : undefined,
+ };
+}
+
+function requireString(value: unknown, field: string): string {
+ if (typeof value !== "string" || !value) {
+ throw new UsageReportValidationError(`usage report ${field} is missing or not a non-empty string`);
+ }
+ return value;
+}
+
+function requireNumber(value: unknown, field: string): number {
+ if (!isFiniteNumber(value)) {
+ throw new UsageReportValidationError(`usage report ${field} is missing or not a finite number`);
+ }
+ return value;
+}
+
+function parseDayModels(value: unknown): UsageDayModel[] {
+ if (!Array.isArray(value)) {
+ throw new UsageReportValidationError("usage report day models must be an array");
+ }
+ return value.map((raw, index) => {
+ if (!isRecord(raw)) {
+ throw new UsageReportValidationError(`usage report day models[${index}] is not an object`);
+ }
+ return {
+ model: requireString(raw.model, `day models[${index}].model`),
+ provider: requireString(raw.provider, `day models[${index}].provider`),
+ requests: requireNumber(raw.requests, `day models[${index}].requests`),
+ totalTokens: requireNumber(raw.totalTokens, `day models[${index}].totalTokens`),
+ };
+ });
+}
+
+function parseDays(value: unknown): UsageDay[] {
+ if (!Array.isArray(value)) {
+ throw new UsageReportValidationError("usage report days must be an array");
+ }
+ return value.map((raw, index) => {
+ if (!isRecord(raw)) {
+ throw new UsageReportValidationError(`usage report days[${index}] is not an object`);
+ }
+ return {
+ date: requireString(raw.date, `days[${index}].date`),
+ requests: requireNumber(raw.requests, `days[${index}].requests`),
+ measuredRequests: requireNumber(raw.measuredRequests, `days[${index}].measuredRequests`),
+ reportedRequests: requireNumber(raw.reportedRequests, `days[${index}].reportedRequests`),
+ totalTokens: requireNumber(raw.totalTokens, `days[${index}].totalTokens`),
+ models: parseDayModels(raw.models),
+ };
+ });
+}
+
+function parseModels(value: unknown): UsageModel[] {
+ if (!Array.isArray(value)) {
+ throw new UsageReportValidationError("usage report models must be an array");
+ }
+ return value.map((raw, index) => {
+ if (!isRecord(raw)) {
+ throw new UsageReportValidationError(`usage report models[${index}] is not an object`);
+ }
+ return {
+ provider: requireString(raw.provider, `models[${index}].provider`),
+ model: requireString(raw.model, `models[${index}].model`),
+ ...(typeof raw.resolvedModel === "string" ? { resolvedModel: raw.resolvedModel } : {}),
+ requests: requireNumber(raw.requests, `models[${index}].requests`),
+ measuredRequests: requireNumber(raw.measuredRequests, `models[${index}].measuredRequests`),
+ reportedRequests: requireNumber(raw.reportedRequests, `models[${index}].reportedRequests`),
+ estimatedRequests: requireNumber(raw.estimatedRequests, `models[${index}].estimatedRequests`),
+ totalTokens: requireNumber(raw.totalTokens, `models[${index}].totalTokens`),
+ inputTokens: requireNumber(raw.inputTokens, `models[${index}].inputTokens`),
+ outputTokens: requireNumber(raw.outputTokens, `models[${index}].outputTokens`),
+ shareRatio: requireNumber(raw.shareRatio, `models[${index}].shareRatio`),
+ };
+ });
+}
+
+function parseProviders(value: unknown): UsageProvider[] {
+ if (!Array.isArray(value)) {
+ throw new UsageReportValidationError("usage report providers must be an array");
+ }
+ return value.map((raw, index) => {
+ if (!isRecord(raw)) {
+ throw new UsageReportValidationError(`usage report providers[${index}] is not an object`);
+ }
+ return {
+ provider: requireString(raw.provider, `providers[${index}].provider`),
+ requests: requireNumber(raw.requests, `providers[${index}].requests`),
+ measuredRequests: requireNumber(raw.measuredRequests, `providers[${index}].measuredRequests`),
+ reportedRequests: requireNumber(raw.reportedRequests, `providers[${index}].reportedRequests`),
+ estimatedRequests: requireNumber(raw.estimatedRequests, `providers[${index}].estimatedRequests`),
+ totalTokens: requireNumber(raw.totalTokens, `providers[${index}].totalTokens`),
+ shareRatio: requireNumber(raw.shareRatio, `providers[${index}].shareRatio`),
+ };
+ });
+}
+
+/**
+ * Parse and validate a GET /api/usage success body. Throws
+ * `UsageReportValidationError` for error envelopes, non-object bodies, invalid
+ * range/surface, missing collection fields, and missing or malformed required
+ * summary fields. Callers must only persist the returned value.
+ */
+export function parseUsageReport(body: unknown): UsageReport {
+ if (!isRecord(body)) {
+ throw new UsageReportValidationError("usage report is not an object");
+ }
+ if (body.error !== undefined) {
+ throw new UsageReportValidationError(`usage report rejected (${String(body.error)})`);
+ }
+ const range = body.range;
+ if (range !== "all" && range !== "30d" && range !== "7d") {
+ throw new UsageReportValidationError("usage report range is missing or invalid");
+ }
+ const surface = body.surface;
+ if (surface !== "all" && surface !== "codex" && surface !== "claude" && surface !== "grok") {
+ throw new UsageReportValidationError("usage report surface is missing or invalid");
+ }
+ if (!isFiniteNumber(body.generatedAt)) {
+ throw new UsageReportValidationError("usage report generatedAt is missing or not a finite number");
+ }
+ return {
+ range,
+ surface,
+ since: isFiniteNumber(body.since) ? body.since : null,
+ generatedAt: body.generatedAt,
+ summary: parseSummary(body.summary),
+ days: parseDays(body.days),
+ models: parseModels(body.models),
+ providers: parseProviders(body.providers),
+ historyTruncated: body.historyTruncated === true,
+ truncatedPrefixBytes: isFiniteNumber(body.truncatedPrefixBytes) ? body.truncatedPrefixBytes : 0,
+ entriesTruncated: body.entriesTruncated === true,
+ entriesDropped: isFiniteNumber(body.entriesDropped) ? body.entriesDropped : 0,
+ };
+}
diff --git a/gui/tests/dashboard-contracts.test.ts b/gui/tests/dashboard-contracts.test.ts
index 61e8391495..df05ce2fbf 100644
--- a/gui/tests/dashboard-contracts.test.ts
+++ b/gui/tests/dashboard-contracts.test.ts
@@ -34,26 +34,29 @@ test("Dashboard wires a single project-config diagnostics owner outside the sett
expect(overviewBody).not.toContain("diagnostics/project-config");
});
-test("Dashboard usage polling cannot delay core health and settings", async () => {
+test("Dashboard usage is owned by the shared usage-report store, not a dashboard poll", async () => {
const core = await Bun.file(new URL("../src/pages/dashboard-core-poll.ts", import.meta.url)).text();
const hook = await Bun.file(new URL("../src/pages/use-dashboard-data.ts", import.meta.url)).text();
const overviewFnStart = core.indexOf("export async function fetchDashboardOverview");
- const usageFnStart = core.indexOf("export async function fetchDashboardUsage");
const sidecarsFnStart = core.indexOf("export async function fetchDashboardSidecars");
expect(overviewFnStart).toBeGreaterThan(-1);
- expect(usageFnStart).toBeGreaterThan(-1);
expect(sidecarsFnStart).toBeGreaterThan(-1);
+ // The overview poll must never own the usage endpoint.
expect(core.slice(overviewFnStart, core.indexOf("export async function fetchDashboardMultiAgent"))).not.toContain("/api/usage?range=30d");
expect(core.slice(overviewFnStart, core.indexOf("export async function fetchDashboardMultiAgent"))).not.toContain("/api/sidecar-settings");
expect(core.slice(overviewFnStart, core.indexOf("export async function fetchDashboardMultiAgent"))).not.toContain("/api/shadow-call-settings");
expect(core.slice(sidecarsFnStart)).toContain("/api/sidecar-settings");
- expect(hook).toContain("dashboard-usage:${apiBase}");
+ // Usage lives in the domain store: the dashboard selects the same `${apiBase}:30d:all`
+ // entry the Usage page uses, so both surfaces dedupe into one in-flight fetch. No
+ // client-resource poll and no dashboard session-cache remain for usage.
+ expect(hook).toContain('useUsageReport(apiBase, "30d", "all")');
+ expect(hook).not.toContain("dashboard-usage:${apiBase}");
+ expect(hook).not.toContain("USAGE_CACHE_PREFIX");
+ expect(hook).not.toContain("fetchDashboardUsage");
expect(hook).toContain("dashboard-sidecars:${apiBase}");
expect(hook).toContain("dashboard-overview:${apiBase}");
- expect(hook).toContain("fetchDashboardUsage(apiBase, signal)");
expect(hook).toContain("fetchDashboardSidecars");
expect(hook).toContain("fetchDashboardOverview");
- expect(hook).toMatch(/dashboard-usage:\$\{apiBase\}[\s\S]*pollMs: 60_000/);
});
test("Dashboard interactive controls load independently of health/providers", async () => {
diff --git a/gui/tests/dashboard-plan-quota.test.tsx b/gui/tests/dashboard-plan-quota.test.tsx
new file mode 100644
index 0000000000..1e06932699
--- /dev/null
+++ b/gui/tests/dashboard-plan-quota.test.tsx
@@ -0,0 +1,510 @@
+import { afterAll, afterEach, beforeEach, expect, test } from "bun:test";
+import { Window } from "happy-dom";
+import { act } from "react";
+import type { Root } from "react-dom/client";
+import { LanguageProvider } from "../src/i18n/provider";
+import { clearClientResourceStoresForTests } from "../src/client-resource";
+import {
+ clearProviderQuotaStoresForTests,
+ PROVIDER_QUOTA_STORAGE_NAME,
+ useProviderQuotaStore,
+} from "../src/provider-quota-store";
+import { DashboardOverviewHead } from "../src/pages/dashboard-overview-head";
+import { DashboardPlanQuotaSection } from "../src/pages/dashboard-plan-quota-section";
+import type { UsageSummary30d } from "../src/pages/dashboard-shared";
+
+/**
+ * Phase 1c contract: the Dashboard overview shows a 30-day estimated cost stat with a
+ * request-coverage line ($0.00 for a defined zero, "—" when absent), and the Plan &
+ * quota section renders per-provider plans / quota windows / reference spend from the
+ * provider-quota store — never persisting account identities.
+ */
+
+const globals = ["document", "window", "navigator", "localStorage", "sessionStorage", "IS_REACT_ACT_ENVIRONMENT"] as const;
+let previousGlobals: Record<(typeof globals)[number], unknown>;
+let testWindow: Window;
+const originalFetch = globalThis.fetch;
+
+beforeEach(() => {
+ previousGlobals = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previousGlobals;
+ clearClientResourceStoresForTests();
+ clearProviderQuotaStoresForTests();
+ testWindow = new Window({ url: "http://localhost/" });
+ Object.defineProperties(globalThis, {
+ document: { configurable: true, value: testWindow.document },
+ window: { configurable: true, value: testWindow },
+ navigator: { configurable: true, value: testWindow.navigator },
+ localStorage: { configurable: true, value: testWindow.localStorage },
+ sessionStorage: { configurable: true, value: testWindow.sessionStorage },
+ });
+ (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+ testWindow.sessionStorage.clear();
+});
+
+afterEach(() => {
+ globalThis.fetch = originalFetch;
+ clearClientResourceStoresForTests();
+ clearProviderQuotaStoresForTests();
+ testWindow.close();
+ for (const key of globals) {
+ Object.defineProperty(globalThis, key, { configurable: true, value: previousGlobals[key] });
+ }
+});
+
+afterAll(() => {
+ globalThis.fetch = originalFetch;
+});
+
+async function waitFor(predicate: () => boolean, timeoutMs = 1000): Promise {
+ const start = Date.now();
+ while (!predicate()) {
+ if (Date.now() - start > timeoutMs) throw new Error("waitFor timed out");
+ await act(async () => {
+ await new Promise(resolve => testWindow.setTimeout(resolve, 10));
+ });
+ }
+}
+
+const NOW = Date.now();
+const aggregation = {
+ kind: "capacity-weighted-v1",
+ scope: "routable-known",
+ presentation: "aggregate",
+ incomplete: false,
+ excludedAccounts: 0,
+ unknownPlanAccounts: 0,
+ partialWindowAccounts: 0,
+ weekly: { usedPercent: 31, includedAccounts: 2, excludedAccounts: 0, incomplete: false, updatedAt: NOW },
+};
+
+test("Dashboard cost stat renders the 30-day estimate and coverage line", async () => {
+ const { createRoot } = await import("react-dom/client");
+ const container = document.createElement("div");
+ document.body.append(container);
+ const root = createRoot(container);
+
+ const usage30d: UsageSummary30d = {
+ summary: {
+ requests: 5,
+ totalTokens: 9000,
+ coverageRatio: 0.8,
+ estimatedCostUsd: 1.25,
+ pricedRequests: 4,
+ unpricedRequests: 1,
+ unmeteredRequests: 0,
+ },
+ };
+ const props = {
+ locale: "en" as const,
+ health: { status: "ok", version: "1.0", uptime: 100 },
+ providers: [],
+ usage30d,
+ usageLoading: false,
+ healthLoading: false,
+ startupHealth: null,
+ projectConfigWarnings: [],
+ maMode: "default" as const,
+ maBusy: false,
+ maHelpTriggerRef: { current: null },
+ maHelpOpen: false,
+ setMaHelpOpen: () => {},
+ switchMaMode: async () => {},
+ };
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ try {
+ const text = container.textContent ?? "";
+ expect(text).toContain("Est. cost (30d)");
+ expect(text).toContain("1.25");
+ expect(text).toContain("4 priced · 1 unpriced · 0 unmetered requests");
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("Dashboard cost stat renders $0.00 for a defined zero and — when absent", async () => {
+ const { createRoot } = await import("react-dom/client");
+ const base = {
+ locale: "en" as const,
+ health: { status: "ok", version: "1.0", uptime: 100 },
+ providers: [],
+ usageLoading: false,
+ healthLoading: false,
+ startupHealth: null,
+ projectConfigWarnings: [],
+ maMode: "default" as const,
+ maBusy: false,
+ maHelpTriggerRef: { current: null },
+ maHelpOpen: false,
+ setMaHelpOpen: () => {},
+ switchMaMode: async () => {},
+ };
+ const zeroContainer = document.createElement("div");
+ document.body.append(zeroContainer);
+ const zeroRoot = createRoot(zeroContainer);
+ await act(async () => {
+ zeroRoot.render(
+
+
+ ,
+ );
+ });
+ try {
+ expect(zeroContainer.textContent ?? "").toContain("$0.00");
+ // A defined zero must never fall back to the ~$0.0000 estimate rendering.
+ expect(zeroContainer.textContent ?? "").not.toContain("~");
+ } finally {
+ await act(async () => { zeroRoot.unmount(); });
+ zeroContainer.remove();
+ }
+ const absentContainer = document.createElement("div");
+ document.body.append(absentContainer);
+ const absentRoot = createRoot(absentContainer);
+ await act(async () => {
+ absentRoot.render(
+
+
+ ,
+ );
+ });
+ try {
+ expect(absentContainer.textContent ?? "").toContain("—");
+ } finally {
+ await act(async () => { absentRoot.unmount(); });
+ absentContainer.remove();
+ }
+});
+
+test("Plan & quota section renders provider plan, windows, and reference spend", async () => {
+ globalThis.fetch = (async () =>
+ new Response(JSON.stringify({
+ reports: [
+ {
+ provider: "openai",
+ label: "OpenAI (Codex login)",
+ source: "chatgpt:wham",
+ updatedAt: NOW,
+ quota: { weeklyPercent: 31, updatedAt: NOW },
+ aggregation,
+ },
+ {
+ provider: "opencode-go",
+ label: "OpenCode Go",
+ source: "opencode-go:published-caps-2026-08-05+local-estimate",
+ updatedAt: NOW,
+ quota: {
+ referenceWindows: [{
+ id: "five_hour",
+ label: "5-hour",
+ windowSeconds: 18_000,
+ publishedLimitUsd: 12,
+ observedSpendUsd: 1.25,
+ observedTokens: 42_000,
+ observedRequests: 3,
+ pricedRequests: 2,
+ unpricedRequests: 1,
+ unmeasuredRequests: 0,
+ coverage: "partial",
+ }],
+ updatedAt: NOW,
+ },
+ aggregation: undefined,
+ },
+ ],
+ availability: [],
+ }), { status: 200, headers: { "Content-Type": "application/json" } })) as typeof fetch;
+
+ const { createRoot } = await import("react-dom/client");
+ const container = document.createElement("div");
+ document.body.append(container);
+ const root = createRoot(container);
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ try {
+ await waitFor(() => (container.textContent ?? "").includes("Plan & quota"));
+ await waitFor(() => (container.textContent ?? "").includes("Configured-weight pool estimate"));
+ const text = container.textContent ?? "";
+ expect(text).toContain("OpenAI (Codex login)");
+ expect(text).toContain("31% used");
+ expect(text).toContain("OpenCode Go");
+ expect(text).toContain("$12 published cap");
+ expect(text).toContain("$1.25 observed through CodexCommander");
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("Plan & quota section never persists account identities to sessionStorage", async () => {
+ globalThis.fetch = (async () =>
+ new Response(JSON.stringify({
+ reports: [
+ {
+ provider: "openai",
+ label: "OpenAI (Codex login)",
+ source: "chatgpt:wham",
+ updatedAt: NOW,
+ quota: { weeklyPercent: 31, updatedAt: NOW },
+ aggregation,
+ // Stray identity fields the real server never emits.
+ accountId: "acct_12345",
+ account: { email: "acct@example.com" },
+ quota: {
+ weeklyPercent: 31,
+ updatedAt: NOW,
+ accountId: "acct_12345",
+ account: { email: "acct@example.com" },
+ },
+ },
+ ],
+ availability: [],
+ }), { status: 200, headers: { "Content-Type": "application/json" } })) as typeof fetch;
+
+ const { createRoot } = await import("react-dom/client");
+ const container = document.createElement("div");
+ document.body.append(container);
+ const root = createRoot(container);
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ try {
+ await waitFor(() => (container.textContent ?? "").includes("OpenAI (Codex login)"));
+ const persisted = testWindow.sessionStorage.getItem(PROVIDER_QUOTA_STORAGE_NAME) ?? "";
+ expect(persisted).not.toContain("acct@example.com");
+ expect(persisted).not.toContain("acct_12345");
+ expect(persisted).not.toContain("accountId");
+ expect(useProviderQuotaStore.getState().entries["http://plan-quota-privacy"]?.reports.openai?.accountId).toBeUndefined();
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+function openaiReport(): Record {
+ return {
+ provider: "openai",
+ label: "OpenAI (Codex login)",
+ source: "chatgpt:wham",
+ updatedAt: NOW,
+ quota: { weeklyPercent: 31, updatedAt: NOW },
+ aggregation,
+ };
+}
+
+test("Plan & quota renders a full-width strip for unavailable providers with per-reason actions", async () => {
+ globalThis.fetch = (async () =>
+ new Response(JSON.stringify({
+ reports: [openaiReport()],
+ availability: [
+ { provider: "xai", status: "unavailable", reason: "upstream_unavailable", checkedAt: NOW },
+ { provider: "anthropic", status: "unavailable", reason: "reauth_required", checkedAt: NOW },
+ { provider: "kimi", status: "unavailable", reason: "local_cli_refresh_required", checkedAt: NOW },
+ ],
+ }), { status: 200, headers: { "Content-Type": "application/json" } })) as typeof fetch;
+
+ const { createRoot } = await import("react-dom/client");
+ const container = document.createElement("div");
+ document.body.append(container);
+ const root = createRoot(container);
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ try {
+ await waitFor(() => (container.textContent ?? "").includes("Quota unavailable"));
+ const strip = container.querySelector(".dash-plan-quota-unavailable");
+ expect(strip).toBeTruthy();
+ const text = strip?.textContent ?? "";
+ expect(text).toContain("xAI Grok");
+ expect(text).toContain("Temporarily unavailable");
+ expect(text).toContain("Anthropic");
+ expect(text).toContain("Sign in required");
+ expect(text).toContain("Kimi");
+ expect(text).toContain("Login needs refresh");
+ // Reauth rows deep-link to the provider; retryable rows share one Retry.
+ expect(text).toContain("Manage Anthropic");
+ expect(text).toContain("Retry quota check");
+ const manageLink = Array.from(strip?.querySelectorAll("a") ?? [])
+ .find(anchor => anchor.textContent?.includes("Manage Anthropic"));
+ expect(manageLink?.getAttribute("href")).toBe("#providers/anthropic/overview");
+ expect(Array.from(strip?.querySelectorAll("button") ?? []).length).toBe(1);
+ // F6: the strip has no live-region role; each message span is the live region
+ // and contains no interactive descendants.
+ expect(strip?.getAttribute("role")).toBeNull();
+ const statusSpans = strip?.querySelectorAll('[role="status"]') ?? [];
+ expect(statusSpans.length).toBe(3);
+ for (const span of Array.from(statusSpans)) {
+ expect(span.querySelector("button, a")).toBeNull();
+ }
+ // F4: grid precedes the strip, strip precedes the disclaimer.
+ expect(strip?.previousElementSibling?.classList.contains("dash-sidecar-grid")).toBe(true);
+ expect(strip?.nextElementSibling?.textContent).toContain("Provider-reported caps and local estimates");
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("reauth-only strip deep-links to the provider and offers no quota retry", async () => {
+ const urls: string[] = [];
+ globalThis.fetch = (async (input: RequestInfo | URL) => {
+ urls.push(String(input));
+ return new Response(JSON.stringify({
+ reports: [],
+ availability: [
+ { provider: "anthropic", status: "unavailable", reason: "reauth_required", checkedAt: NOW },
+ ],
+ }), { status: 200, headers: { "Content-Type": "application/json" } });
+ }) as typeof fetch;
+
+ const { createRoot } = await import("react-dom/client");
+ const container = document.createElement("div");
+ document.body.append(container);
+ const root = createRoot(container);
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ try {
+ await waitFor(() => (container.textContent ?? "").includes("Sign in required"));
+ expect(container.textContent).toContain("Manage Anthropic");
+ expect(Array.from(container.querySelectorAll("button"))
+ .some(button => button.textContent?.includes("Retry"))).toBe(false);
+ expect(urls.every(url => !url.includes("refresh=1"))).toBe(true);
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("Plan & quota strip Retry forces refresh and disappears once the provider reports", async () => {
+ const urls: string[] = [];
+ globalThis.fetch = (async (input: RequestInfo | URL) => {
+ urls.push(String(input));
+ if (String(input).includes("refresh=1")) {
+ return new Response(JSON.stringify({
+ reports: [openaiReport(), { ...openaiReport(), provider: "xai", label: "xAI Grok" }],
+ availability: [{ provider: "xai", status: "available", checkedAt: NOW }],
+ }), { status: 200, headers: { "Content-Type": "application/json" } });
+ }
+ return new Response(JSON.stringify({
+ reports: [openaiReport()],
+ availability: [
+ { provider: "xai", status: "unavailable", reason: "upstream_unavailable", checkedAt: NOW },
+ ],
+ }), { status: 200, headers: { "Content-Type": "application/json" } });
+ }) as typeof fetch;
+
+ const { createRoot } = await import("react-dom/client");
+ const container = document.createElement("div");
+ document.body.append(container);
+ const root = createRoot(container);
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ try {
+ await waitFor(() => (container.textContent ?? "").includes("Quota unavailable"));
+ const strip = container.querySelector(".dash-plan-quota-unavailable");
+ expect(strip).toBeTruthy();
+ const retry = Array.from(container.querySelectorAll("button"))
+ .find(button => button.textContent?.trim() === "Retry quota check");
+ expect(retry).toBeTruthy();
+ await act(async () => { retry?.click(); });
+ await waitFor(() => urls.some(url => url.includes("refresh=1")));
+ await waitFor(() => !container.querySelector(".dash-plan-quota-unavailable"));
+ expect((container.textContent ?? "")).toContain("xAI Grok");
+ expect((container.textContent ?? "")).not.toContain("Temporarily unavailable");
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("Plan & quota strip stays hidden when providers are available or availability is absent", async () => {
+ globalThis.fetch = (async () =>
+ new Response(JSON.stringify({
+ reports: [openaiReport()],
+ availability: [{ provider: "xai", status: "available", checkedAt: NOW }],
+ }), { status: 200, headers: { "Content-Type": "application/json" } })) as typeof fetch;
+
+ const { createRoot } = await import("react-dom/client");
+ const container = document.createElement("div");
+ document.body.append(container);
+ const root = createRoot(container);
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ try {
+ await waitFor(() => (container.textContent ?? "").includes("Plan & quota"));
+ expect(container.querySelector(".dash-plan-quota-unavailable")).toBeNull();
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+
+ // No availability data at all: still no strip.
+ globalThis.fetch = (async () =>
+ new Response(JSON.stringify({
+ reports: [openaiReport()],
+ availability: [],
+ }), { status: 200, headers: { "Content-Type": "application/json" } })) as typeof fetch;
+ const container2 = document.createElement("div");
+ document.body.append(container2);
+ const root2 = createRoot(container2);
+ await act(async () => {
+ root2.render(
+
+
+ ,
+ );
+ });
+ try {
+ await waitFor(() => (container2.textContent ?? "").includes("Plan & quota"));
+ expect(container2.querySelector(".dash-plan-quota-unavailable")).toBeNull();
+ } finally {
+ await act(async () => { root2.unmount(); });
+ container2.remove();
+ }
+});
diff --git a/gui/tests/models-empty-provider.test.tsx b/gui/tests/models-empty-provider.test.tsx
index beb4cc12de..2509a486da 100644
--- a/gui/tests/models-empty-provider.test.tsx
+++ b/gui/tests/models-empty-provider.test.tsx
@@ -232,7 +232,7 @@ test("Models page combines final visibility, atomic actions, discovery status, a
expect(discoveryLink?.textContent).toContain("Auto-discovery on");
expect(discoveryLink?.getAttribute("aria-label")).toContain("Open provider settings");
expect(container.textContent).not.toContain("Not selected");
- expect(container.textContent).toContain("Reliable v1");
+ expect(container.textContent).toContain("Reliable V1");
expect(container.textContent).toContain("Flexible model selection");
expect(container.textContent).toContain("Uncapped");
expect(container.textContent).toContain("Models use their full advertised window");
diff --git a/gui/tests/page-loading-contract.test.tsx b/gui/tests/page-loading-contract.test.tsx
index 9793052778..aa98f45078 100644
--- a/gui/tests/page-loading-contract.test.tsx
+++ b/gui/tests/page-loading-contract.test.tsx
@@ -48,6 +48,13 @@ const MIGRATED = [
test("every migrated surface subscribes through the shared resource layer", async () => {
for (const surface of MIGRATED) {
const source = await read(surface.file);
+ if (surface.name === "Usage") {
+ // PR1 moved Usage onto the usage-report domain store. It still classifies render
+ // state through the shared data-surface machine (classifyDataSurface), so the
+ // contract holds; only the subscription mechanism changed.
+ expect(source, surface.name).toContain("classifyDataSurface");
+ continue;
+ }
expect(source, surface.name).toContain("useDataSurface");
}
});
diff --git a/gui/tests/provider-capacity-shell.test.tsx b/gui/tests/provider-capacity-shell.test.tsx
index 8c34855e47..b4b15cb62f 100644
--- a/gui/tests/provider-capacity-shell.test.tsx
+++ b/gui/tests/provider-capacity-shell.test.tsx
@@ -3,8 +3,14 @@ import { Window } from "happy-dom";
import { act } from "react";
import type { Root } from "react-dom/client";
import ProviderWorkspaceShell from "../src/components/provider-workspace/ProviderWorkspaceShell";
+import type { DetailSlotData } from "../src/components/provider-workspace/ProviderWorkspaceShell";
import { LanguageProvider } from "../src/i18n/provider";
import { readSessionListCache, writeSessionListCache } from "../src/session-list-cache";
+import {
+ clearProviderQuotaStoresForTests,
+ PROVIDER_QUOTA_STORAGE_NAME,
+ rehydrateProviderQuotaForTests,
+} from "../src/provider-quota-store";
const globals = ["document", "window", "navigator", "localStorage", "sessionStorage", "IS_REACT_ACT_ENVIRONMENT"] as const;
let previous: Record<(typeof globals)[number], unknown>;
@@ -146,6 +152,7 @@ function aggregateWindowPayload(weeklyIncomplete: boolean, monthlyIncomplete: bo
}
beforeEach(() => {
+ clearProviderQuotaStoresForTests();
previous = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previous;
originalFetch = globalThis.fetch;
win = new Window({ url: "http://localhost/" });
@@ -229,6 +236,71 @@ test("quota auth availability immediately moves Grok from connected to needs att
expect(text).not.toContain("1Ready");
});
+test("detail slot keeps genuine account reauth separate from quota-derived attention", async () => {
+ quotaPayload = {
+ reports: [],
+ availability: [{
+ provider: "xai",
+ status: "unavailable",
+ reason: "local_cli_refresh_required",
+ checkedAt: Date.now(),
+ }],
+ };
+
+ let capturedItem: { activeNeedsReauth?: boolean } | null = null;
+ let capturedData: DetailSlotData | null = null;
+ const { createRoot } = await import("react-dom/client");
+ await act(async () => {
+ root ??= createRoot(host);
+ root.render(
+
+ {}}
+ onAddProvider={() => {}}
+ activeAccountNeedsReauth={undefined}
+ detail={(item, data) => {
+ capturedItem = item;
+ capturedData = data;
+ return null;
+ }}
+ />
+ ,
+ );
+ });
+ await act(async () => { await new Promise(resolve => setTimeout(resolve, 30)); });
+ // Quota-derived attention flips the merged flag, but the provenance slot stays
+ // false because no account-health input says a browser reauth is needed.
+ expect(capturedData?.accountNeedsReauth).toBe(false);
+ expect(capturedItem?.activeNeedsReauth).toBe(true);
+
+ await act(async () => {
+ root?.render(
+
+ {}}
+ onAddProvider={() => {}}
+ activeAccountNeedsReauth={{ xai: true }}
+ detail={(item, data) => {
+ capturedItem = item;
+ capturedData = data;
+ return null;
+ }}
+ />
+ ,
+ );
+ });
+ await act(async () => { await new Promise(resolve => setTimeout(resolve, 30)); });
+ expect(capturedData?.accountNeedsReauth).toBe(true);
+});
+
test("provider quota fetch preserves aggregate capacity through shell state and render", async () => {
await mountShell();
@@ -248,29 +320,51 @@ test("provider quota fetch preserves aggregate capacity through shell state and
expect(text).not.toMatch(/configured units|weighted units|units remaining|projected/i);
});
-test("successful empty quota response removes cached providers and updates session cache", async () => {
+test("successful empty quota response clears persisted quota data", async () => {
const seeded = (aggregatePayload().reports[0]);
- const { provider: _provider, ...cached } = seeded;
- writeSessionListCache(QUOTA_CACHE_KEY, { openai: cached });
+ const { provider: _provider, ...report } = seeded;
+ // A session-cache revisit seeds through the provider-quota store's persisted slice.
+ win.sessionStorage.setItem(PROVIDER_QUOTA_STORAGE_NAME, JSON.stringify({
+ state: {
+ entries: {
+ "": { reports: { openai: report }, updatedAt: Date.now() },
+ },
+ },
+ version: 0,
+ }));
+ rehydrateProviderQuotaForTests();
quotaPayload = { reports: [] };
await mountShell();
expect(host.textContent ?? "").not.toContain("Configured-weight pool estimate");
- expect(readSessionListCache(QUOTA_CACHE_KEY)).toEqual({});
+ // The empty authoritative response removed the provider from the persisted slice.
+ const persisted = JSON.parse(win.sessionStorage.getItem(PROVIDER_QUOTA_STORAGE_NAME) ?? "{}");
+ expect(persisted.state?.entries?.[""]).toBeUndefined();
});
test("expired session quota is rejected and a failed fetch cannot keep it rendered", async () => {
const old = Date.now() - 31 * 60_000;
- writeSessionListCache(QUOTA_CACHE_KEY, {
- openai: {
- label: "OpenAI (Codex login)",
- source: "chatgpt:wham",
- updatedAt: old,
- quota: { weeklyPercent: 99, updatedAt: old },
- aggregation: { ...aggregatePayload().reports[0].aggregation, presentation: "aggregate" },
+ win.sessionStorage.setItem(PROVIDER_QUOTA_STORAGE_NAME, JSON.stringify({
+ state: {
+ entries: {
+ "": {
+ reports: {
+ openai: {
+ label: "OpenAI (Codex login)",
+ source: "chatgpt:wham",
+ updatedAt: old,
+ quota: { weeklyPercent: 99, updatedAt: old },
+ aggregation: { ...aggregatePayload().reports[0].aggregation, presentation: "aggregate" },
+ },
+ },
+ updatedAt: old,
+ },
+ },
},
- });
+ version: 0,
+ }));
+ rehydrateProviderQuotaForTests();
rejectQuotaFetch = true;
await mountShell();
@@ -278,7 +372,9 @@ test("expired session quota is rejected and a failed fetch cannot keep it render
const text = host.textContent ?? "";
expect(text).not.toContain("Configured-weight pool estimate");
expect(text).not.toContain("99% used");
- expect(readSessionListCache(QUOTA_CACHE_KEY)).toEqual({});
+ // The stale seed was rejected at rehydrate, and the failed fetch persisted nothing.
+ const persisted = JSON.parse(win.sessionStorage.getItem(PROVIDER_QUOTA_STORAGE_NAME) ?? "{}");
+ expect(persisted.state?.entries?.[""]).toBeUndefined();
});
test("a cancelled superseded quota rejection cannot rewrite state or session cache", async () => {
diff --git a/gui/tests/provider-overview-quota-unavailable.test.tsx b/gui/tests/provider-overview-quota-unavailable.test.tsx
new file mode 100644
index 0000000000..59f3792200
--- /dev/null
+++ b/gui/tests/provider-overview-quota-unavailable.test.tsx
@@ -0,0 +1,252 @@
+import { afterEach, beforeEach, expect, test } from "bun:test";
+import { Window } from "happy-dom";
+import { act } from "react";
+import type { Root } from "react-dom/client";
+import { LanguageProvider } from "../src/i18n/provider";
+import ProviderOverview from "../src/components/provider-workspace/ProviderOverview";
+import type { WorkspaceItem } from "../src/provider-workspace/catalog";
+import type { ProviderQuotaReportView } from "../src/provider-workspace/report";
+
+const globals = ["document", "window", "navigator", "localStorage", "IS_REACT_ACT_ENVIRONMENT"] as const;
+let previousGlobals: Record<(typeof globals)[number], unknown>;
+let testWindow: Window;
+const originalFetch = globalThis.fetch;
+
+beforeEach(() => {
+ previousGlobals = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previousGlobals;
+ testWindow = new Window({ url: "http://localhost/#providers" });
+ Object.defineProperties(globalThis, {
+ document: { configurable: true, value: testWindow.document },
+ window: { configurable: true, value: testWindow },
+ navigator: { configurable: true, value: testWindow.navigator },
+ localStorage: { configurable: true, value: testWindow.localStorage },
+ });
+ (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+});
+
+afterEach(() => {
+ globalThis.fetch = originalFetch;
+ testWindow.close();
+ for (const key of globals) {
+ Object.defineProperty(globalThis, key, { configurable: true, value: previousGlobals[key] });
+ }
+});
+
+const item = {
+ name: "xai",
+ adapter: "openai-chat",
+ baseUrl: "https://api.x.ai/v1",
+ authMode: "oauth",
+ hasApiKey: false,
+} as WorkspaceItem;
+
+async function mountOverview(props: {
+ quotaReport?: ProviderQuotaReportView;
+ quotaUnavailableReason?: string;
+ onRetryQuota?: () => void;
+ accountNeedsReauth?: boolean;
+ onReauthenticate?: () => void;
+ itemOverrides?: Partial;
+}): Promise<{ root: Root; container: HTMLElement }> {
+ const container = document.createElement("div");
+ document.body.append(container);
+ const { createRoot } = await import("react-dom/client");
+ let root!: Root;
+ await act(async () => {
+ root = createRoot(container);
+ root.render(
+
+
+ ,
+ );
+ });
+ return { root, container };
+}
+
+test("renders the quota-unavailable notice with reason copy and Retry when no report exists", async () => {
+ const { root, container } = await mountOverview({
+ quotaUnavailableReason: "upstream_unavailable",
+ onRetryQuota: () => {},
+ });
+ try {
+ const text = container.textContent ?? "";
+ expect(text).toContain("Quota unavailable");
+ expect(text).toContain("Temporarily unavailable");
+ expect(text).toContain("Retry");
+ expect(text).not.toContain("Rate limits");
+ // F6: the warning container has no live-region role; the message span does,
+ // and no interactive control lives inside it.
+ const warnings = container.querySelectorAll(".pws-auth-summary--warn");
+ expect(warnings.length).toBe(1);
+ expect(warnings[0]?.getAttribute("role")).toBeNull();
+ const status = warnings[0]?.querySelector('[role="status"]');
+ expect(status?.textContent).toContain("Temporarily unavailable");
+ expect(status?.querySelector("button, a")).toBeNull();
+ const retry = Array.from(container.querySelectorAll("button"))
+ .find(button => button.textContent?.trim() === "Retry");
+ expect(retry).toBeTruthy();
+ expect(status?.contains(retry as Node)).toBe(false);
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("reauth_required is owned by the auth warning, not a second quota warning", async () => {
+ let reauthed = false;
+ const { root, container } = await mountOverview({
+ quotaUnavailableReason: "reauth_required",
+ onRetryQuota: () => { throw new Error("quota retry must not be offered when auth owns the warning"); },
+ onReauthenticate: () => { reauthed = true; },
+ itemOverrides: { activeNeedsReauth: true },
+ });
+ try {
+ const warnings = container.querySelectorAll(".pws-auth-summary--warn");
+ expect(warnings.length).toBe(1);
+ const text = container.textContent ?? "";
+ expect(text).toContain("Needs attention");
+ expect(text).toContain("Active account needs re-authentication");
+ expect(text).toContain("Re-authenticate");
+ expect(text).not.toContain("Quota unavailable");
+ expect(text).not.toContain("Sign in required");
+ expect(text).not.toContain("Retry");
+ const warn = warnings[0]!;
+ expect(warn.getAttribute("role")).toBeNull();
+ const status = warn.querySelector('[role="status"]');
+ expect(status?.textContent).toContain("Needs attention");
+ expect(status?.querySelector("button, a")).toBeNull();
+ await act(async () => {
+ (Array.from(container.querySelectorAll("button"))
+ .find(button => button.textContent?.trim() === "Re-authenticate"))?.click();
+ });
+ expect(reauthed).toBe(true);
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("Retry invokes onRetryQuota", async () => {
+ let retried = false;
+ const { root, container } = await mountOverview({
+ quotaUnavailableReason: "local_cli_refresh_required",
+ onRetryQuota: () => { retried = true; },
+ });
+ try {
+ const retry = Array.from(container.querySelectorAll("button"))
+ .find(button => button.textContent?.trim() === "Retry");
+ expect(retry).toBeTruthy();
+ await act(async () => { retry?.click(); });
+ expect(retried).toBe(true);
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("local_cli_refresh_required alone is owned by the quota notice, not the auth warning", async () => {
+ const { root, container } = await mountOverview({
+ quotaUnavailableReason: "local_cli_refresh_required",
+ onRetryQuota: () => {},
+ itemOverrides: { activeNeedsReauth: true },
+ accountNeedsReauth: false,
+ });
+ try {
+ const warnings = container.querySelectorAll(".pws-auth-summary--warn");
+ expect(warnings.length).toBe(1);
+ const text = container.textContent ?? "";
+ expect(text).toContain("Quota unavailable");
+ expect(text).toContain("Login needs refresh");
+ expect(text).toContain("Retry");
+ expect(text).not.toContain("Active account needs re-authentication");
+ expect(text).not.toContain("Re-authenticate");
+ // The connection status row still surfaces attention (status metadata only).
+ expect(text).toContain("Needs attention");
+ const warn = warnings[0]!;
+ expect(warn.getAttribute("role")).toBeNull();
+ const status = warn.querySelector('[role="status"]');
+ expect(status?.textContent).toContain("Login needs refresh");
+ expect(status?.querySelector("button, a")).toBeNull();
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("local CLI refresh plus a genuine account reauth shows both warnings", async () => {
+ const { root, container } = await mountOverview({
+ quotaUnavailableReason: "local_cli_refresh_required",
+ onRetryQuota: () => {},
+ onReauthenticate: () => {},
+ itemOverrides: { activeNeedsReauth: true },
+ accountNeedsReauth: true,
+ });
+ try {
+ expect(container.querySelectorAll(".pws-auth-summary--warn").length).toBe(2);
+ const text = container.textContent ?? "";
+ expect(text).toContain("Login needs refresh");
+ expect(text).toContain("Retry");
+ expect(text).toContain("Active account needs re-authentication");
+ expect(text).toContain("Re-authenticate");
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("reauth_required without merged attention keeps the quota notice visible", async () => {
+ const { root, container } = await mountOverview({
+ quotaUnavailableReason: "reauth_required",
+ onRetryQuota: () => {},
+ });
+ try {
+ const text = container.textContent ?? "";
+ expect(text).toContain("Quota unavailable");
+ expect(text).toContain("Sign in required");
+ expect(text).toContain("Retry");
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("quota card (not the notice) renders when a report exists", async () => {
+ const report: ProviderQuotaReportView = {
+ label: "xAI Grok",
+ source: "xai:api",
+ updatedAt: Date.now(),
+ quota: { weeklyPercent: 10 },
+ aggregation: undefined,
+ };
+ const { root, container } = await mountOverview({
+ quotaReport: report,
+ quotaUnavailableReason: "upstream_unavailable",
+ });
+ try {
+ const text = container.textContent ?? "";
+ expect(text).toContain("Rate limits");
+ expect(text).not.toContain("Temporarily unavailable");
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("no notice and no quota card when neither report nor reason exists", async () => {
+ const { root, container } = await mountOverview({});
+ try {
+ const text = container.textContent ?? "";
+ expect(text).not.toContain("Rate limits");
+ expect(text).not.toContain("Quota unavailable");
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
diff --git a/gui/tests/provider-quota-store.test.ts b/gui/tests/provider-quota-store.test.ts
new file mode 100644
index 0000000000..29c43b1228
--- /dev/null
+++ b/gui/tests/provider-quota-store.test.ts
@@ -0,0 +1,295 @@
+import { afterAll, afterEach, beforeEach, expect, test } from "bun:test";
+import { Window } from "happy-dom";
+
+/**
+ * Store-level contract for the provider-quota domain store: keyed by apiBase,
+ * singleflight dedupe, force-refresh TTL bypass, stale-seed rejection at rehydrate,
+ * and the privacy invariant — the persisted slice never carries account identities.
+ *
+ * sessionStorage is installed before the store module is imported so zustand persist
+ * sees it (Bun shares the module registry within one run, so the lazy storage reads
+ * whatever sessionStorage is current at each call).
+ */
+const testWindow = new Window({ url: "http://localhost/" });
+const originalFetch = globalThis.fetch;
+const INSTALLED_GLOBALS = ["document", "window", "navigator", "sessionStorage", "IS_REACT_ACT_ENVIRONMENT"] as const;
+const previousGlobals = Object.fromEntries(
+ INSTALLED_GLOBALS.map(key => [key, Reflect.get(globalThis, key)]),
+) as Record<(typeof INSTALLED_GLOBALS)[number], unknown>;
+Object.defineProperties(globalThis, {
+ document: { configurable: true, value: testWindow.document },
+ window: { configurable: true, value: testWindow },
+ navigator: { configurable: true, value: testWindow.navigator },
+ sessionStorage: { configurable: true, value: testWindow.sessionStorage },
+});
+(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+
+const {
+ clearProviderQuotaStoresForTests,
+ PROVIDER_QUOTA_STORAGE_NAME,
+ quotaAvailabilityFromResponse,
+ rehydrateProviderQuotaForTests,
+ unavailableQuotaProviders,
+ useProviderQuotaStore,
+} = await import("../src/provider-quota-store");
+
+const now = Date.now();
+const aggregation = {
+ kind: "capacity-weighted-v1",
+ scope: "routable-known",
+ presentation: "aggregate",
+ incomplete: false,
+ excludedAccounts: 0,
+ unknownPlanAccounts: 0,
+ partialWindowAccounts: 0,
+};
+
+function report(overrides: Record = {}) {
+ return {
+ provider: "openai",
+ label: "OpenAI (Codex login)",
+ source: "chatgpt:wham",
+ updatedAt: now,
+ quota: { weeklyPercent: 20, updatedAt: now },
+ aggregation,
+ ...overrides,
+ };
+}
+
+function quotaResponse(payload: unknown) {
+ return {
+ ok: true,
+ status: 200,
+ json: async () => payload,
+ } as unknown as Response;
+}
+
+beforeEach(() => {
+ clearProviderQuotaStoresForTests();
+ testWindow.sessionStorage.clear();
+});
+
+afterEach(() => {
+ globalThis.fetch = originalFetch;
+ clearProviderQuotaStoresForTests();
+});
+
+afterAll(() => {
+ testWindow.close();
+ for (const key of INSTALLED_GLOBALS) {
+ Object.defineProperty(globalThis, key, { configurable: true, value: previousGlobals[key] });
+ }
+});
+
+async function flush(times = 3): Promise {
+ for (let i = 0; i < times; i++) {
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+}
+
+test("force refresh adds the server TTL bypass (?refresh=1)", async () => {
+ const urls: string[] = [];
+ globalThis.fetch = (async (input: RequestInfo | URL) => {
+ urls.push(String(input));
+ return quotaResponse({ reports: [report()], availability: [] });
+ }) as typeof fetch;
+
+ useProviderQuotaStore.getState().ensure("");
+ await flush();
+ useProviderQuotaStore.getState().refresh("", { force: true });
+ await flush();
+ expect(urls).toEqual(["/api/provider-quotas", "/api/provider-quotas?refresh=1"]);
+});
+
+test("singleflight dedupes concurrent subscribers into one fetch", async () => {
+ let calls = 0;
+ const gates: Array<() => void> = [];
+ globalThis.fetch = (async () => {
+ calls += 1;
+ await new Promise(resolve => gates.push(resolve));
+ return quotaResponse({ reports: [report()], availability: [] });
+ }) as typeof fetch;
+
+ useProviderQuotaStore.getState().ensure("");
+ useProviderQuotaStore.getState().ensure("");
+ useProviderQuotaStore.getState().ensure("");
+ expect(calls).toBe(1);
+ gates[0]!();
+ await flush();
+ expect(useProviderQuotaStore.getState().entries[""]?.reports.openai).toBeDefined();
+ expect(useProviderQuotaStore.getState().entries[""]?.hasSucceeded).toBe(true);
+});
+
+test("persists only reports and a timestamp — never account identities", async () => {
+ // A hostile/legacy server response carrying account identity fields must never reach
+ // sessionStorage: the store projects the wire shape, not the raw payload — including
+ // identity fields stashed INSIDE quota or aggregation.
+ const payload = {
+ reports: [report({
+ // Stray identity fields on the wire row (the real server never emits these).
+ accountId: "acct_12345",
+ account: { email: "acct@example.com" },
+ quota: {
+ weeklyPercent: 20,
+ updatedAt: now,
+ accountId: "acct_12345",
+ account: { email: "acct@example.com" },
+ },
+ aggregation: {
+ ...aggregation,
+ currentAccount: {
+ isMain: true,
+ plan: "pro",
+ quota: { weeklyPercent: 8, updatedAt: now },
+ email: "acct@example.com",
+ accountId: "acct_12345",
+ },
+ },
+ })],
+ availability: [{ provider: "openai", status: "available", checkedAt: now }],
+ // Stray top-level identity fields.
+ accountId: "acct_12345",
+ account: { email: "acct@example.com" },
+ };
+ globalThis.fetch = (async () => quotaResponse(payload)) as typeof fetch;
+ useProviderQuotaStore.getState().ensure("");
+ await flush();
+
+ const persisted = testWindow.sessionStorage.getItem(PROVIDER_QUOTA_STORAGE_NAME) ?? "";
+ expect(persisted).toContain("openai");
+ expect(persisted).not.toContain("acct@example.com");
+ expect(persisted).not.toContain("acct_12345");
+ expect(persisted).not.toContain("accountId");
+ const parsed = JSON.parse(persisted);
+ const entry = parsed.state.entries[""];
+ expect(entry.reports.openai.provider).toBeUndefined();
+ expect(entry.reports.openai.label).toBe("OpenAI (Codex login)");
+ expect(entry.reports.openai.quota.weeklyPercent).toBe(20);
+ expect(typeof entry.updatedAt).toBe("number");
+});
+
+test("a failed fetch keeps the last-known-good reports", async () => {
+ globalThis.fetch = (async () => quotaResponse({ reports: [report()], availability: [] })) as typeof fetch;
+ useProviderQuotaStore.getState().ensure("");
+ await flush();
+
+ globalThis.fetch = (async () => new Response("boom", { status: 503 })) as typeof fetch;
+ useProviderQuotaStore.getState().refresh("");
+ await flush();
+ const entry = useProviderQuotaStore.getState().entries[""];
+ expect(entry?.reports.openai).toBeDefined();
+ expect(entry?.lastAttemptOk).toBe(false);
+});
+
+test("stale rehydrated seeds are rejected at rehydrate", async () => {
+ const old = Date.now() - 31 * 60_000;
+ testWindow.sessionStorage.setItem(PROVIDER_QUOTA_STORAGE_NAME, JSON.stringify({
+ state: {
+ entries: {
+ "": { reports: { openai: report({ updatedAt: old }) }, updatedAt: old },
+ },
+ },
+ version: 0,
+ }));
+ rehydrateProviderQuotaForTests();
+ const entry = useProviderQuotaStore.getState().entries[""];
+ // The stale row was dropped: either no entry, or an entry without reports.
+ expect(entry?.reports).toBeUndefined();
+});
+
+test("quotaAvailabilityFromResponse projects provider/status/reason/checkedAt only", () => {
+ const projected = quotaAvailabilityFromResponse([
+ { provider: "xai", status: "unavailable", reason: "upstream_unavailable", checkedAt: 123, email: "x@example.com", accountId: "acct_1" },
+ { provider: "openai", status: "available", checkedAt: 456 },
+ { provider: "anthropic", status: "unavailable", reason: "unknown_reason_code", checkedAt: 789 },
+ { provider: "banana", status: "hostile_status", checkedAt: 111 },
+ { provider: "" },
+ null,
+ "garbage",
+ ]);
+ expect(projected).toEqual({
+ xai: { status: "unavailable", reason: "upstream_unavailable", checkedAt: 123 },
+ openai: { status: "available", checkedAt: 456 },
+ // Unknown reason codes are dropped (never projected onto the DOM path).
+ anthropic: { status: "unavailable", checkedAt: 789 },
+ // Unknown status strings are dropped too (never treated as unavailable).
+ banana: undefined,
+ });
+ expect(projected.banana).toBeUndefined();
+});
+
+test("unavailableQuotaProviders lists only non-available providers without reports, sorted", () => {
+ const availability = {
+ xai: { status: "unavailable", reason: "upstream_unavailable", checkedAt: 1 },
+ openai: { status: "available", checkedAt: 2 },
+ anthropic: { status: "stale", reason: "reauth_required", checkedAt: 3 },
+ grok: { status: "unavailable", checkedAt: 4 },
+ gemini: { status: "stale", checkedAt: 5 },
+ };
+ const reports = { openai: {}, anthropic: {} };
+ expect(unavailableQuotaProviders(availability, reports)).toEqual([
+ // stale-without-report qualifies (status !== "available").
+ { provider: "gemini" },
+ { provider: "grok" },
+ { provider: "xai", reason: "upstream_unavailable" },
+ ]);
+});
+
+test("a successful fetch populates availability; refresh updates it", async () => {
+ globalThis.fetch = (async () => quotaResponse({
+ reports: [report()],
+ availability: [{ provider: "xai", status: "unavailable", reason: "upstream_unavailable", checkedAt: now }],
+ })) as typeof fetch;
+ useProviderQuotaStore.getState().ensure("");
+ await flush();
+ expect(useProviderQuotaStore.getState().entries[""]?.availability).toEqual({
+ xai: { status: "unavailable", reason: "upstream_unavailable", checkedAt: now },
+ });
+
+ globalThis.fetch = (async () => quotaResponse({
+ reports: [report()],
+ availability: [{ provider: "xai", status: "available", checkedAt: now }],
+ })) as typeof fetch;
+ useProviderQuotaStore.getState().refresh("");
+ await flush();
+ expect(useProviderQuotaStore.getState().entries[""]?.availability.xai).toEqual({
+ status: "available",
+ checkedAt: now,
+ });
+});
+
+test("a failed fetch keeps the last-known-good availability", async () => {
+ globalThis.fetch = (async () => quotaResponse({
+ reports: [report()],
+ availability: [{ provider: "xai", status: "unavailable", reason: "upstream_unavailable", checkedAt: now }],
+ })) as typeof fetch;
+ useProviderQuotaStore.getState().ensure("");
+ await flush();
+
+ globalThis.fetch = (async () => new Response("boom", { status: 503 })) as typeof fetch;
+ useProviderQuotaStore.getState().refresh("");
+ await flush();
+ const entry = useProviderQuotaStore.getState().entries[""];
+ expect(entry?.availability.xai.reason).toBe("upstream_unavailable");
+ expect(entry?.lastAttemptOk).toBe(false);
+});
+
+test("availability is never persisted to sessionStorage", async () => {
+ const payload = {
+ reports: [report()],
+ availability: [
+ { provider: "xai", status: "unavailable", reason: "upstream_unavailable", checkedAt: now, email: "x@example.com", accountId: "acct_xai" },
+ ],
+ };
+ globalThis.fetch = (async () => quotaResponse(payload)) as typeof fetch;
+ useProviderQuotaStore.getState().ensure("");
+ await flush();
+
+ const persisted = testWindow.sessionStorage.getItem(PROVIDER_QUOTA_STORAGE_NAME) ?? "";
+ const parsed = JSON.parse(persisted) as { state: { entries: Record } };
+ const entry = parsed.state.entries[""] as Record;
+ expect(entry).not.toHaveProperty("availability");
+ expect(persisted).not.toContain("upstream_unavailable");
+ expect(persisted).not.toContain("acct_xai");
+ expect(persisted).not.toContain("x@example.com");
+});
diff --git a/gui/tests/provider-revalidation-policy.test.tsx b/gui/tests/provider-revalidation-policy.test.tsx
index 5bbbdb9f7e..e970ba1e40 100644
--- a/gui/tests/provider-revalidation-policy.test.tsx
+++ b/gui/tests/provider-revalidation-policy.test.tsx
@@ -5,6 +5,7 @@ import { createRoot, type Root } from "react-dom/client";
import Providers from "../src/pages/Providers";
import { LanguageProvider } from "../src/i18n/provider";
import { clearClientResourceStoresForTests } from "../src/client-resource";
+import { clearProviderQuotaStoresForTests } from "../src/provider-quota-store";
/**
* Quota revalidation policy.
@@ -30,6 +31,9 @@ const PROVIDERS = ["anthropic", "cursor", "kimi"];
beforeEach(() => {
previousGlobals = Object.fromEntries(globals.map(k => [k, Reflect.get(globalThis, k)])) as typeof previousGlobals;
clearClientResourceStoresForTests();
+ // Quota lives in the shared provider-quota store now; a sibling case's healthy entry
+ // would short-circuit the cold read this file pins (ensure() is a no-op on healthy data).
+ clearProviderQuotaStoresForTests();
testWindow = new Window({ url: "http://localhost/#providers" });
Object.defineProperty(testWindow.navigator, "language", { configurable: true, value: "en-US" });
Object.defineProperties(globalThis, {
@@ -99,6 +103,7 @@ afterEach(async () => {
root = null;
}
clearClientResourceStoresForTests();
+ clearProviderQuotaStoresForTests();
for (const key of globals) {
Object.defineProperty(globalThis, key, { configurable: true, value: previousGlobals[key] });
}
diff --git a/gui/tests/startup-usage-loading-race.test.tsx b/gui/tests/startup-usage-loading-race.test.tsx
index e12e53c3f3..8db10e562e 100644
--- a/gui/tests/startup-usage-loading-race.test.tsx
+++ b/gui/tests/startup-usage-loading-race.test.tsx
@@ -164,6 +164,7 @@ test("an aborted Usage fetch must not clear loading while its replacement is in
requests: 0, measuredRequests: 0, reportedRequests: 0, unreportedRequests: 0,
unsupportedRequests: 0, estimatedRequests: 0, inputTokens: 0, outputTokens: 0,
cachedInputTokens: 0, reasoningOutputTokens: 0, totalTokens: 0, coverageRatio: 1,
+ estimatedCostUsd: 0, pricedRequests: 0, unpricedRequests: 0, unmeteredRequests: 0,
};
const usage = (generatedAt: number) => ({
range: "7d", surface: "all", since: null, generatedAt,
diff --git a/gui/tests/subagents-busy-race.test.tsx b/gui/tests/subagents-busy-race.test.tsx
index 2acf07d87c..ffd622358c 100644
--- a/gui/tests/subagents-busy-race.test.tsx
+++ b/gui/tests/subagents-busy-race.test.tsx
@@ -113,7 +113,7 @@ async function mount() {
function addToggle(id: string): HTMLButtonElement {
const row = Array.from(container.querySelectorAll("button")).find((b) =>
- (b.getAttribute("aria-label") ?? "").includes(`Add ${id} to active roster`),
+ (b.getAttribute("aria-label") ?? "").includes(`Add ${id} to configured roster`),
);
if (!row) throw new Error(`add toggle not found: ${id}`);
return row as unknown as HTMLButtonElement;
diff --git a/gui/tests/subagents-classic.test.ts b/gui/tests/subagents-classic.test.ts
index ae13927c52..3da8391ce3 100644
--- a/gui/tests/subagents-classic.test.ts
+++ b/gui/tests/subagents-classic.test.ts
@@ -1,7 +1,7 @@
import { expect, test } from "bun:test";
/**
- * Subagents ships one command-center layout (active roster + library + policy).
+ * Subagents ships one command-center layout (configured roster + library + policy).
* Classic stacked cards and the view-mode toggle are gone.
*/
diff --git a/gui/tests/subagents-classic.test.tsx b/gui/tests/subagents-classic.test.tsx
index 9aab859136..76fea27727 100644
--- a/gui/tests/subagents-classic.test.tsx
+++ b/gui/tests/subagents-classic.test.tsx
@@ -159,46 +159,46 @@ async function mount() {
/** Library add/remove toggles are labelled from sub.workspace.addToFeatured / removeFromFeatured. */
function addToggle(id: string): HTMLButtonElement {
const row = Array.from(container.querySelectorAll("button"))
- .find((b) => (b.getAttribute("aria-label") ?? "").includes(`Add ${id} to active roster`));
+ .find((b) => (b.getAttribute("aria-label") ?? "").includes(`Add ${id} to configured roster`));
if (!row) throw new Error(`add toggle not found: ${id}`);
return row as unknown as HTMLButtonElement;
}
-/** Active-roster remove only (the library also exposes remove toggles). */
+/** Configured-roster remove only (the library also exposes remove toggles). */
function removeButtons(): HTMLButtonElement[] {
return Array.from(container.querySelectorAll(".swi-roster-actions button")).filter((b) =>
/^Remove /.test(b.getAttribute("aria-label") ?? "")) as unknown as HTMLButtonElement[];
}
-test("renders one active roster, one agent library, and one run-policy card", async () => {
+test("renders one configured roster, one agent library, and one run-policy card", async () => {
await mount();
expect(container.querySelector(".subagents-workspace-shell")).toBeTruthy();
expect(container.querySelectorAll(".subagents-command-card").length).toBe(3);
const headings = Array.from(container.querySelectorAll(".swi-card-title"))
.map(node => node.textContent?.trim());
- expect(headings).toEqual(["Active Roster", "Agent Library", "Run Policy"]);
+ expect(headings).toEqual(["Configured Roster", "Agent Library", "Run Policy"]);
expect(container.textContent).toContain("Use roster as worker guidance");
- expect(container.textContent).toContain("No preferred model — Codex chooses from roster");
+ expect(container.textContent).toContain("No preferred model");
});
test("shows the encrypted V2 compatibility notice for base and V2, but not classic V1", async () => {
policyMode = "v2";
await mount();
- expect(container.textContent).toContain("external providers cannot read (#92)");
+ expect(container.textContent).toContain("V2 tasks are encrypted; external providers cannot read them");
const v2Root = root!;
await act(async () => { v2Root.unmount(); });
root = null;
policyMode = "default";
await mount();
- expect(container.textContent).toContain("external providers cannot read (#92)");
+ expect(container.textContent).toContain("V2 tasks are encrypted; external providers cannot read them");
const currentRoot = root!;
await act(async () => { currentRoot.unmount(); });
root = null;
policyMode = "v1";
await mount();
- expect(container.textContent).not.toContain("external providers cannot read (#92)");
+ expect(container.textContent).not.toContain("V2 tasks are encrypted");
});
test("shows the plaintext privacy notice when Codex defaults may select V2", async () => {
@@ -206,7 +206,7 @@ test("shows the plaintext privacy notice when Codex defaults may select V2", asy
messageDelivery = "plaintext";
await mount();
expect(container.textContent).toContain("including messages to native workers");
- expect(container.textContent).toContain("V2 task-message delivery from this parent is plaintext");
+ expect(container.textContent).toContain("Task-message delivery from this parent is plaintext");
expect(container.textContent).toContain("does not require Apply");
});
diff --git a/gui/tests/usage-layout.test.ts b/gui/tests/usage-layout.test.ts
index f3e77f2880..8e9043db0b 100644
--- a/gui/tests/usage-layout.test.ts
+++ b/gui/tests/usage-layout.test.ts
@@ -103,6 +103,10 @@ test("Usage renders Available history and a persistent qualification when histor
reasoningOutputTokens: 0,
totalTokens: 0,
coverageRatio: 1,
+ estimatedCostUsd: 0,
+ pricedRequests: 0,
+ unpricedRequests: 0,
+ unmeteredRequests: 0,
},
days: [],
models: [],
diff --git a/gui/tests/usage-report-store-rehydrate.test.ts b/gui/tests/usage-report-store-rehydrate.test.ts
new file mode 100644
index 0000000000..6648f08849
--- /dev/null
+++ b/gui/tests/usage-report-store-rehydrate.test.ts
@@ -0,0 +1,160 @@
+import { afterAll, afterEach, expect, test } from "bun:test";
+import { Window } from "happy-dom";
+
+/**
+ * Rehydration contract for the usage-report store: sessionStorage seeds become store
+ * entries marked `seedNeedsRevalidate`, and the first subscriber quiet-revalidates
+ * (exactly one fetch) without ever blanking the seeded data.
+ *
+ * The store module may already be loaded by another test file (Bun shares the module
+ * registry within one run), so the seed is written and `rehydrateUsageReportForTests`
+ * is invoked explicitly instead of relying on creation-time hydration.
+ */
+const testWindow = new Window({ url: "http://localhost/" });
+const originalFetch = globalThis.fetch;
+const INSTALLED_GLOBALS = ["document", "window", "navigator", "sessionStorage", "IS_REACT_ACT_ENVIRONMENT"] as const;
+const previousGlobals = Object.fromEntries(
+ INSTALLED_GLOBALS.map(key => [key, Reflect.get(globalThis, key)]),
+) as Record<(typeof INSTALLED_GLOBALS)[number], unknown>;
+Object.defineProperties(globalThis, {
+ document: { configurable: true, value: testWindow.document },
+ window: { configurable: true, value: testWindow },
+ navigator: { configurable: true, value: testWindow.navigator },
+ sessionStorage: { configurable: true, value: testWindow.sessionStorage },
+});
+(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+
+const SEED_KEY = "http://rehydrate:30d:all";
+const seedReport = {
+ range: "30d",
+ surface: "all",
+ since: null,
+ generatedAt: 1,
+ summary: {
+ requests: 7,
+ measuredRequests: 7,
+ reportedRequests: 7,
+ unreportedRequests: 0,
+ unsupportedRequests: 0,
+ estimatedRequests: 0,
+ inputTokens: 0,
+ outputTokens: 0,
+ cachedInputTokens: 0,
+ reasoningOutputTokens: 0,
+ totalTokens: 500,
+ coverageRatio: 1,
+ estimatedCostUsd: 0.25,
+ pricedRequests: 7,
+ unpricedRequests: 0,
+ unmeteredRequests: 0,
+ },
+ days: [],
+ models: [],
+ providers: [],
+ historyTruncated: false,
+ truncatedPrefixBytes: 0,
+ entriesTruncated: false,
+ entriesDropped: 0,
+} as const;
+
+testWindow.sessionStorage.setItem(
+ "ccx.usage-reports.v1",
+ JSON.stringify({
+ state: {
+ entries: {
+ [SEED_KEY]: { data: seedReport, persistedAt: 1234 },
+ },
+ },
+ version: 0,
+ }),
+);
+
+const {
+ clearUsageReportStoresForTests,
+ rehydrateUsageReportForTests,
+ useUsageReportStore,
+} = await import("../src/usage-report-store");
+
+afterEach(() => {
+ globalThis.fetch = originalFetch;
+ clearUsageReportStoresForTests();
+});
+
+afterAll(() => {
+ testWindow.close();
+ for (const key of INSTALLED_GLOBALS) {
+ Object.defineProperty(globalThis, key, { configurable: true, value: previousGlobals[key] });
+ }
+});
+
+// Re-run persist rehydration against the seeded sessionStorage above.
+rehydrateUsageReportForTests();
+
+async function flush(times = 3): Promise {
+ for (let i = 0; i < times; i++) {
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+}
+
+function reseed(): void {
+ testWindow.sessionStorage.setItem(
+ "ccx.usage-reports.v1",
+ JSON.stringify({
+ state: {
+ entries: {
+ [SEED_KEY]: { data: seedReport, persistedAt: 1234 },
+ },
+ },
+ version: 0,
+ }),
+ );
+ rehydrateUsageReportForTests();
+}
+
+test("rehydrated seed is available with seedNeedsRevalidate set", () => {
+ const entry = useUsageReportStore.getState().entries[SEED_KEY];
+ expect(entry?.data).toEqual(seedReport);
+ expect(entry?.persistedAt).toBe(1234);
+ expect(entry?.seedNeedsRevalidate).toBe(true);
+});
+
+test("first subscriber quiet-revalidates the seed with exactly one fetch", async () => {
+ clearUsageReportStoresForTests();
+ // Re-seed after clearing so this test owns a fresh seed.
+ reseed();
+ let calls = 0;
+ globalThis.fetch = (async () => {
+ calls += 1;
+ return Response.json({ ...seedReport, generatedAt: 2 });
+ }) as typeof fetch;
+
+ useUsageReportStore.getState().ensure(SEED_KEY, "http://rehydrate", "30d", "all");
+ // A second concurrent subscriber must dedupe onto the same revalidation.
+ useUsageReportStore.getState().ensure(SEED_KEY, "http://rehydrate", "30d", "all");
+ await flush();
+ expect(calls).toBe(1);
+ const entry = useUsageReportStore.getState().entries[SEED_KEY];
+ expect(entry?.data?.generatedAt).toBe(2);
+ expect(entry?.seedNeedsRevalidate).toBe(false);
+ expect(entry?.hasSucceeded).toBe(true);
+});
+
+test("a failed refresh keeps the seeded last-good data", async () => {
+ clearUsageReportStoresForTests();
+ reseed();
+ const entry = useUsageReportStore.getState().entries[SEED_KEY];
+ expect(entry?.data).toEqual(seedReport);
+ // A rehydrated seed reads as last-known-good: hasSucceeded true, retry armed.
+ expect(entry?.hasSucceeded).toBe(true);
+ expect(entry?.seedNeedsRevalidate).toBe(true);
+ globalThis.fetch = (async () => new Response("boom", { status: 503 })) as typeof fetch;
+ useUsageReportStore.getState().refresh(SEED_KEY, "http://rehydrate", "30d", "all");
+ await flush();
+ const after = useUsageReportStore.getState().entries[SEED_KEY];
+ expect(after?.data).toEqual(seedReport);
+ expect(after?.lastAttemptOk).toBe(false);
+ // The failed revalidation restored the retry flag for the next subscriber and kept
+ // the seed readable as succeeded (healthy display, not a never-succeeded attempt).
+ expect(after?.seedNeedsRevalidate).toBe(true);
+ expect(after?.hasSucceeded).toBe(true);
+});
diff --git a/gui/tests/usage-report-store.test.ts b/gui/tests/usage-report-store.test.ts
new file mode 100644
index 0000000000..37a98f3480
--- /dev/null
+++ b/gui/tests/usage-report-store.test.ts
@@ -0,0 +1,208 @@
+import { afterAll, afterEach, beforeEach, expect, test } from "bun:test";
+import { Window } from "happy-dom";
+
+/**
+ * Store-level contract for the usage-report domain store: key derivation, singleflight
+ * dedupe, AbortController cancellation, and persistence of validated reports only.
+ *
+ * sessionStorage is installed BEFORE the store module is imported so zustand persist
+ * captures it (Bun isolates module registries per test file).
+ */
+const testWindow = new Window({ url: "http://localhost/" });
+const originalFetch = globalThis.fetch;
+const INSTALLED_GLOBALS = ["document", "window", "navigator", "sessionStorage", "IS_REACT_ACT_ENVIRONMENT"] as const;
+const previousGlobals = Object.fromEntries(
+ INSTALLED_GLOBALS.map(key => [key, Reflect.get(globalThis, key)]),
+) as Record<(typeof INSTALLED_GLOBALS)[number], unknown>;
+Object.defineProperties(globalThis, {
+ document: { configurable: true, value: testWindow.document },
+ window: { configurable: true, value: testWindow },
+ navigator: { configurable: true, value: testWindow.navigator },
+ sessionStorage: { configurable: true, value: testWindow.sessionStorage },
+});
+(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+
+const {
+ clearUsageReportStoresForTests,
+ usageReportKey,
+ useUsageReportStore,
+ USAGE_REPORT_STORAGE_NAME,
+} = await import("../src/usage-report-store");
+
+type UsageReport = import("../src/usage-report-validation").UsageReport;
+
+function validReport(overrides: Partial = {}): UsageReport {
+ return {
+ range: "30d",
+ surface: "all",
+ since: null,
+ generatedAt: 1,
+ summary: {
+ requests: 0,
+ measuredRequests: 0,
+ reportedRequests: 0,
+ unreportedRequests: 0,
+ unsupportedRequests: 0,
+ estimatedRequests: 0,
+ inputTokens: 0,
+ outputTokens: 0,
+ cachedInputTokens: 0,
+ reasoningOutputTokens: 0,
+ totalTokens: 0,
+ coverageRatio: 1,
+ estimatedCostUsd: 0,
+ pricedRequests: 0,
+ unpricedRequests: 0,
+ unmeteredRequests: 0,
+ },
+ days: [],
+ models: [],
+ providers: [],
+ historyTruncated: false,
+ truncatedPrefixBytes: 0,
+ entriesTruncated: false,
+ entriesDropped: 0,
+ ...overrides,
+ };
+}
+
+beforeEach(() => {
+ clearUsageReportStoresForTests();
+ testWindow.sessionStorage.clear();
+});
+
+afterEach(() => {
+ clearUsageReportStoresForTests();
+ globalThis.fetch = originalFetch;
+});
+
+afterAll(() => {
+ // The default bun test runner shares one global scope across files: restore every
+ // global installed at module load so sibling files never inherit a closed window.
+ testWindow.close();
+ for (const key of INSTALLED_GLOBALS) {
+ Object.defineProperty(globalThis, key, { configurable: true, value: previousGlobals[key] });
+ }
+});
+
+async function flush(times = 3): Promise {
+ for (let i = 0; i < times; i++) {
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+}
+
+test("usage report key derives from apiBase/range/surface", () => {
+ expect(usageReportKey("http://localhost:1234", "30d", "all")).toBe("http://localhost:1234:30d:all");
+ expect(usageReportKey("http://x", "7d", "codex")).toBe("http://x:7d:codex");
+});
+
+test("singleflight dedupes concurrent subscribers into one fetch", async () => {
+ let calls = 0;
+ const gates: Array<() => void> = [];
+ globalThis.fetch = (async () => {
+ calls += 1;
+ await new Promise(resolve => gates.push(resolve));
+ return Response.json(validReport());
+ }) as typeof fetch;
+
+ const key = usageReportKey("http://sf", "30d", "all");
+ useUsageReportStore.getState().ensure(key, "http://sf", "30d", "all");
+ useUsageReportStore.getState().ensure(key, "http://sf", "30d", "all");
+ useUsageReportStore.getState().ensure(key, "http://sf", "30d", "all");
+ expect(calls).toBe(1);
+
+ gates[0]!();
+ await flush();
+ expect(useUsageReportStore.getState().entries[key]?.data).toBeDefined();
+ expect(useUsageReportStore.getState().entries[key]?.hasSucceeded).toBe(true);
+});
+
+test("refresh aborts the previous in-flight request for the same key", async () => {
+ let aborted = false;
+ const gates: Array<() => void> = [];
+ globalThis.fetch = (async (_input, init) => {
+ init?.signal?.addEventListener("abort", () => { aborted = true; });
+ await new Promise(resolve => gates.push(resolve));
+ if (init?.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
+ return Response.json(validReport());
+ }) as typeof fetch;
+
+ const key = usageReportKey("http://abort", "30d", "all");
+ useUsageReportStore.getState().ensure(key, "http://abort", "30d", "all");
+ useUsageReportStore.getState().refresh(key, "http://abort", "30d", "all");
+ expect(aborted).toBe(true);
+
+ gates[0]!();
+ gates[1]!();
+ await flush();
+ expect(useUsageReportStore.getState().entries[key]?.data).toBeDefined();
+});
+
+test("persists only validated successful reports with a timestamp", async () => {
+ // An error envelope must never reach the persisted slice.
+ globalThis.fetch = (async () =>
+ Response.json({ error: "read_failed", range: "30d", surface: "all" })) as typeof fetch;
+ const key = usageReportKey("http://persist", "30d", "all");
+ useUsageReportStore.getState().ensure(key, "http://persist", "30d", "all");
+ await flush();
+ let parsed = JSON.parse(testWindow.sessionStorage.getItem(USAGE_REPORT_STORAGE_NAME) ?? "{}");
+ expect(parsed.state?.entries?.[key]).toBeUndefined();
+ expect(useUsageReportStore.getState().entries[key]?.data).toBeUndefined();
+
+ // A validated success is persisted as data + timestamp only.
+ globalThis.fetch = (async () => Response.json(validReport())) as typeof fetch;
+ useUsageReportStore.getState().refresh(key, "http://persist", "30d", "all");
+ await flush();
+ parsed = JSON.parse(testWindow.sessionStorage.getItem(USAGE_REPORT_STORAGE_NAME) ?? "{}");
+ const persisted = parsed.state?.entries?.[key];
+ expect(persisted?.data).toBeDefined();
+ expect(typeof persisted?.persistedAt).toBe("number");
+ // Never errors or in-flight flags in the persisted slice.
+ expect(persisted).not.toHaveProperty("error");
+ expect(persisted).not.toHaveProperty("loading");
+ expect(persisted).not.toHaveProperty("refreshing");
+});
+
+test("a malformed report element is rejected and never persisted", async () => {
+ const key = usageReportKey("http://malformed", "30d", "all");
+ const body = {
+ range: "30d",
+ surface: "all",
+ since: null,
+ generatedAt: 1,
+ summary: {
+ requests: 1,
+ measuredRequests: 1,
+ reportedRequests: 1,
+ unreportedRequests: 0,
+ unsupportedRequests: 0,
+ estimatedRequests: 0,
+ inputTokens: 0,
+ outputTokens: 0,
+ cachedInputTokens: 0,
+ reasoningOutputTokens: 0,
+ totalTokens: 10,
+ coverageRatio: 1,
+ estimatedCostUsd: 0,
+ pricedRequests: 1,
+ unpricedRequests: 0,
+ unmeteredRequests: 0,
+ },
+ days: [],
+ // A malformed model row: totalTokens is not a finite number.
+ models: [{ provider: "openai", model: "gpt-5", totalTokens: "many" }],
+ providers: [],
+ historyTruncated: false,
+ truncatedPrefixBytes: 0,
+ entriesTruncated: false,
+ entriesDropped: 0,
+ };
+ globalThis.fetch = (async () => Response.json(body)) as typeof fetch;
+ useUsageReportStore.getState().ensure(key, "http://malformed", "30d", "all");
+ await flush();
+ const entry = useUsageReportStore.getState().entries[key];
+ expect(entry?.data).toBeUndefined();
+ expect(entry?.hasSucceeded).toBe(false);
+ const parsed = JSON.parse(testWindow.sessionStorage.getItem(USAGE_REPORT_STORAGE_NAME) ?? "{}");
+ expect(parsed.state?.entries?.[key]).toBeUndefined();
+});
diff --git a/gui/tests/usage-report-validation.test.ts b/gui/tests/usage-report-validation.test.ts
new file mode 100644
index 0000000000..6b82bd4ed8
--- /dev/null
+++ b/gui/tests/usage-report-validation.test.ts
@@ -0,0 +1,120 @@
+import { expect, test } from "bun:test";
+import {
+ parseUsageReport,
+ UsageReportValidationError,
+ type UsageReport,
+} from "../src/usage-report-validation";
+
+/** Element-level validation: days/models/providers rows must be well-formed, not cast. */
+
+function validReport(): UsageReport {
+ return {
+ range: "30d",
+ surface: "all",
+ since: null,
+ generatedAt: 1,
+ summary: {
+ requests: 3,
+ measuredRequests: 3,
+ reportedRequests: 3,
+ unreportedRequests: 0,
+ unsupportedRequests: 0,
+ estimatedRequests: 0,
+ inputTokens: 100,
+ outputTokens: 50,
+ cachedInputTokens: 0,
+ reasoningOutputTokens: 0,
+ totalTokens: 150,
+ coverageRatio: 1,
+ estimatedCostUsd: 0.25,
+ pricedRequests: 3,
+ unpricedRequests: 0,
+ unmeteredRequests: 0,
+ },
+ days: [{
+ date: "2026-08-01",
+ requests: 3,
+ measuredRequests: 3,
+ reportedRequests: 3,
+ totalTokens: 150,
+ models: [{ model: "gpt-5", provider: "openai", requests: 3, totalTokens: 150 }],
+ }],
+ models: [{
+ provider: "openai",
+ model: "gpt-5",
+ requests: 3,
+ measuredRequests: 3,
+ reportedRequests: 3,
+ estimatedRequests: 0,
+ totalTokens: 150,
+ inputTokens: 100,
+ outputTokens: 50,
+ shareRatio: 1,
+ }],
+ providers: [{
+ provider: "openai",
+ requests: 3,
+ measuredRequests: 3,
+ reportedRequests: 3,
+ estimatedRequests: 0,
+ totalTokens: 150,
+ shareRatio: 1,
+ }],
+ historyTruncated: false,
+ truncatedPrefixBytes: 0,
+ entriesTruncated: false,
+ entriesDropped: 0,
+ };
+}
+
+function mutate(report: UsageReport, mutate: (draft: Record) => void): unknown {
+ const draft = JSON.parse(JSON.stringify(report)) as Record;
+ mutate(draft);
+ return draft;
+}
+
+test("a well-formed report passes element validation", () => {
+ const parsed = parseUsageReport(validReport());
+ expect(parsed.days).toHaveLength(1);
+ expect(parsed.models[0]?.totalTokens).toBe(150);
+ expect(parsed.providers[0]?.shareRatio).toBe(1);
+});
+
+test("a malformed model row rejects the whole report", () => {
+ const body = mutate(validReport(), draft => {
+ ((draft.models as Array>)[0]!).totalTokens = "many";
+ });
+ expect(() => parseUsageReport(body)).toThrow(UsageReportValidationError);
+ expect(() => parseUsageReport(body)).toThrow(/models\[0\]\.totalTokens/);
+});
+
+test("a malformed day row rejects the whole report", () => {
+ const body = mutate(validReport(), draft => {
+ ((draft.days as Array>)[0]!).requests = null;
+ });
+ expect(() => parseUsageReport(body)).toThrow(UsageReportValidationError);
+ expect(() => parseUsageReport(body)).toThrow(/days\[0\]\.requests/);
+});
+
+test("a malformed day-model row rejects the whole report", () => {
+ const body = mutate(validReport(), draft => {
+ ((draft.days as Array>)[0]!.models as Array>)[0]!.provider = 42;
+ });
+ expect(() => parseUsageReport(body)).toThrow(UsageReportValidationError);
+ expect(() => parseUsageReport(body)).toThrow(/day models\[0\]\.provider/);
+});
+
+test("a malformed provider row rejects the whole report", () => {
+ const body = mutate(validReport(), draft => {
+ ((draft.providers as Array>)[0]!).shareRatio = undefined;
+ });
+ expect(() => parseUsageReport(body)).toThrow(UsageReportValidationError);
+ expect(() => parseUsageReport(body)).toThrow(/providers\[0\]\.shareRatio/);
+});
+
+test("a non-array collection rejects the whole report", () => {
+ const body = mutate(validReport(), draft => {
+ draft.models = "not-an-array";
+ });
+ expect(() => parseUsageReport(body)).toThrow(UsageReportValidationError);
+});
diff --git a/gui/tests/usage-validation.test.tsx b/gui/tests/usage-validation.test.tsx
new file mode 100644
index 0000000000..c9caad8ab2
--- /dev/null
+++ b/gui/tests/usage-validation.test.tsx
@@ -0,0 +1,234 @@
+import { afterEach, beforeEach, expect, test } from "bun:test";
+import { Window } from "happy-dom";
+import { act } from "react";
+import type { Root } from "react-dom/client";
+import { LanguageProvider } from "../src/i18n/provider";
+import { clearClientResourceStoresForTests } from "../src/client-resource";
+import {
+ clearUsageReportStoresForTests,
+ seedUsageReportForTests,
+ usageReportKey,
+ useUsageReportStore,
+} from "../src/usage-report-store";
+import Usage from "../src/pages/Usage";
+import type { UsageReport } from "../src/usage-report-validation";
+
+/**
+ * Phase 1b contract, now enforced through the usage-report store: only validated
+ * successful reports reach the store (never error envelopes), a defined zero cost
+ * renders $0.00 (only a genuinely missing legacy field shows "Unavailable"), a cold
+ * failure shows the failed-cold Notice with retry, and a failed refresh keeps
+ * last-known-good data with the stale/error banner.
+ */
+
+const globals = ["document", "window", "navigator", "localStorage", "sessionStorage", "IS_REACT_ACT_ENVIRONMENT", "ResizeObserver"] as const;
+let previousGlobals: Record<(typeof globals)[number], unknown>;
+let testWindow: Window;
+const originalFetch = globalThis.fetch;
+
+function summary(overrides: Partial = {}) {
+ return {
+ requests: 0,
+ measuredRequests: 0,
+ reportedRequests: 0,
+ unreportedRequests: 0,
+ unsupportedRequests: 0,
+ estimatedRequests: 0,
+ inputTokens: 0,
+ outputTokens: 0,
+ cachedInputTokens: 0,
+ reasoningOutputTokens: 0,
+ totalTokens: 0,
+ coverageRatio: 1,
+ estimatedCostUsd: 0,
+ pricedRequests: 0,
+ unpricedRequests: 0,
+ unmeteredRequests: 0,
+ ...overrides,
+ };
+}
+
+function validReport(overrides: Partial = {}): UsageReport {
+ return {
+ range: "30d",
+ surface: "all",
+ since: null,
+ generatedAt: 1,
+ summary: summary(),
+ days: [],
+ models: [],
+ providers: [],
+ historyTruncated: false,
+ truncatedPrefixBytes: 0,
+ entriesTruncated: false,
+ entriesDropped: 0,
+ ...overrides,
+ };
+}
+
+beforeEach(() => {
+ previousGlobals = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previousGlobals;
+ clearClientResourceStoresForTests();
+ clearUsageReportStoresForTests();
+ testWindow = new Window({ url: "http://localhost/" });
+ Object.defineProperties(globalThis, {
+ document: { configurable: true, value: testWindow.document },
+ window: { configurable: true, value: testWindow },
+ navigator: { configurable: true, value: testWindow.navigator },
+ localStorage: { configurable: true, value: testWindow.localStorage },
+ sessionStorage: { configurable: true, value: testWindow.sessionStorage },
+ });
+ class ResizeObserverStub {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ }
+ Object.defineProperty(globalThis, "ResizeObserver", { configurable: true, value: ResizeObserverStub });
+ (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+ testWindow.sessionStorage.clear();
+});
+
+afterEach(() => {
+ globalThis.fetch = originalFetch;
+ clearClientResourceStoresForTests();
+ clearUsageReportStoresForTests();
+ testWindow.close();
+ for (const key of globals) {
+ Object.defineProperty(globalThis, key, { configurable: true, value: previousGlobals[key] });
+ }
+});
+
+async function renderUsage(apiBase: string): Promise<{ container: HTMLElement; root: Root }> {
+ const { createRoot } = await import("react-dom/client");
+ const container = document.createElement("div");
+ document.body.append(container);
+ const root = createRoot(container);
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ return { container, root };
+}
+
+async function waitFor(predicate: () => boolean, timeoutMs = 1000): Promise {
+ const start = Date.now();
+ while (!predicate()) {
+ if (Date.now() - start > timeoutMs) throw new Error("waitFor timed out");
+ await act(async () => {
+ await new Promise(resolve => testWindow.setTimeout(resolve, 10));
+ });
+ }
+}
+
+test("cost row renders $0.00 for a defined zero", async () => {
+ globalThis.fetch = (async () =>
+ Response.json(validReport({ summary: summary({ requests: 3, estimatedCostUsd: 0 }) }))) as typeof fetch;
+ const { container, root } = await renderUsage("http://usage-zero");
+ try {
+ await waitFor(() => (container.textContent ?? "").includes("$0.00"));
+ expect(container.textContent).toContain("$0.00");
+ // A defined zero must never fall back to the ~$0.0000 estimate rendering.
+ expect(container.textContent).not.toContain("~");
+ expect(container.textContent).not.toContain("Unavailable");
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("legacy undefined cost field renders Unavailable", async () => {
+ const key = usageReportKey("http://usage-legacy", "30d", "all");
+ const legacy = validReport({ summary: summary({ requests: 3 }) });
+ // Simulate a pre-validation seed from an older server: no cost fields on the summary.
+ const { estimatedCostUsd: _cost, pricedRequests: _priced, unpricedRequests: _unpriced, unmeteredRequests: _unmetered, ...legacySummary } = legacy.summary;
+ seedUsageReportForTests(key, { ...legacy, summary: legacySummary } as unknown as UsageReport);
+ // The quiet revalidation also answers with the legacy shape, so it is rejected and the
+ // seeded last-known-good payload (with undefined cost) stays on screen.
+ globalThis.fetch = (async () => Response.json({ ...legacy, summary: legacySummary })) as typeof fetch;
+
+ const { container, root } = await renderUsage("http://usage-legacy");
+ try {
+ await waitFor(() => (container.textContent ?? "").includes("Unavailable"));
+ expect(container.textContent).toContain("Unavailable");
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("a 200 response with an error envelope is rejected and never cached", async () => {
+ const key = usageReportKey("http://usage-error-envelope", "30d", "all");
+ globalThis.fetch = (async () =>
+ Response.json({ error: "read_failed", range: "30d", surface: "all" })) as typeof fetch;
+
+ const { container, root } = await renderUsage("http://usage-error-envelope");
+ try {
+ await waitFor(() => (container.textContent ?? "").includes("Retry"));
+ expect(container.textContent).toContain("Retry");
+ const entry = useUsageReportStore.getState().entries[key];
+ expect(entry?.data).toBeUndefined();
+ expect(entry?.hasSucceeded).toBe(false);
+ expect(entry?.lastAttemptOk).toBe(false);
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("401 cold failure shows the failed-cold Notice with retry", async () => {
+ globalThis.fetch = (async () => new Response("unauthorized", { status: 401 })) as typeof fetch;
+
+ const { container, root } = await renderUsage("http://usage-401");
+ try {
+ await waitFor(() => (container.textContent ?? "").includes("Retry"));
+ expect(container.textContent).toContain("Retry");
+ expect(container.querySelector("button")).not.toBeNull();
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("an HTTP 503 cold failure shows the failed-cold Notice with retry and caches nothing", async () => {
+ // The server's genuine read-failure contract answers 503 { error: "read_failed", ... }.
+ globalThis.fetch = (async () => new Response("read_failed", { status: 503 })) as typeof fetch;
+ const key = usageReportKey("http://usage-503", "30d", "all");
+
+ const { container, root } = await renderUsage("http://usage-503");
+ try {
+ await waitFor(() => (container.textContent ?? "").includes("Retry"));
+ expect(container.textContent).toContain("Retry");
+ const entry = useUsageReportStore.getState().entries[key];
+ expect(entry?.data).toBeUndefined();
+ expect(entry?.hasSucceeded).toBe(false);
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
+
+test("refresh failure retains last-good data and shows the stale/error banner", async () => {
+ const key = usageReportKey("http://usage-stale", "30d", "all");
+ const good = validReport({ summary: summary({ requests: 42, totalTokens: 9000, estimatedCostUsd: 1.25 }) });
+ // Last-known-good payload is seeded like a rehydrated session seed; the quiet
+ // revalidation fails and must not wipe it.
+ seedUsageReportForTests(key, good);
+ globalThis.fetch = (async () => new Response("boom", { status: 500 })) as typeof fetch;
+
+ const { container, root } = await renderUsage("http://usage-stale");
+ try {
+ await waitFor(() => (container.textContent ?? "").includes("42"));
+ expect(container.textContent).toContain("42");
+ // The stale/error banner appears next to the retained data.
+ await waitFor(() => (container.textContent ?? "").includes("Could not load usage data"));
+ expect(container.textContent).toContain("Could not load usage data");
+ // The failed refresh must not have wiped the store entry.
+ expect(useUsageReportStore.getState().entries[key]?.data).not.toBeUndefined();
+ } finally {
+ await act(async () => { root.unmount(); });
+ container.remove();
+ }
+});
diff --git a/src/adapters/anthropic.ts b/src/adapters/anthropic.ts
index caff67ad3d..09b56304bb 100644
--- a/src/adapters/anthropic.ts
+++ b/src/adapters/anthropic.ts
@@ -427,9 +427,10 @@ function reasoningBudget(effort: string): number {
case "low": return 4096;
case "high": return 16384;
case "xhigh": return 24576;
- case "max": return 32000;
- case "medium":
- default: return 8192;
+ case "max":
+ case "ultra": return 32000; // codex-rs maps ultra -> max before the wire; mirror it.
+ case "medium": return 8192;
+ default: return 16384; // unknown efforts clamp to "high" on the wire; budget must match.
}
}
@@ -510,9 +511,25 @@ function supportsExplicitThinkingDisable(modelId: string): boolean {
return meetsFamilyMinimum(modelId, EXPLICIT_THINKING_DISABLE_FAMILY_MINIMUMS);
}
-/** `output_config.effort` accepts low|medium|high|xhigh|max — "minimal" is rejected with a 400. */
+/**
+ * `output_config.effort` accepts low|medium|high|xhigh|max — "minimal" and anything
+ * above the ladder (e.g. "ultra") are rejected with a 400 ("Invalid reasoning effort").
+ * "ultra" mirrors the codex-rs boundary (ultra -> max); unknown values clamp to "high"
+ * so the proxy never forwards an invalid effort to Anthropic.
+ */
function adaptiveEffort(effort: string): string {
- return effort === "minimal" ? "low" : effort;
+ switch (effort) {
+ case "minimal": return "low";
+ case "ultra": return "max";
+ case "low":
+ case "medium":
+ case "high":
+ case "xhigh":
+ case "max":
+ return effort;
+ default:
+ return "high";
+ }
}
function usageFromAnthropic(usage: Record | undefined): CodexCommanderUsage | undefined {
diff --git a/src/providers/derive.ts b/src/providers/derive.ts
index 7d97ba2a23..b66e340703 100644
--- a/src/providers/derive.ts
+++ b/src/providers/derive.ts
@@ -89,6 +89,24 @@ function cloneRecordOfArrays(input: Record): Record [key, [...value]]));
}
+/**
+ * Per-key merge of record-of-arrays metadata: the seed (registry) fills keys missing from
+ * the saved map, while saved entries always win per key. Mirrors the request-time merge in
+ * router.ts so the catalog advertises the same ladder the wire clamps to — a partially
+ * saved map (e.g. an older grok-4.5-only ladder) still picks up new registry tiers.
+ */
+function mergeRecordOfArrays(
+ seed?: Record,
+ saved?: Record,
+): Record | undefined {
+ if (!seed) return saved ? { ...saved } : undefined;
+ const out: Record = { ...(saved ?? {}) };
+ for (const [key, value] of Object.entries(seed)) {
+ if (out[key] === undefined) out[key] = [...value];
+ }
+ return out;
+}
+
/**
* Fill registry defaults BENEATH the user's per-model entries.
*
@@ -273,7 +291,9 @@ export function enrichProviderFromRegistry(name: string, prov: CodexCommanderPro
prov.chatCompletionTokenField = seed.chatCompletionTokenField;
}
if (!prov.reasoningEfforts && seed.reasoningEfforts) prov.reasoningEfforts = [...seed.reasoningEfforts];
- if (!prov.modelReasoningEfforts && seed.modelReasoningEfforts) prov.modelReasoningEfforts = cloneRecordOfArrays(seed.modelReasoningEfforts);
+ if (seed.modelReasoningEfforts) {
+ prov.modelReasoningEfforts = mergeRecordOfArrays(seed.modelReasoningEfforts, prov.modelReasoningEfforts);
+ }
if (!prov.modelDefaultReasoningEfforts && seed.modelDefaultReasoningEfforts) prov.modelDefaultReasoningEfforts = { ...seed.modelDefaultReasoningEfforts };
if (prov.reasoningContentMode === undefined && seed.reasoningContentMode !== undefined) prov.reasoningContentMode = seed.reasoningContentMode;
if (seed.modelSupportsReasoningSummaries) {
diff --git a/src/providers/quota.ts b/src/providers/quota.ts
index 2e9264f57a..8208d3626e 100644
--- a/src/providers/quota.ts
+++ b/src/providers/quota.ts
@@ -24,6 +24,7 @@ import {
} from "../oauth/store";
import { antigravityUserAgent } from "../adapters/client-fingerprint";
import { apiKeyPoolEntryId } from "./api-keys";
+import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "./xai-transport";
import { getProviderRegistryEntry, providerCodexAccountMode } from "./registry";
import type { CodexCommanderConfig, CodexCommanderProviderConfig } from "../types";
import { readUsageSnapshotForManagement, usageTotalTokens, type PersistedUsageEntry } from "../usage/log";
@@ -537,6 +538,71 @@ function centsValue(value: unknown): number | undefined {
return rec ? toFiniteNumber(rec.val) : undefined;
}
+const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing";
+const XAI_CREDITS_URL = `${XAI_BILLING_URL}?format=credits`;
+
+/** Decode JWT payload `sub` for xAI weekly credits when the stored credential lacks accountId. */
+function xaiUserIdFromAccessToken(accessToken: string): string | undefined {
+ const parts = accessToken.split(".");
+ if (parts.length < 2 || !parts[1]) return undefined;
+ try {
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) as { sub?: unknown };
+ return typeof payload.sub === "string" && payload.sub.trim() ? payload.sub.trim() : undefined;
+ } catch {
+ return undefined;
+ }
+}
+
+/**
+ * Grok Build weekly credits envelope:
+ * `{ config: { creditUsagePercent?, currentPeriod: { type: "USAGE_PERIOD_TYPE_WEEKLY", end } } }`.
+ * Omitted percent is treated as 0 (proto3 default) — the same 0% the grok.com app shows
+ * for a fresh weekly pool. NOTE: a schema drift that stops emitting the field would also
+ * read as 0%, indistinguishable from a healthy fresh week; keep this heuristic documented.
+ */
+export function parseXaiCreditsResponse(value: unknown): { percent: number; resetAt?: number } | null {
+ const body = asRecord(value);
+ const config = asRecord(body?.config);
+ if (!config) return null;
+ const period = asRecord(config.currentPeriod);
+ if (!period || period.type !== "USAGE_PERIOD_TYPE_WEEKLY") return null;
+ const resetAt = normalizeResetAt(period.end);
+ if (resetAt === undefined) return null;
+ if (config.creditUsagePercent !== undefined) {
+ const percent = normalizePercent(config.creditUsagePercent);
+ if (percent === undefined) return null;
+ return { percent, resetAt };
+ }
+ return { percent: 0, resetAt };
+}
+
+async function fetchXaiWeeklyCredits(accessToken: string, userId: string): Promise {
+ try {
+ const response = await fetch(XAI_CREDITS_URL, {
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${accessToken}`,
+ [XAI_GROK_COMPATIBILITY.headers.tokenAuth]: "xai-grok-cli",
+ [XAI_GROK_COMPATIBILITY.headers.authenticateResponse]: "authenticate-response",
+ "x-userid": userId,
+ [XAI_GROK_COMPATIBILITY.headers.clientVersion]: XAI_GROK_CLIENT_VERSION,
+ },
+ redirect: "error",
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
+ });
+ if (!response.ok) return null;
+ const parsed = parseXaiCreditsResponse(await response.json().catch(() => null));
+ if (!parsed) return null;
+ return {
+ weeklyPercent: parsed.percent,
+ ...(parsed.resetAt !== undefined ? { weeklyResetAt: parsed.resetAt } : {}),
+ updatedAt: Date.now(),
+ };
+ } catch {
+ return null;
+ }
+}
+
async function fetchXaiQuota(provider: string): Promise {
let auth: Awaited>;
try {
@@ -546,8 +612,25 @@ async function fetchXaiQuota(provider: string): Promise (
- fetch("https://cli-chat-proxy.grok.com/v1/billing", {
+ fetch(XAI_BILLING_URL, {
headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
redirect: "error",
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -564,6 +647,19 @@ async function fetchXaiQuota(provider: string): Promise 0) {
+ const resetAt = normalizeResetAt(config.billingPeriodEnd);
+ const quota: ProviderQuota = {
+ // Unlocalized display label by precedent (e.g. a6api "Prepaid credits"); the GUI
+ // renders custom windows with their reset time when present.
+ customWindows: [{ label: "No reported cap", percent: 0, ...(resetAt !== undefined ? { resetAt } : {}) }],
+ updatedAt: Date.now(),
+ };
+ return report(provider, "xai:grok-billing", quota)
+ ?? quotaUnavailable("upstream_unavailable");
+ }
return quotaUnavailable("upstream_unavailable");
}
const percent = normalizePercent((usedCents / limitCents) * 100);
diff --git a/src/providers/registry.ts b/src/providers/registry.ts
index 3c830feeee..4dc8676ef0 100644
--- a/src/providers/registry.ts
+++ b/src/providers/registry.ts
@@ -959,8 +959,18 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
// (docs.x.ai prompt-caching/multi-turn, verified 2026-07-13).
// Models that never emit reasoning simply have no thinking parts to replay (no-op).
preserveReasoningContentModels: ["grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning"],
- // grok-4.5 reasoning is always-on with low/medium/high control (no off tier upstream).
- modelReasoningEfforts: { "grok-4.5": ["low", "medium", "high"] },
+ // grok reasoning is always-on with low/medium/high control (no off tier upstream);
+ // grok-4.6+ extends the ladder with xhigh per docs.x.ai. xAI rejects max/ultra outright
+ // (400 "Invalid reasoning effort"), so every grok reasoning model clamps ultra/max down
+ // to its real top tier. The provider default covers all other reasoning models (incl.
+ // live-discovered ones); per-model entries exist only to raise the ceiling where verified.
+ modelReasoningEfforts: {
+ "grok-4.5": ["low", "medium", "high"],
+ "grok-4.6": ["low", "medium", "high", "xhigh"],
+ },
+ // Provider default for live-discovered reasoning models: clamp to the verified xAI
+ // ladder unless a per-model entry raises it (noReasoningModels stay effort-free).
+ reasoningEfforts: ["low", "medium", "high"],
modelContextWindows: {
"grok-4.5": 500_000,
"grok-4.3": 1_000_000,
diff --git a/src/server/management/agent-settings-routes.ts b/src/server/management/agent-settings-routes.ts
index 7c9de8b758..0a97f68452 100644
--- a/src/server/management/agent-settings-routes.ts
+++ b/src/server/management/agent-settings-routes.ts
@@ -1,7 +1,4 @@
-import { randomUUID } from "node:crypto";
-import { readFileSync } from "node:fs";
-import type { CatalogModel } from "../../codex/catalog";
-import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
+import { catalogModelSlug, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
import {
catalogOnlyWorkerStateFromActivation,
captureCodexCatalogDesiredSnapshot,
@@ -12,60 +9,13 @@ import {
} from "../../codex/catalog-activation";
import type { CatalogConfigAuthoritySnapshot } from "../../codex/catalog-admission";
import { resetCodexAppServerCatalogStateCache } from "../../codex/app-server-processes";
-import {
- DEFAULT_SUBAGENT_MODELS,
- codexAutoStartEnabled,
- hasOwnProvider,
- isValidProviderName,
- loadConfig,
- multiAgentGuidanceEnabled,
- mutatePersistedConfig,
- providerBaseUrlConfigError,
- providerHeadersConfigError,
- saveConfigPreservingClaudeCode,
- subagentDefaultSyncEffective,
-} from "../../config";
-import {
- clearLoginState,
- getLoginStatus,
- isPublicOAuthProvider,
- listOAuthProviders,
- startLoginFlow,
- submitManualLoginCode,
- upsertOAuthProvider,
-} from "../../oauth";
-import { removeCredential } from "../../oauth/store";
-import { providerDestinationResolvedError } from "../../lib/destination-policy";
-import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
-import { deriveProviderPresets } from "../../providers/derive";
-import { providerCodexAccountMode } from "../../providers/registry";
+import { loadConfig, multiAgentGuidanceEnabled, mutatePersistedConfig, saveConfigPreservingClaudeCode, subagentDefaultSyncEffective } from "../../config";
+
import { routedSlug } from "../../providers/slug-codec";
-import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
-import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
-import { clearThreadAccountMap } from "../../codex/routing";
-import { primeCodexPoolQuotas } from "../../codex/auth-api";
-import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
-import { resolveCodexHomeDir } from "../../codex/home";
-import { readUsageEntries } from "../../usage/log";
-import { getUsageDebugLogEntries } from "../../usage/debug";
-import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
-import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
-import { getProviderRegistryEntry } from "../../providers/registry";
-import { getDebugLogEntries } from "../../lib/debug-log-buffer";
-import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
-import {
- clearDebugSettings,
- clearDebugSetting,
- getDebugSettings,
- setDebugSettings,
- type DebugFlag,
-} from "../../lib/debug-settings";
-import type { CodexCommanderClaudeCodeConfig, CodexCommanderConfig, CodexCommanderCustomModel, CodexCommanderProviderConfig } from "../../types";
-import { drainAndShutdown } from "../lifecycle";
-import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
-import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
-import type { PersistedUsageAttempt } from "../../usage/log";
-import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
+
+import type { CodexCommanderClaudeCodeConfig, CodexCommanderConfig } from "../../types";
+
+import { jsonResponse } from "../auth-cors";
import { applySystemEnvToggle } from "../system-env";
import {
acquireProxyLifecycleAuthority,
@@ -73,8 +23,8 @@ import {
type ProxyLifecycleAuthority,
} from "../proxy-lifecycle-authority";
-import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels, fetchGrokCandidateModels, buildClaudeDesktopState } from "./shared";
-import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
+import { fetchAllModels, fetchGrokCandidateModels, buildClaudeDesktopState } from "./shared";
+
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
import { projectCatalogActivationForPrincipal } from "./catalog-activation-routes";
diff --git a/src/server/management/combo-routes.ts b/src/server/management/combo-routes.ts
index 2cdd118509..a345daa813 100644
--- a/src/server/management/combo-routes.ts
+++ b/src/server/management/combo-routes.ts
@@ -1,72 +1,21 @@
-import { randomUUID } from "node:crypto";
-import { readFileSync } from "node:fs";
-import type { CatalogModel } from "../../codex/catalog";
-import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
-import {
- DEFAULT_SUBAGENT_MODELS,
- codexAutoStartEnabled,
- hasOwnProvider,
- isValidProviderName,
- multiAgentGuidanceEnabled,
- providerBaseUrlConfigError,
- providerHeadersConfigError,
- saveConfigPreservingClaudeCode,
-} from "../../config";
-import {
- clearLoginState,
- getLoginStatus,
- isPublicOAuthProvider,
- listOAuthProviders,
- startLoginFlow,
- submitManualLoginCode,
- upsertOAuthProvider,
-} from "../../oauth";
-import { removeCredential } from "../../oauth/store";
-import { providerDestinationResolvedError } from "../../lib/destination-policy";
-import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
-import { deriveProviderPresets } from "../../providers/derive";
-import { providerCodexAccountMode } from "../../providers/registry";
-import { routedSlug, slugEquals } from "../../providers/slug-codec";
-import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
-import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
+import { saveConfigPreservingClaudeCode } from "../../config";
+
import {
CODEX_ACCOUNT_NAMESPACE_COMBO_ALIAS_COLLISION_ERROR,
codexAccountNamespaceForModel,
} from "../../codex/account-namespace-match";
-import { clearThreadAccountMap } from "../../codex/routing";
-import { primeCodexPoolQuotas } from "../../codex/auth-api";
-import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
-import { resolveCodexHomeDir } from "../../codex/home";
-import { readUsageEntries } from "../../usage/log";
-import { getUsageDebugLogEntries } from "../../usage/debug";
-import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
-import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
-import { getProviderRegistryEntry } from "../../providers/registry";
-import { getDebugLogEntries } from "../../lib/debug-log-buffer";
-import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
-import {
- clearDebugSettings,
- clearDebugSetting,
- getDebugSettings,
- setDebugSettings,
- type DebugFlag,
-} from "../../lib/debug-settings";
-import type { CodexCommanderClaudeCodeConfig, CodexCommanderConfig, CodexCommanderCustomModel, CodexCommanderProviderConfig } from "../../types";
-import { drainAndShutdown } from "../lifecycle";
+
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
-import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
-import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
-import type { PersistedUsageAttempt } from "../../usage/log";
-import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
-import { applySystemEnvToggle } from "../system-env";
-import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
-import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
+import { jsonResponse } from "../auth-cors";
+
+import { isPlainRecord } from "./shared";
+
import type { ManagementContext } from "./context";
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
export async function handleComboRoutes(ctx: ManagementContext): Promise {
- const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
+ const { req, url, config, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
if (url.pathname === "/api/combos" && req.method === "GET") {
const { comboPublicModelId, getCombo, listComboIds } = await import("../../combos");
diff --git a/src/server/management/config-routes.ts b/src/server/management/config-routes.ts
index ce002fc5e0..176cb5031f 100644
--- a/src/server/management/config-routes.ts
+++ b/src/server/management/config-routes.ts
@@ -1,27 +1,5 @@
-import { readFileSync } from "node:fs";
-import type { CatalogModel } from "../../codex/catalog";
-import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
-import {
- DEFAULT_SUBAGENT_MODELS,
- codexAutoStartEnabled,
- hasOwnProvider,
- isValidProviderName,
- multiAgentGuidanceEnabled,
- providerBaseUrlConfigError,
- providerHeadersConfigError,
- saveConfigPreservingClaudeCode,
-} from "../../config";
-import {
- clearLoginState,
- getLoginStatus,
- isPublicOAuthProvider,
- listOAuthProviders,
- startLoginFlow,
- submitManualLoginCode,
- upsertOAuthProvider,
-} from "../../oauth";
-import { removeCredential } from "../../oauth/store";
-import { providerDestinationResolvedError } from "../../lib/destination-policy";
+import { codexAutoStartEnabled, saveConfigPreservingClaudeCode } from "../../config";
+
import { isStreamMode } from "../../lib/bun-stream-caps";
import { shadowSourceModels } from "../../lib/shadow-call";
import {
@@ -31,36 +9,9 @@ import {
MIN_APP_OWNED_MEMORY_BUDGET_MB,
resolveAppOwnedMemoryBudgetBytes,
} from "../../lib/app-owned-memory";
-import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
-import { deriveProviderPresets } from "../../providers/derive";
-import { providerCodexAccountMode } from "../../providers/registry";
-import { routedSlug, slugEquals } from "../../providers/slug-codec";
-import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
-import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
-import { clearThreadAccountMap } from "../../codex/routing";
-import { primeCodexPoolQuotas } from "../../codex/auth-api";
-import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
-import { resolveCodexHomeDir } from "../../codex/home";
-import { readUsageEntries } from "../../usage/log";
-import { getUsageDebugLogEntries } from "../../usage/debug";
-import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
-import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
-import { getProviderRegistryEntry } from "../../providers/registry";
-import { getDebugLogEntries } from "../../lib/debug-log-buffer";
-import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
-import {
- clearDebugSettings,
- clearDebugSetting,
- getDebugSettings,
- setDebugSettings,
- type DebugFlag,
-} from "../../lib/debug-settings";
-import type { CodexCommanderClaudeCodeConfig, CodexCommanderConfig, CodexCommanderCustomModel, CodexCommanderProviderConfig } from "../../types";
-import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
-import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
-import type { PersistedUsageAttempt } from "../../usage/log";
-import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
-import { applySystemEnvToggle } from "../system-env";
+
+import { jsonResponse, safeConfigDTO } from "../auth-cors";
+
import { getCachedStartupHealth, invalidateStartupHealthCache } from "../startup-health-cache";
import {
decorateStartupHealth,
@@ -74,13 +25,13 @@ import { acquireProxyLifecycleAuthority, type ProxyLifecycleAuthority } from "..
import { validateProxyLifecycleLockLease } from "../proxy-start-lock";
import { readProxyLifecycleLockLeaseHeaders } from "../proxy-lifecycle-protocol";
-import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
-import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
+import { isPlainRecord } from "./shared";
+
import type { ManagementContext } from "./context";
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
export async function handleConfigRoutes(ctx: ManagementContext): Promise {
- const { req, url, config, deps, syncClaudeAgentDefsBestEffort } = ctx;
+ const { req, url, config, deps } = ctx;
if (url.pathname === "/api/config" && req.method === "GET") {
return jsonResponse(safeConfigDTO(config));
}
diff --git a/src/server/management/logs-usage-routes.ts b/src/server/management/logs-usage-routes.ts
index aff047db63..345e9cf207 100644
--- a/src/server/management/logs-usage-routes.ts
+++ b/src/server/management/logs-usage-routes.ts
@@ -1,40 +1,6 @@
-import { randomUUID } from "node:crypto";
-import { readFileSync } from "node:fs";
-import type { CatalogModel } from "../../codex/catalog";
-import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
-import {
- DEFAULT_SUBAGENT_MODELS,
- codexAutoStartEnabled,
- hasOwnProvider,
- isValidProviderName,
- multiAgentGuidanceEnabled,
- providerBaseUrlConfigError,
- providerHeadersConfigError,
- saveConfigPreservingClaudeCode,
-} from "../../config";
-import {
- clearLoginState,
- getLoginStatus,
- isPublicOAuthProvider,
- listOAuthProviders,
- startLoginFlow,
- submitManualLoginCode,
- upsertOAuthProvider,
-} from "../../oauth";
-import { removeCredential } from "../../oauth/store";
-import { providerDestinationResolvedError } from "../../lib/destination-policy";
-import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
-import { deriveProviderPresets } from "../../providers/derive";
-import { providerCodexAccountMode } from "../../providers/registry";
-import { routedSlug, slugEquals } from "../../providers/slug-codec";
-import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
-import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
-import { clearThreadAccountMap } from "../../codex/routing";
-import { primeCodexPoolQuotas } from "../../codex/auth-api";
-import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
import { resolveCodexHomeDir } from "../../codex/home";
import { scanStorage } from "../../storage/scanner";
-import { executeArchivedCleanup, listTrashEntries, pickWireCleanupTestHooks, previewArchivedCleanup, type CleanupMode, type RestoreErrorCode } from "../../storage/cleanup";
+import { listTrashEntries, pickWireCleanupTestHooks, previewArchivedCleanup, type CleanupMode, type RestoreErrorCode } from "../../storage/cleanup";
import { runArchivedCleanupJob } from "../../storage/cleanup-job";
import { getRestoreTrashTestStreamResponse, runRestoreTrashEntryJob } from "../../storage/restore-job";
import {
@@ -55,8 +21,7 @@ import {
} from "../../usage/log";
import { getUsageDebugLogEntries } from "../../usage/debug";
import { parseRange, parseUsageSurface, summarizeUsage, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
-import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
-import { getProviderRegistryEntry } from "../../providers/registry";
+
import { getDebugLogEntries } from "../../lib/debug-log-buffer";
import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
import {
@@ -66,22 +31,20 @@ import {
setDebugSettings,
type DebugFlag,
} from "../../lib/debug-settings";
-import type { CodexCommanderClaudeCodeConfig, CodexCommanderConfig, CodexCommanderCustomModel, CodexCommanderProviderConfig } from "../../types";
-import { drainAndShutdown } from "../lifecycle";
-import { filterRequestLogs, filteredRequestLogCount, getRequestLogEntries, type RequestLogEntry } from "../request-log";
-import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
-import type { PersistedUsageAttempt } from "../../usage/log";
-import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
-import { applySystemEnvToggle } from "../system-env";
-import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
-import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
+import { filterRequestLogs, filteredRequestLogCount, getRequestLogEntries } from "../request-log";
+
+import { jsonResponse } from "../auth-cors";
+
+import { parseDebugLogQuery, requestLogDto } from "./shared";
+
import type { ManagementContext } from "./context";
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
import {
discardUsageSummaryCacheEntry,
getUsageSummaryCacheEntry,
setUsageSummaryCacheEntry,
+ type CachedUsageSummary,
} from "./usage-summary-cache";
const USAGE_DAY_MS = 86_400_000;
@@ -121,7 +84,7 @@ function refreshedUsageSummary {
- const { req, url, config, deps, syncClaudeAgentDefsBestEffort } = ctx;
+ const { req, url, config } = ctx;
if (url.pathname === "/api/logs" && req.method === "GET") {
const all = getRequestLogEntries();
@@ -188,68 +151,46 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise> | null = null;
try {
- const cacheKey = `${range}:${surface}`;
- const effectiveReadLimit = config.managementUsageMaxReadBytes ?? 64 * 1024 * 1024;
+ // File-read machinery: the log-file revision stat (usageLogRevision) and the
+ // snapshot read. A missing log file returns a zeroed snapshot (not a throw); only
+ // genuine read/stat/schema failures reach this catch and answer with the 503
+ // contract.
const observedRevisionKey = `${usageLogRevisionKey(currentUsageLogRevision())}\0${effectiveReadLimit}`;
const cached = getUsageSummaryCacheEntry(cacheKey);
if (cached && cached.revisionKey === observedRevisionKey && now < cached.expiresAt) {
- return jsonResponse(refreshedUsageSummary(cached.summary, range, now));
+ cachedHit = cached.summary;
+ } else {
+ if (cached) discardUsageSummaryCacheEntry(cacheKey);
+ snapshot = await readUsageSnapshotForManagement(effectiveReadLimit);
}
- if (cached) discardUsageSummaryCacheEntry(cacheKey);
- const snapshot = await readUsageSnapshotForManagement(effectiveReadLimit);
- const revisionReadAt = Date.now();
- const summary = {
- ...summarizeUsage(snapshot.entries, range, now, surface),
- historyTruncated: snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated,
- truncatedPrefixBytes: snapshot.truncatedPrefixBytes,
- entriesTruncated: snapshot.entriesTruncated,
- entriesDropped: snapshot.entriesDropped,
- };
- setUsageSummaryCacheEntry(cacheKey, {
- revisionKey: `${usageLogRevisionKey(snapshot.revision)}\0${effectiveReadLimit}`,
- expiresAt: usageSummaryExpiresAt(snapshot.entries, range, surface, now),
- revisionReadAt,
- summary,
- });
- return jsonResponse(summary);
} catch {
- return jsonResponse({
- range,
- surface,
- since: null,
- generatedAt: now,
- summary: {
- requests: 0,
- attemptCount: 0,
- measuredRequests: 0,
- reportedRequests: 0,
- unreportedRequests: 0,
- unsupportedRequests: 0,
- estimatedRequests: 0,
- inputTokens: 0,
- outputTokens: 0,
- cachedInputTokens: 0,
- cacheReadInputTokens: 0,
- cacheCreationInputTokens: 0,
- reasoningOutputTokens: 0,
- totalTokens: 0,
- coverageRatio: 0,
- estimatedCostUsd: 0,
- pricedRequests: 0,
- unpricedRequests: 0,
- unmeteredRequests: 0,
- },
- days: [],
- models: [],
- providers: [],
- historyTruncated: false,
- truncatedPrefixBytes: 0,
- entriesTruncated: false,
- entriesDropped: 0,
- error: "read_failed",
- });
+ return jsonResponse({ error: "read_failed", range, surface }, 503);
}
+ // Outside the catch: genuine errors in the derivation/cache layer are server bugs and
+ // must surface as ordinary server errors, never misreported as read_failed.
+ if (cachedHit) {
+ return jsonResponse(refreshedUsageSummary(cachedHit, range, now));
+ }
+ const revisionReadAt = Date.now();
+ const summary = {
+ ...summarizeUsage(snapshot!.entries, range, now, surface),
+ historyTruncated: snapshot!.truncatedPrefixBytes > 0 || snapshot!.entriesTruncated,
+ truncatedPrefixBytes: snapshot!.truncatedPrefixBytes,
+ entriesTruncated: snapshot!.entriesTruncated,
+ entriesDropped: snapshot!.entriesDropped,
+ };
+ setUsageSummaryCacheEntry(cacheKey, {
+ revisionKey: `${usageLogRevisionKey(snapshot!.revision)}\0${effectiveReadLimit}`,
+ expiresAt: usageSummaryExpiresAt(snapshot!.entries, range, surface, now),
+ revisionReadAt,
+ summary,
+ });
+ return jsonResponse(summary);
}
if (url.pathname === "/api/storage" && req.method === "GET") {
diff --git a/src/server/management/model-routes.ts b/src/server/management/model-routes.ts
index f4765d6b35..d84e665129 100644
--- a/src/server/management/model-routes.ts
+++ b/src/server/management/model-routes.ts
@@ -1,5 +1,4 @@
import { randomUUID } from "node:crypto";
-import { readFileSync } from "node:fs";
/**
* Codex parses a catalog entry's `input_modalities` as a closed enum, and one out-of-enum
@@ -29,80 +28,24 @@ function readInputModalities(raw: unknown): { values?: string[]; error?: string
}
return { values: raw as string[] };
}
-import type { CatalogModel } from "../../codex/catalog";
-import { catalogModelSlug, configuredNativeAliasSlugs, disabledNativeSlugs, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
+
+import { configuredNativeAliasSlugs, disabledNativeSlugs, nativeModelRows } from "../../codex/catalog";
import { CatalogGatherBusyError } from "../../codex/catalog/provider-fetch";
import { getProviderLiveModelCount } from "../../codex/model-cache";
-import {
- DEFAULT_SUBAGENT_MODELS,
- codexAutoStartEnabled,
- hasOwnProvider,
- isValidProviderName,
- multiAgentGuidanceEnabled,
- providerBaseUrlConfigError,
- providerHeadersConfigError,
- saveConfigPreservingClaudeCode,
-} from "../../config";
-import {
- clearLoginState,
- getLoginStatus,
- isPublicOAuthProvider,
- listOAuthProviders,
- startLoginFlow,
- submitManualLoginCode,
- upsertOAuthProvider,
-} from "../../oauth";
-import { removeCredential } from "../../oauth/store";
-import { providerDestinationResolvedError } from "../../lib/destination-policy";
-import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
-import { deriveProviderPresets } from "../../providers/derive";
-import { providerCodexAccountMode } from "../../providers/registry";
+import { hasOwnProvider, isValidProviderName, saveConfigPreservingClaudeCode } from "../../config";
+
import { routedSlug, slugEquals } from "../../providers/slug-codec";
import { COMBO_NAMESPACE, comboDisabledModelSelectors, comboModelId, preservesPhysicalComboProvider } from "../../combos";
-import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
-import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
-import { clearThreadAccountMap } from "../../codex/routing";
-import { primeCodexPoolQuotas } from "../../codex/auth-api";
-import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
-import { resolveCodexHomeDir } from "../../codex/home";
-import { readUsageEntries } from "../../usage/log";
-import { getUsageDebugLogEntries } from "../../usage/debug";
-import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
-import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
-import { getProviderRegistryEntry } from "../../providers/registry";
-import { getDebugLogEntries } from "../../lib/debug-log-buffer";
-import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
-import {
- clearDebugSettings,
- clearDebugSetting,
- getDebugSettings,
- setDebugSettings,
- type DebugFlag,
-} from "../../lib/debug-settings";
-import type { CodexCommanderClaudeCodeConfig, CodexCommanderConfig, CodexCommanderCustomModel, CodexCommanderProviderConfig } from "../../types";
-import { drainAndShutdown } from "../lifecycle";
-import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
-import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
-import type { PersistedUsageAttempt } from "../../usage/log";
-import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO, corsHeaders } from "../auth-cors";
-import { applySystemEnvToggle } from "../system-env";
-import {
- EXPORT_CLIENTS,
- EXPORT_CLIENT_IDS,
- OPENCODE_PROVIDER_ID,
- buildClientConfigText,
- isExportClientId,
- opencodeProxyBaseUrl,
-} from "../../clients/config-export";
-import type {
- ExportClientId,
- ExportModel,
- OpencodeGeneratedConfig,
- PiGeneratedConfig,
-} from "../../clients/config-export";
-import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
-import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
+import type { CodexCommanderCustomModel } from "../../types";
+
+import { jsonResponse, corsHeaders } from "../auth-cors";
+
+import { EXPORT_CLIENTS, EXPORT_CLIENT_IDS, buildClientConfigText, isExportClientId, opencodeProxyBaseUrl } from "../../clients/config-export";
+import type { ExportClientId, ExportModel } from "../../clients/config-export";
+
+import { isPlainRecord, fetchAllModels } from "./shared";
+
import type { ManagementContext } from "./context";
import { listManagementModelRows, loadExportModels } from "./model-rows";
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
@@ -121,7 +64,7 @@ function summarizeExportedModels(client: ExportClientId, document: unknown): { m
}
export async function handleModelRoutes(ctx: ManagementContext): Promise {
- const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
+ const { req, url, config, deps, convergeCodexCatalog } = ctx;
// A handler persists the exact config object passed in. Production defaults to
// the real store; tests that pass an in-memory fixture inject a no-op/spy. Do not
// bypass this seam with a dynamic config import — doing so replaced a user's
@@ -205,6 +148,9 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise {
- const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
+ const { req, url, config, convergeCodexCatalog } = ctx;
// Which providers support real OAuth login (drives the GUI's "Log in with …" buttons).
if (url.pathname === "/api/oauth/providers" && req.method === "GET") {
return jsonResponse({ providers: listOAuthProviders() });
}
+ // DEPRECATED route: no GUI or other client calls /api/key-providers anymore (the key
+ // provider picker now reads /api/providers). Kept for script compatibility; retained
+ // only in tests/docs.
// API-key "login" providers (open dashboard → paste key). Drives the GUI's key-provider picker.
if (url.pathname === "/api/key-providers" && req.method === "GET") {
return jsonResponse({ providers: listKeyLoginProviders() });
@@ -236,7 +203,6 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
if (url.pathname === "/api/oauth/accounts" && req.method === "GET") {
const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
- const status = getLoginStatus(provider);
const { getAccountSet } = await import("../../oauth/store");
const {
oauthAccountHealthFields,
diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts
index dda4b22bed..9b58959547 100644
--- a/src/server/management/provider-routes.ts
+++ b/src/server/management/provider-routes.ts
@@ -1,32 +1,9 @@
-import { randomUUID } from "node:crypto";
-import { readFileSync } from "node:fs";
-import type { CatalogModel } from "../../codex/catalog";
-import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
-import {
- DEFAULT_SUBAGENT_MODELS,
- codexAutoStartEnabled,
- hasOwnProvider,
- isValidProviderName,
- multiAgentGuidanceEnabled,
- providerBaseUrlConfigError,
- providerHeadersConfigError,
- saveConfigPreservingClaudeCode,
- withConfigMutationLockSync,
-} from "../../config";
-import {
- clearLoginState,
- getLoginStatus,
- isPublicOAuthProvider,
- listOAuthProviders,
- startLoginFlow,
- submitManualLoginCode,
- upsertOAuthProvider,
-} from "../../oauth";
-import { removeCredential } from "../../oauth/store";
+import { hasOwnProvider, isValidProviderName, providerHeadersConfigError, saveConfigPreservingClaudeCode, withConfigMutationLockSync } from "../../config";
+
import { providerDestinationResolvedError } from "../../lib/destination-policy";
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
import { ProviderOutboundPolicyError, providerOutboundGet, providerRedirectError } from "../../lib/provider-outbound";
-import { enrichProviderFromCatalog, isPublicCatalogOnlyKeyValidation, listKeyLoginProviders } from "../../oauth/key-providers";
+import { enrichProviderFromCatalog, isPublicCatalogOnlyKeyValidation } from "../../oauth/key-providers";
import { providerCredentialVerification } from "../../providers/credential-verification";
import { deriveProviderPresets } from "../../providers/derive";
import { providerCodexAccountMode, providerMatchesRegistryTransport } from "../../providers/registry";
@@ -36,7 +13,7 @@ import {
readBoundedDiscoveryJson,
resolveProviderModelDiscovery,
} from "../../providers/model-discovery";
-import { routedSlug, slugEquals } from "../../providers/slug-codec";
+
import {
clearProviderQuotaCache,
fetchProviderQuotaReports,
@@ -47,32 +24,17 @@ import { codexAccountNamespaceProviderCollisionError } from "../../codex/account
import { clearThreadAccountMap } from "../../codex/routing";
import { primeCodexPoolQuotas } from "../../codex/auth-api";
import { getProviderDiscoveryStatus } from "../../codex/model-cache";
-import { globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
-import { resolveCodexHomeDir } from "../../codex/home";
-import { readUsageEntries } from "../../usage/log";
-import { getUsageDebugLogEntries } from "../../usage/debug";
-import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
+import { globalContextCapValue, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
+
import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
import { getProviderRegistryEntry } from "../../providers/registry";
-import { getDebugLogEntries } from "../../lib/debug-log-buffer";
-import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
-import {
- clearDebugSettings,
- clearDebugSetting,
- getDebugSettings,
- setDebugSettings,
- type DebugFlag,
-} from "../../lib/debug-settings";
-import type { CodexCommanderClaudeCodeConfig, CodexCommanderConfig, CodexCommanderCustomModel, CodexCommanderProviderConfig } from "../../types";
-import { drainAndShutdown } from "../lifecycle";
-import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
-import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
-import type { PersistedUsageAttempt } from "../../usage/log";
-import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
-import { applySystemEnvToggle } from "../system-env";
-import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
-import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
+import type { CodexCommanderConfig, CodexCommanderProviderConfig } from "../../types";
+
+import { jsonResponse, providerManagementConfigError, publicProviderBaseUrl } from "../auth-cors";
+
+import { isPlainRecord, stripRegistryOnlyStaticHeaders } from "./shared";
+
import type { ManagementContext } from "./context";
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
@@ -238,7 +200,7 @@ function applyProviderPatchFields(
}
export async function handleProviderRoutes(ctx: ManagementContext): Promise {
- const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
+ const { req, url, config, deps, convergeCodexCatalog } = ctx;
if (url.pathname === "/api/provider-quotas" && req.method === "GET") {
const forceRefresh = url.searchParams.get("refresh") === "1" || url.searchParams.get("refresh") === "true";
diff --git a/src/server/management/shared.ts b/src/server/management/shared.ts
index 4624d00a22..6987de4b95 100644
--- a/src/server/management/shared.ts
+++ b/src/server/management/shared.ts
@@ -1,61 +1,13 @@
-import { randomUUID } from "node:crypto";
-import { readFileSync } from "node:fs";
import type { CatalogModel } from "../../codex/catalog";
-import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
-import {
- DEFAULT_SUBAGENT_MODELS,
- codexAutoStartEnabled,
- hasOwnProvider,
- isValidProviderName,
- multiAgentGuidanceEnabled,
- providerBaseUrlConfigError,
- providerHeadersConfigError,
- saveConfigPreservingClaudeCode,
-} from "../../config";
-import {
- clearLoginState,
- getLoginStatus,
- isPublicOAuthProvider,
- listOAuthProviders,
- startLoginFlow,
- submitManualLoginCode,
- upsertOAuthProvider,
-} from "../../oauth";
-import { removeCredential } from "../../oauth/store";
-import { providerDestinationResolvedError } from "../../lib/destination-policy";
-import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
-import { deriveProviderPresets } from "../../providers/derive";
-import { providerCodexAccountMode } from "../../providers/registry";
-import { routedSlug, slugEquals } from "../../providers/slug-codec";
-import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
-import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
-import { clearThreadAccountMap } from "../../codex/routing";
-import { primeCodexPoolQuotas } from "../../codex/auth-api";
-import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
-import { resolveCodexHomeDir } from "../../codex/home";
-import { readUsageEntries } from "../../usage/log";
-import { getUsageDebugLogEntries } from "../../usage/debug";
-import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
-import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
+
import { getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
-import { getDebugLogEntries } from "../../lib/debug-log-buffer";
-import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
-import {
- clearDebugSettings,
- clearDebugSetting,
- getDebugSettings,
- setDebugSettings,
- type DebugFlag,
-} from "../../lib/debug-settings";
-import type { CodexCommanderClaudeCodeConfig, CodexCommanderClaudeDesktopProfile, CodexCommanderConfig, CodexCommanderCustomModel, CodexCommanderProviderConfig } from "../../types";
+
+import type { CodexCommanderClaudeDesktopProfile, CodexCommanderConfig, CodexCommanderProviderConfig } from "../../types";
import type { DesktopProfileModel } from "../../claude/desktop-profile";
-import { drainAndShutdown } from "../lifecycle";
-import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
+
+import { type RequestLogEntry } from "../request-log";
import { estimateComboCost, estimateRequestCost, serviceTierContext, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
import type { PersistedUsageAttempt } from "../../usage/log";
-import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
-import { applySystemEnvToggle } from "../system-env";
-
export function isPlainRecord(v: unknown): v is Record {
return typeof v === "object" && v !== null && !Array.isArray(v);
diff --git a/tests/anthropic-reasoning.test.ts b/tests/anthropic-reasoning.test.ts
index 32f531bfc5..fa55ae1245 100644
--- a/tests/anthropic-reasoning.test.ts
+++ b/tests/anthropic-reasoning.test.ts
@@ -97,14 +97,40 @@ describe("anthropic extended-thinking gate", () => {
});
test.each([
+ ["medium", 16_384], // medium keeps its 8192 budget + headroom; never falls through to unknown.
["high", 24_576],
["xhigh", 32_768],
["max", 40_192],
+ ["ultra", 40_192], // ultra mirrors the codex-rs boundary and clamps to max on the wire.
])("adaptive-thinking %s effort reserves visible-output headroom", async (effort, expected) => {
const b = await bodyOf(parsed(effort, {}, "claude-fable-5"));
expect(b.max_tokens).toBe(expected);
});
+ test("adaptive-thinking model clamps unsupported 'ultra' effort to 'max'", async () => {
+ const b = await bodyOf(parsed("ultra", {}, "claude-fable-5"));
+ expect(b.thinking).toEqual({ type: "adaptive" });
+ // Anthropic's output_config.effort ladder tops out at max; "ultra" would 400.
+ expect(b.output_config).toEqual({ effort: "max" });
+ });
+
+ test("adaptive-thinking model clamps unknown efforts to 'high'", async () => {
+ const b = await bodyOf(parsed("ludicrous", {}, "claude-fable-5"));
+ expect(b.output_config).toEqual({ effort: "high" });
+ // Unknown clamps to high on the wire AND budgets like high (16384 + headroom).
+ expect(b.max_tokens).toBe(24_576);
+ });
+
+ test("non-adaptive model maps ultra to the max thinking budget", async () => {
+ // Default modelId (claude-sonnet-4.5) is NOT an adaptive-thinking family, so ultra
+ // goes through the budget ladder: it must budget like max, above xhigh's 24576.
+ const b = await bodyOf(parsed("ultra"));
+ const thinking = b.thinking as { type: string; budget_tokens: number } | undefined;
+ expect(thinking?.type).toBe("enabled");
+ expect(thinking?.budget_tokens ?? 0).toBeGreaterThan(24_576);
+ expect(b.max_tokens as number).toBeGreaterThan(thinking!.budget_tokens);
+ });
+
test("Anthropic streaming and JSON responses preserve max_tokens stop reasons", async () => {
const adapter = createAnthropicAdapter(provider);
const sse = [
diff --git a/tests/api-usage.test.ts b/tests/api-usage.test.ts
index 4b21a54a4a..b82388a30b 100644
--- a/tests/api-usage.test.ts
+++ b/tests/api-usage.test.ts
@@ -238,10 +238,9 @@ describe("GET /api/usage", () => {
const server = startServer(0);
try {
const res = await fetch(new URL("/api/usage?surface=claude", server.url));
- expect(res.status).toBe(200);
+ expect(res.status).toBe(503);
const body = await res.json();
expect(body.surface).toBe("claude");
- expect(body.summary.requests).toBe(0);
expect(body.error).toBe("read_failed");
} finally {
await server.stop(true);
@@ -262,4 +261,20 @@ describe("GET /api/usage", () => {
await server.stop(true);
}
});
+
+ test("success response includes cost and request classification fields", async () => {
+ writeFixture(Date.now());
+ const server = startServer(0);
+ try {
+ const res = await fetch(new URL("/api/usage?range=all", server.url));
+ expect(res.status).toBe(200);
+ const body = await res.json();
+ expect(typeof body.summary.estimatedCostUsd).toBe("number");
+ expect(typeof body.summary.pricedRequests).toBe("number");
+ expect(typeof body.summary.unpricedRequests).toBe("number");
+ expect(typeof body.summary.unmeteredRequests).toBe("number");
+ } finally {
+ await server.stop(true);
+ }
+ });
});
diff --git a/tests/provider-quota.test.ts b/tests/provider-quota.test.ts
index 8d4a99f9da..f22b635a0b 100644
--- a/tests/provider-quota.test.ts
+++ b/tests/provider-quota.test.ts
@@ -15,10 +15,12 @@ import {
markAccountNeedsReauth as markOAuthAccountNeedsReauth,
saveCredential,
} from "../src/oauth/store";
+import * as oauthApi from "../src/oauth";
import { getLoginStatus } from "../src/oauth";
import {
clearProviderQuotaCache,
fetchProviderQuotaReports,
+ parseXaiCreditsResponse,
setProviderQuotaBeforePublishForTests,
supportsProviderQuotaReporting,
} from "../src/providers/quota";
@@ -1604,3 +1606,182 @@ describe("fetchProviderQuotaReports", () => {
expect(pruned.reports).toEqual([]);
});
});
+
+test("parseXaiCreditsResponse maps weekly credits and rejects non-weekly periods", () => {
+ expect(parseXaiCreditsResponse({
+ config: {
+ creditUsagePercent: 57.4,
+ currentPeriod: { type: "USAGE_PERIOD_TYPE_WEEKLY", end: "2026-08-15T13:05:52.277209Z" },
+ },
+ })).toEqual({
+ percent: 57.4,
+ resetAt: Date.parse("2026-08-15T13:05:52.277209Z"),
+ });
+ expect(parseXaiCreditsResponse({
+ config: {
+ currentPeriod: { type: "USAGE_PERIOD_TYPE_WEEKLY", end: "2026-08-15T13:05:52.277209Z" },
+ },
+ })).toEqual({
+ percent: 0,
+ resetAt: Date.parse("2026-08-15T13:05:52.277209Z"),
+ });
+ expect(parseXaiCreditsResponse({
+ config: {
+ creditUsagePercent: 10,
+ currentPeriod: { type: "USAGE_PERIOD_TYPE_MONTHLY", end: "2026-08-15T13:05:52.277209Z" },
+ },
+ })).toBeNull();
+});
+
+test("xAI OAuth quota prefers weekly credits and falls back to monthly when weekly fails", async () => {
+ spyOn(oauthApi, "getValidAccessTokenSnapshot").mockResolvedValue({
+ provider: "xai",
+ accountId: "xai-user-1",
+ generation: "test-generation",
+ accessToken: "xai-access-secret",
+ });
+ const seen: { url: string; headers: Record }[] = [];
+ globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
+ const url = String(input);
+ const headers = Object.fromEntries(new Headers(init?.headers).entries());
+ seen.push({ url, headers });
+ if (url === "https://cli-chat-proxy.grok.com/v1/billing?format=credits") {
+ return new Response(JSON.stringify({
+ config: {
+ creditUsagePercent: 31,
+ currentPeriod: { type: "USAGE_PERIOD_TYPE_WEEKLY", end: "2026-08-15T00:00:00Z" },
+ raw_secret_should_not_escape: "xai-access-secret",
+ },
+ }), { status: 200, headers: { "content-type": "application/json" } });
+ }
+ if (url === "https://cli-chat-proxy.grok.com/v1/billing") {
+ return new Response(JSON.stringify({
+ config: {
+ monthlyLimit: { val: 10_000 },
+ used: { val: 2_500 },
+ billingPeriodEnd: "2026-08-31T00:00:00Z",
+ },
+ }), { status: 200, headers: { "content-type": "application/json" } });
+ }
+ return new Response("not found", { status: 404 });
+ }) as typeof fetch;
+
+ const config = {
+ defaultProvider: "xai",
+ providers: {
+ xai: { adapter: "openai-chat", authMode: "oauth", baseUrl: "https://api.x.ai/v1" },
+ },
+ } as CodexCommanderConfig;
+ const weekly = await fetchProviderQuotaReports(config, true);
+ expect(weekly.reports).toHaveLength(1);
+ expect(weekly.reports[0]?.source).toBe("xai:grok-billing-credits");
+ expect(weekly.reports[0]?.quota).toMatchObject({
+ weeklyPercent: 31,
+ weeklyResetAt: Date.parse("2026-08-15T00:00:00Z"),
+ });
+ expect(weekly.reports[0]?.quota.monthlyPercent).toBeUndefined();
+ const creditsCall = seen.find(row => row.url.endsWith("format=credits"));
+ expect(creditsCall?.headers.authorization).toBe("Bearer xai-access-secret");
+ expect(creditsCall?.headers["x-userid"]).toBe("xai-user-1");
+ expect(creditsCall?.headers["x-xai-token-auth"]).toBe("xai-grok-cli");
+ expect(creditsCall?.headers["x-authenticateresponse"]).toBe("authenticate-response");
+ expect(creditsCall?.headers["x-grok-client-version"]).toBeTruthy();
+ expect(JSON.stringify(weekly)).not.toContain("xai-access-secret");
+ expect(JSON.stringify(weekly)).not.toContain("xai-user-1");
+
+ // Weekly non-2xx falls back to the monthly dollar pool.
+ seen.length = 0;
+ globalThis.fetch = (async (input: RequestInfo | URL) => {
+ const url = String(input);
+ seen.push({ url, headers: {} });
+ if (url.endsWith("format=credits")) {
+ return new Response("nope", { status: 503 });
+ }
+ if (url === "https://cli-chat-proxy.grok.com/v1/billing") {
+ return new Response(JSON.stringify({
+ config: {
+ monthlyLimit: { val: 10_000 },
+ used: { val: 2_500 },
+ billingPeriodEnd: "2026-08-31T00:00:00Z",
+ },
+ }), { status: 200, headers: { "content-type": "application/json" } });
+ }
+ return new Response("not found", { status: 404 });
+ }) as typeof fetch;
+ const monthly = await fetchProviderQuotaReports(config, true);
+ expect(monthly.reports[0]?.source).toBe("xai:grok-billing");
+ expect(monthly.reports[0]?.quota.monthlyPercent).toBe(25);
+ expect(monthly.reports[0]?.quota.weeklyPercent).toBeUndefined();
+ expect(seen.some(row => row.url.endsWith("format=credits"))).toBe(true);
+ expect(seen.some(row => row.url === "https://cli-chat-proxy.grok.com/v1/billing")).toBe(true);
+});
+
+test("xAI OAuth quota skips weekly when identity is absent and keeps monthly", async () => {
+ spyOn(oauthApi, "getValidAccessTokenSnapshot").mockResolvedValue({
+ provider: "xai",
+ accountId: "",
+ generation: "test-generation",
+ accessToken: "xai-access-secret",
+ });
+ const seen: string[] = [];
+ globalThis.fetch = (async (input: RequestInfo | URL) => {
+ const url = String(input);
+ seen.push(url);
+ if (url === "https://cli-chat-proxy.grok.com/v1/billing") {
+ return new Response(JSON.stringify({
+ config: {
+ monthlyLimit: { val: 10_000 },
+ used: { val: 2_500 },
+ billingPeriodEnd: "2026-08-31T00:00:00Z",
+ },
+ }), { status: 200, headers: { "content-type": "application/json" } });
+ }
+ return new Response("not found", { status: 404 });
+ }) as typeof fetch;
+ const result = await fetchProviderQuotaReports({
+ defaultProvider: "xai",
+ providers: {
+ xai: { adapter: "openai-chat", authMode: "oauth", baseUrl: "https://api.x.ai/v1" },
+ },
+ } as CodexCommanderConfig, true);
+ expect(seen.some(url => url.includes("format=credits"))).toBe(false);
+ expect(result.reports[0]?.source).toBe("xai:grok-billing");
+ expect(result.reports[0]?.quota.monthlyPercent).toBe(25);
+});
+
+test("xAI quota reports observed usage when the account reports no cap", async () => {
+ spyOn(oauthApi, "getValidAccessTokenSnapshot").mockResolvedValue({
+ provider: "xai",
+ accountId: "xai-user-1",
+ generation: "test-generation",
+ accessToken: "xai-access-secret",
+ });
+ globalThis.fetch = (async (input: RequestInfo | URL) => {
+ const url = String(input);
+ if (url.endsWith("format=credits")) return new Response("nope", { status: 503 });
+ if (url === "https://cli-chat-proxy.grok.com/v1/billing") {
+ return new Response(JSON.stringify({
+ config: {
+ monthlyLimit: { val: 0 },
+ used: { val: 243 },
+ billingPeriodEnd: "2026-09-01T00:00:00Z",
+ },
+ }), { status: 200, headers: { "content-type": "application/json" } });
+ }
+ return new Response("not found", { status: 404 });
+ }) as typeof fetch;
+ const result = await fetchProviderQuotaReports({
+ defaultProvider: "xai",
+ providers: {
+ xai: { adapter: "openai-chat", authMode: "oauth", baseUrl: "https://api.x.ai/v1" },
+ },
+ } as CodexCommanderConfig, true);
+ expect(result.reports[0]?.source).toBe("xai:grok-billing");
+ expect(result.reports[0]?.quota.customWindows).toEqual([{
+ label: "No reported cap",
+ percent: 0,
+ resetAt: Date.parse("2026-09-01T00:00:00Z"),
+ }]);
+ expect(result.reports[0]?.quota.monthlyResetAt).toBeUndefined();
+ expect(result.availability?.[0]).toMatchObject({ provider: "xai", status: "available" });
+});
diff --git a/tests/reasoning-effort.test.ts b/tests/reasoning-effort.test.ts
index b18ac2e38b..0d5d3a7372 100644
--- a/tests/reasoning-effort.test.ts
+++ b/tests/reasoning-effort.test.ts
@@ -4,6 +4,7 @@ import { createAnthropicAdapter } from "../src/adapters/anthropic";
import { createOpenAIChatAdapter } from "../src/adapters/openai-chat";
import type { AdapterRequest } from "../src/adapters/base";
import { configuredReasoningEfforts, mapReasoningEffort, sanitizeCodexReasoningEfforts } from "../src/reasoning-effort";
+import { enrichProviderFromRegistry } from "../src/providers/derive";
import { routeModel } from "../src/router";
import { resolveWireProtocolOverride } from "../src/server/adapter-resolve";
import type { CodexCommanderConfig, CodexCommanderParsedRequest, CodexCommanderProviderConfig } from "../src/types";
@@ -700,6 +701,47 @@ describe("ultra reasoning effort (upstream codex-rs parity)", () => {
expect(mapReasoningEffort(base, "m", "ultra")).toBe("max");
});
+ test("xAI registry ladder clamps max/ultra on the real route path (grok-4.6)", () => {
+ // xAI reasoning_effort accepts low|medium|high (xhigh on grok-4.6+ only); max and ultra
+ // are rejected with 400 "Invalid reasoning effort" (observed via the proxy request log:
+ // grok-4.6 + wireValue "max" -> 400). This exercises the REAL registry entry through the
+ // route path, so it fails on a registry that omits the grok-4.6 ladder.
+ const config = {
+ defaultProvider: "xai",
+ providers: {
+ xai: { adapter: "openai-chat", baseUrl: "https://api.x.ai/v1", authMode: "oauth" },
+ },
+ } as unknown as CodexCommanderConfig;
+ const route = routeModel(config, "xai/grok-4.6");
+ expect(route.provider.modelReasoningEfforts?.["grok-4.6"]).toEqual(["low", "medium", "high", "xhigh"]);
+ expect(mapReasoningEffort(route.provider, "grok-4.6", "ultra")).toBe("xhigh");
+ expect(mapReasoningEffort(route.provider, "grok-4.6", "max")).toBe("xhigh");
+ expect(mapReasoningEffort(route.provider, "grok-4.6", "xhigh")).toBe("xhigh");
+ const older = routeModel(config, "xai/grok-4.5");
+ expect(mapReasoningEffort(older.provider, "grok-4.5", "ultra")).toBe("high");
+ expect(mapReasoningEffort(older.provider, "grok-4.5", "max")).toBe("high");
+ // Live-discovered models without a per-model entry fall back to the provider ladder.
+ expect(mapReasoningEffort(route.provider, "grok-4.7", "ultra")).toBe("high");
+ // noReasoningModels members stay effort-free.
+ expect(mapReasoningEffort(route.provider, "grok-build-0.1", "max")).toBeUndefined();
+ });
+
+ test("enrichProviderFromRegistry merges new ladder keys into a partially saved map", () => {
+ // A config saved when the registry only knew grok-4.5 must pick up the grok-4.6 xhigh
+ // tier from the current registry for catalog advertisement, without overwriting the
+ // saved per-model entry (saved entries always win per key).
+ const provider: CodexCommanderProviderConfig = {
+ adapter: "openai-chat",
+ baseUrl: "https://api.x.ai/v1",
+ authMode: "oauth",
+ modelReasoningEfforts: { "grok-4.5": ["low", "medium", "high"] },
+ };
+ enrichProviderFromRegistry("xai", provider);
+ expect(provider.modelReasoningEfforts?.["grok-4.5"]).toEqual(["low", "medium", "high"]);
+ expect(provider.modelReasoningEfforts?.["grok-4.6"]).toEqual(["low", "medium", "high", "xhigh"]);
+ expect(provider.reasoningEfforts).toEqual(["low", "medium", "high"]);
+ });
+
test("a max wire alias applies to converted ultra; a raw ultra alias never bypasses the boundary", () => {
expect(mapReasoningEffort({ ...base, reasoningEfforts: ["low", "medium", "high", "xhigh", "max", "ultra"], reasoningEffortMap: { max: "think-hard" } }, "m", "ultra")).toBe("think-hard");
// Upstream never lets "ultra" influence the provider wire; the alias table is consulted with