diff --git a/assets/pr2950-capacity-expiry.png b/assets/pr2950-capacity-expiry.png new file mode 100644 index 0000000000..10e3c995b9 Binary files /dev/null and b/assets/pr2950-capacity-expiry.png differ diff --git a/gui/src/components/provider-workspace/ProviderCapacityQuota.tsx b/gui/src/components/provider-workspace/ProviderCapacityQuota.tsx index abc2fb9e63..20907b8f6c 100644 --- a/gui/src/components/provider-workspace/ProviderCapacityQuota.tsx +++ b/gui/src/components/provider-workspace/ProviderCapacityQuota.tsx @@ -56,18 +56,30 @@ export function ProviderCapacityQuota({ report, pending }: { report: ProviderQuo ...(aggregation.customWindows ?? []).map((window, index) => ({ key: index + 3, label: window.label, window })), ] : []; const formatPercent = (value: number) => new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value); - const formatRecoveryAt = (value: number) => new Intl.DateTimeFormat(locale, { - dateStyle: "medium", - timeStyle: "short", - }).format(new Date(value > 10_000_000_000 ? value : value * 1000)); + // `Intl.DateTimeFormat.format()` throws a RangeError on a time value outside ±8.64e15 ms. + // These timestamps come from provider APIs and persisted cache, so one unrepresentable value + // would take down the whole capacity panel. Resolve to null and omit the line instead. + const asDate = (value: number): Date | null => { + const date = new Date(value > 10_000_000_000 ? value : value * 1000); + return Number.isFinite(date.getTime()) ? date : null; + }; + const formatRecoveryAt = (value: number) => { + const date = asDate(value); + return date === null ? null : new Intl.DateTimeFormat(locale, { + dateStyle: "medium", + timeStyle: "short", + }).format(date); + }; const localeTag = bcp47(locale); const formatCredits = (value: number) => new Intl.NumberFormat(localeTag, { style: "currency", currency: "USD", }).format(value); - const formatPeriodEnd = (value: number) => new Intl.DateTimeFormat(localeTag, { - dateStyle: "medium", - }).format(new Date(value > 10_000_000_000 ? value : value * 1000)); + const formatPeriodEnd = (value: number) => { + const date = asDate(value); + return date === null ? null : new Intl.DateTimeFormat(localeTag, { dateStyle: "medium" }).format(date); + }; + const periodEnd = credits?.expiresAt === undefined ? null : formatPeriodEnd(credits.expiresAt); return ( <> @@ -91,19 +103,20 @@ export function ProviderCapacityQuota({ report, pending }: { report: ProviderQuo {formatCredits(credits.remaining)} )} - {credits?.expiresAt !== undefined && ( + {periodEnd !== null && (
- {t("quota.creditsPeriodEnds", { date: formatPeriodEnd(credits.expiresAt) })} + {t("quota.creditsPeriodEnds", { date: periodEnd })}
)} - {recoveryRows.flatMap(({ key, label, window }) => ( - window.nextRecoveryAt !== undefined && window.nextRecoveryPercent !== undefined + {recoveryRows.flatMap(({ key, label, window }) => { + const recoveryAt = window.nextRecoveryAt === undefined ? null : formatRecoveryAt(window.nextRecoveryAt); + return recoveryAt !== null && window.nextRecoveryPercent !== undefined ? [
- {t("pws.capacity.nextRecovery")} · {label} · {formatRecoveryAt(window.nextRecoveryAt)} + {t("pws.capacity.nextRecovery")} · {label} · {recoveryAt} {t("pws.capacity.recoveryShare", { percent: formatPercent(window.nextRecoveryPercent) })}
] - : [] - ))} + : []; + })} {showsAggregate && aggregation && aggregation.currentAccount?.quota && (
diff --git a/gui/src/provider-workspace/report.ts b/gui/src/provider-workspace/report.ts index 539fbba2f2..4d4ff53f3b 100644 --- a/gui/src/provider-workspace/report.ts +++ b/gui/src/provider-workspace/report.ts @@ -39,6 +39,22 @@ const finite = (value: unknown): number | undefined => ( typeof value === "number" && Number.isFinite(value) ? value : undefined ); +/** + * A finite number is not necessarily a representable date. + * + * Reports are also read from persisted cache, so a bogus expiry recorded before the wire-side + * guard existed still reaches the GUI. `Intl.DateTimeFormat.format()` throws a RangeError on an + * out-of-range time value rather than rendering it, which turns one bad provider field into a + * render fault for the whole capacity panel. Drop the field instead: the rest of the credit + * figures stay useful without it. + */ +const dateTimestamp = (value: unknown): number | undefined => { + const timestamp = finite(value); + if (timestamp === undefined) return undefined; + const milliseconds = timestamp > 10_000_000_000 ? timestamp : timestamp * 1000; + return Number.isFinite(new Date(milliseconds).getTime()) ? timestamp : undefined; +}; + function quotaFromUnknown(quota: unknown, fallbackUpdatedAt?: number): AccountQuota | null { if (!quota || typeof quota !== "object" || Array.isArray(quota)) return null; const q = quota as Record; @@ -61,7 +77,7 @@ function quotaFromUnknown(quota: unknown, fallbackUpdatedAt?: number): AccountQu const creditsLimit = finite(creditsRaw?.limit); const creditsRemaining = finite(creditsRaw?.remaining); const creditsPercent = finite(creditsRaw?.percent); - const creditsExpiresAt = finite(creditsRaw?.expiresAt); + const creditsExpiresAt = dateTimestamp(creditsRaw?.expiresAt); const creditsUsd = creditsUsed !== undefined && creditsLimit !== undefined && creditsRemaining !== undefined diff --git a/gui/tests/provider-capacity-credits.test.tsx b/gui/tests/provider-capacity-credits.test.tsx index 35045aa870..38035c97c3 100644 --- a/gui/tests/provider-capacity-credits.test.tsx +++ b/gui/tests/provider-capacity-credits.test.tsx @@ -39,3 +39,13 @@ test("credits with an expiry render the localized billing-period end date", () = expect(markup).toContain("Credits balance"); expect(markup).toContain("Billing period ends 26 Aug 2026"); }); + +// Rendering is the failure point, not just the value: Intl.DateTimeFormat.format() throws a +// RangeError on a time value outside ±8.64e15 ms, so an unrepresentable expiry took down the +// whole capacity panel rather than showing a wrong date. +test("credits with an unrepresentable expiry still render the balance", () => { + const markup = renderCredits(1e20); + expect(markup).toContain("Credits balance"); + expect(markup).toContain("US$37.50"); + expect(markup).not.toContain("Billing period ends"); +}); diff --git a/gui/tests/provider-capacity.test.ts b/gui/tests/provider-capacity.test.ts index 3916b3eadb..5911e89d61 100644 --- a/gui/tests/provider-capacity.test.ts +++ b/gui/tests/provider-capacity.test.ts @@ -61,6 +61,18 @@ test("provider quota reports reject malformed required credits and drop malforme creditsUsd: { used: 12.5, limit: 50, remaining: 37.5, percent: 25 }, updatedAt: 123, }); + + // Persisted reports predate the wire-side guard, so normalization is a second line of + // defence: an unrepresentable expiry must be dropped rather than handed to a formatter. + expect(accountQuotaFromReport({ + updatedAt: 123, + quota: { + creditsUsd: { used: 12.5, limit: 50, remaining: 37.5, percent: 25, expiresAt: 1e20 }, + }, + })).toEqual({ + creditsUsd: { used: 12.5, limit: 50, remaining: 37.5, percent: 25 }, + updatedAt: 123, + }); }); test("capacity metadata preserves estimate, raw current quota, recovery percent, and incomplete coverage", () => { diff --git a/src/providers/quota-wire.ts b/src/providers/quota-wire.ts index 0abcb69efe..31a0c81457 100644 --- a/src/providers/quota-wire.ts +++ b/src/providers/quota-wire.ts @@ -30,7 +30,12 @@ export const QUOTA_JSON_READ_FAILURE = Symbol("quota-json-read-failure"); /** Unix 0 / negative values are sentinels, not reset clocks (Command Code fiveHour.resetAt: 0). */ export function epochMillis(value: number): number | undefined { if (!Number.isFinite(value) || value <= 0) return undefined; - return value > 10_000_000_000 ? value : value * 1000; + const milliseconds = value > 10_000_000_000 ? value : value * 1000; + // A finite number is not necessarily a representable date. ECMAScript caps time values at + // ±8.64e15 ms, and `Intl.DateTimeFormat.format()` throws a RangeError past that instead of + // rendering something wrong. A provider that reports a bogus expiry must not become a + // rendering fault in every consumer that formats it. + return Number.isFinite(new Date(milliseconds).getTime()) ? milliseconds : undefined; } export function normalizeResetAt(value: unknown): number | undefined { diff --git a/src/providers/quota.ts b/src/providers/quota.ts index 98bd079119..e27bb29a31 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -1153,14 +1153,17 @@ export function parseXaiCreditsResponse(value: unknown): { percent: number; rese 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; + let percent = 0; if (config.creditUsagePercent !== undefined) { - const percent = normalizePercent(config.creditUsagePercent); - if (percent === undefined) return null; - return { percent, resetAt }; + const normalized = normalizePercent(config.creditUsagePercent); + if (normalized === undefined) return null; + percent = normalized; } - return { percent: 0, resetAt }; + const resetAt = normalizeResetAt(period.end); + return { + percent, + ...(resetAt !== undefined ? { resetAt } : {}), + }; } async function fetchXaiWeeklyCredits(accessToken: string, userId: string): Promise { diff --git a/tests/command-code-quota.test.ts b/tests/command-code-quota.test.ts index 3e8da6d0d3..8ba447baaf 100644 --- a/tests/command-code-quota.test.ts +++ b/tests/command-code-quota.test.ts @@ -364,6 +364,38 @@ describe("Command Code provider quota", () => { }); }); + // An out-of-range period end is not merely a wrong date: every consumer that formats it + // through Intl throws a RangeError. Drop the field and keep the usable credit figures. + test("an out-of-range subscription period end is dropped, not carried into the report", async () => { + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = String(input); + const body = url.includes("/alpha/whoami") + ? {} + : url.includes("/alpha/billing/subscriptions") + ? { data: { currentPeriodStart: "2026-08-01T00:00:00.000Z", currentPeriodEnd: 1e20 } } + : url.includes("/alpha/usage/summary") + ? { totalCost: 12 } + : { + credits: { monthlyCredits: 0, purchasedCredits: 0, freeCredits: 0 }, + windowLimits: { fiveHour: { cap: 100, used: 40 } }, + }; + return new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(commandCodeConfig(), true); + + expect(result.reports[0]?.quota).toEqual({ + fiveHourPercent: 40, + creditsUsd: { + used: 12, + limit: 12, + remaining: 0, + percent: 100, + }, + updatedAt: expect.any(Number), + }); + }); + test("a mixed balance with roll-over purchased credits carries no subscription expiry", async () => { globalThis.fetch = (async (input: RequestInfo | URL) => { const url = String(input); diff --git a/tests/provider-quota.test.ts b/tests/provider-quota.test.ts index 6d0e395f3d..5eef964306 100644 --- a/tests/provider-quota.test.ts +++ b/tests/provider-quota.test.ts @@ -2393,6 +2393,12 @@ describe("fetchProviderQuotaReports", () => { percent: 0, resetAt: Date.parse("2026-08-15T13:05:52.277209Z"), }); + expect(parseXaiCreditsResponse({ + config: { + creditUsagePercent: 42, + currentPeriod: { type: "USAGE_PERIOD_TYPE_WEEKLY", end: 1e20 }, + }, + })).toEqual({ percent: 42 }); expect(parseXaiCreditsResponse({ config: { creditUsagePercent: 10,