0
- ? `${(merged.costQuality.cacheSavingsUsd / merged.costUsd).toFixed(1)}x the raw token cost`
- : "vs full input rates"
+ costUnavailable
+ ? "requires the rate table"
+ : merged.costUsd > 0
+ ? `${(merged.costQuality.cacheSavingsUsd / merged.costUsd).toFixed(1)}x the raw token cost`
+ : "vs full input rates"
}
/>
@@ -365,10 +396,10 @@ export function UsagePage() {
|
- {formatUsd(model.costUsd)}
+ {costUnavailable ? "—" : formatUsd(model.costUsd)}
|
- {formatPercent(model.costShare)}
+ {costUnavailable ? "—" : formatPercent(model.costShare)}
|
{formatTokens(model.totalTokens)}
@@ -415,11 +446,13 @@ export function UsagePage() {
key={provider}
className="py-2 text-right text-muted-foreground tabular-nums"
>
- {formatUsd(period.byProvider.get(provider)?.costUsd ?? 0)}
+ {costUnavailable
+ ? "—"
+ : formatUsd(period.byProvider.get(provider)?.costUsd ?? 0)}
|
))}
- {formatUsd(period.costUsd)}
+ {costUnavailable ? "—" : formatUsd(period.costUsd)}
|
{formatTokens(period.totalTokens)}
@@ -471,30 +504,44 @@ function Metric({
}
/**
- * Says plainly when the totals are incomplete: an environment that failed, or
- * one whose transcripts another environment already reported. Environments
- * that are still answering never reach this notice; the page shows the
- * loading skeleton until every one is terminal.
+ * Says plainly when the totals are incomplete: an environment that failed,
+ * one whose transcripts another environment already reported, or a missing
+ * rate table. Environments that are still answering never reach this notice;
+ * the page shows the loading skeleton until every one is terminal.
*/
function UsageCoverageNotice({
environments,
duplicateSources,
staleEnvironments,
+ pricingStatus,
}: {
readonly environments: readonly EnvironmentUsageStatus[];
readonly duplicateSources: readonly string[];
readonly staleEnvironments: readonly string[];
+ readonly pricingStatus: UsagePricingStatus;
}) {
const failed = environments.filter((environment) => environment.error !== null);
const stale = environments.filter((environment) =>
staleEnvironments.includes(environment.environmentId),
);
- if (failed.length === 0 && stale.length === 0 && duplicateSources.length === 0) {
+ const costUnavailable = pricingStatus === "unavailable";
+ if (
+ failed.length === 0 &&
+ stale.length === 0 &&
+ duplicateSources.length === 0 &&
+ !costUnavailable
+ ) {
return null;
}
return (
+ {costUnavailable ? (
+
+ The model rate table could not be loaded, so costs are omitted. Token counts are still
+ valid.
+
+ ) : null}
{failed.map((environment) => (
{environment.label} could not report usage.
))}
diff --git a/docs/user/usage.md b/docs/user/usage.md
index 72d19ba77f37c..b066dad8c791c 100644
--- a/docs/user/usage.md
+++ b/docs/user/usage.md
@@ -3,7 +3,8 @@
The Usage page combines Codex and Claude Code activity from your connected environments. It reads
the providers' local session history and shows API-equivalent token cost, processed tokens, cache
savings, provider shares, and model breakdowns. Subscription billing is separate from the raw token
-cost shown here.
+cost shown here. If the model rate table cannot be loaded, the page says cost is unavailable
+instead of $0.00. Token counts are still valid.
Use **Past 24h** for an hourly chart covering the exact rolling 24-hour period. The **7 days**,
**30 days**, and **90 days** ranges use daily resolution. Cost and token toggles update both the
diff --git a/packages/shared/src/usageFormat.test.ts b/packages/shared/src/usageFormat.test.ts
index fb231fbacb206..de9737fb86039 100644
--- a/packages/shared/src/usageFormat.test.ts
+++ b/packages/shared/src/usageFormat.test.ts
@@ -6,9 +6,28 @@ import {
formatDateTimeShort,
formatHourShort,
formatRelativeHourShort,
+ formatUsageCost,
makeWindow,
} from "./usageFormat.ts";
+describe("formatUsageCost", () => {
+ it("does not present a missing rate table as $0.00", () => {
+ expect(formatUsageCost("unavailable", 0)).toBe("Cost unavailable");
+ });
+
+ it("keeps a genuine zero when rates loaded", () => {
+ expect(formatUsageCost("fresh", 0)).toBe("$0.00");
+ });
+
+ it("formats a fresh dollar amount", () => {
+ expect(formatUsageCost("fresh", 12.3)).toBe("$12.30");
+ });
+
+ it("formats a cached dollar amount", () => {
+ expect(formatUsageCost("cached", 4.5)).toBe("$4.50");
+ });
+});
+
describe("hourly usage formatting", () => {
it("enumerates 24 fixed buckets across a rolling window", () => {
const hours = enumerateHourStarts("2026-08-10T12:37:00.000Z", "2026-08-11T12:37:00.000Z");
diff --git a/packages/shared/src/usageFormat.ts b/packages/shared/src/usageFormat.ts
index bd751829dd87b..2ed94fa6e89b9 100644
--- a/packages/shared/src/usageFormat.ts
+++ b/packages/shared/src/usageFormat.ts
@@ -4,7 +4,12 @@
*
* @module usageFormat
*/
-import { UsageDay, type UsageResolution, type UsageSummaryInput } from "@t3tools/contracts";
+import {
+ UsageDay,
+ type UsagePricingStatus,
+ type UsageResolution,
+ type UsageSummaryInput,
+} from "@t3tools/contracts";
const CURRENCY = new Intl.NumberFormat("en-US", {
style: "currency",
@@ -19,6 +24,15 @@ export function formatUsd(value: number): string {
return CURRENCY.format(value);
}
+/**
+ * Dollar column for the usage page. A missing rate table must not render as a
+ * real `$0.00`.
+ */
+export function formatUsageCost(status: UsagePricingStatus, costUsd: number): string {
+ if (status === "unavailable") return "Cost unavailable";
+ return formatUsd(costUsd);
+}
+
export function formatCount(value: number): string {
return INTEGER.format(Math.round(value));
}
diff --git a/packages/shared/src/usageMerge.test.ts b/packages/shared/src/usageMerge.test.ts
index c2fa9e2a86a19..39b6f9006a867 100644
--- a/packages/shared/src/usageMerge.test.ts
+++ b/packages/shared/src/usageMerge.test.ts
@@ -3,6 +3,7 @@ import {
type EnvironmentId,
type UsageBucket,
type UsageDay,
+ type UsagePricingStatus,
type UsageProviderKind,
type UsageSummary,
} from "@t3tools/contracts";
@@ -42,6 +43,7 @@ function summary(
distinctSessions?: number;
}[],
contractVersion: number = USAGE_CONTRACT_VERSION,
+ pricingStatus: UsagePricingStatus = "fresh",
): UsageSummary {
return {
contractVersion,
@@ -64,7 +66,7 @@ function summary(
distinctSessions: source.distinctSessions ?? 1,
message: null,
})),
- pricing: { status: "fresh", source: "litellm", fetchedAt: null, knownModels: 10 },
+ pricing: { status: pricingStatus, source: "litellm", fetchedAt: null, knownModels: 10 },
scanDurationMs: 1,
};
}
@@ -253,10 +255,62 @@ describe("mergeUsage", () => {
it("returns empty totals with no environments", () => {
const merged = mergeUsage([], USAGE_CONTRACT_VERSION);
expect(merged.costUsd).toBe(0);
+ expect(merged.pricingStatus).toBe("fresh");
expect(merged.daily).toHaveLength(0);
expect(merged.hourly).toHaveLength(0);
});
+ it("keeps the worst pricing status among environments that contributed buckets", () => {
+ const sourceA = { provider: "claude" as const, hostId: "mac", homePath: "/a/.claude" };
+ const sourceB = { provider: "claude" as const, hostId: "linux", homePath: "/b/.claude" };
+
+ const cachedAndFresh = mergeUsage(
+ [
+ environment("env-a", summary([bucket()], [sourceA], USAGE_CONTRACT_VERSION, "cached")),
+ environment("env-b", summary([bucket()], [sourceB], USAGE_CONTRACT_VERSION, "fresh")),
+ ],
+ USAGE_CONTRACT_VERSION,
+ );
+ expect(cachedAndFresh.pricingStatus).toBe("cached");
+
+ const mixedUnavailable = mergeUsage(
+ [
+ environment("env-a", summary([bucket()], [sourceA], USAGE_CONTRACT_VERSION, "cached")),
+ environment("env-b", summary([bucket()], [sourceB], USAGE_CONTRACT_VERSION, "unavailable")),
+ ],
+ USAGE_CONTRACT_VERSION,
+ );
+ expect(mixedUnavailable.pricingStatus).toBe("unavailable");
+ });
+
+ it("ignores pricing status from environments that contributed no buckets", () => {
+ const merged = mergeUsage(
+ [
+ environment(
+ "env-a",
+ summary(
+ [bucket()],
+ [{ provider: "claude", hostId: "mac", homePath: "/a/.claude" }],
+ USAGE_CONTRACT_VERSION,
+ "fresh",
+ ),
+ ),
+ environment(
+ "env-b",
+ summary(
+ [],
+ [{ provider: "claude", hostId: "linux", homePath: "/b/.claude" }],
+ USAGE_CONTRACT_VERSION,
+ "unavailable",
+ ),
+ ),
+ ],
+ USAGE_CONTRACT_VERSION,
+ );
+
+ expect(merged.pricingStatus).toBe("fresh");
+ });
+
it("derives hourly totals without losing the daily rollup", () => {
const merged = mergeUsage(
[
diff --git a/packages/shared/src/usageMerge.ts b/packages/shared/src/usageMerge.ts
index 886b214183bc2..c1c5e3596ad8c 100644
--- a/packages/shared/src/usageMerge.ts
+++ b/packages/shared/src/usageMerge.ts
@@ -9,6 +9,7 @@
import type {
EnvironmentId,
UsageBucket,
+ UsagePricingStatus,
UsageProviderKind,
UsageSourceFingerprint,
UsageSummary,
@@ -75,6 +76,11 @@ export interface MergedUsage {
readonly daily: readonly DailyTotals[];
readonly hourly: readonly HourlyTotals[];
readonly costQuality: CostQuality;
+ /**
+ * Worst rate-table status among environments that contributed buckets.
+ * `unavailable` means dollar figures must not be shown as real zeros.
+ */
+ readonly pricingStatus: UsagePricingStatus;
/** Environments whose data was dropped as a duplicate of another's. */
readonly duplicateSources: readonly string[];
readonly contributingEnvironments: readonly EnvironmentId[];
@@ -184,11 +190,22 @@ const EMPTY_MERGED: MergedUsage = {
unpricedShare: 0,
cacheSavingsUsd: 0,
},
+ pricingStatus: "fresh",
duplicateSources: [],
contributingEnvironments: [],
staleEnvironments: [],
};
+const PRICING_STATUS_RANK: Record = {
+ fresh: 0,
+ cached: 1,
+ unavailable: 2,
+};
+
+function worsePricingStatus(a: UsagePricingStatus, b: UsagePricingStatus): UsagePricingStatus {
+ return PRICING_STATUS_RANK[a] >= PRICING_STATUS_RANK[b] ? a : b;
+}
+
/**
* Merges every connected environment's summary.
*
@@ -253,13 +270,17 @@ export function mergeUsage(
}
>();
const contributingEnvironments: EnvironmentId[] = [];
+ let pricingStatus: UsagePricingStatus = "fresh";
for (const environment of current) {
const { buckets, sessions: environmentSessions } = ownedContribution(
environment,
ownerByFingerprint,
);
- if (buckets.length > 0) contributingEnvironments.push(environment.environmentId);
+ if (buckets.length > 0) {
+ contributingEnvironments.push(environment.environmentId);
+ pricingStatus = worsePricingStatus(pricingStatus, environment.summary.pricing.status);
+ }
sessions += environmentSessions;
for (const bucket of buckets) {
@@ -391,6 +412,7 @@ export function mergeUsage(
records === 0 ? 0 : (records - providerReportedRecords - unpricedRecords) / records,
cacheSavingsUsd,
},
+ pricingStatus,
duplicateSources: duplicates,
contributingEnvironments,
staleEnvironments,
|