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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/pr2950-capacity-expiry.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 27 additions & 14 deletions gui/src/components/provider-workspace/ProviderCapacityQuota.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
Expand All @@ -91,19 +103,20 @@ export function ProviderCapacityQuota({ report, pending }: { report: ProviderQuo
<strong>{formatCredits(credits.remaining)}</strong>
</div>
)}
{credits?.expiresAt !== undefined && (
{periodEnd !== null && (
<div className="pws-capacity-recovery">
<span>{t("quota.creditsPeriodEnds", { date: formatPeriodEnd(credits.expiresAt) })}</span>
<span>{t("quota.creditsPeriodEnds", { date: periodEnd })}</span>
</div>
)}
{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
? [<div className="pws-capacity-recovery" key={key}>
<span>{t("pws.capacity.nextRecovery")} · {label} · {formatRecoveryAt(window.nextRecoveryAt)}</span>
<span>{t("pws.capacity.nextRecovery")} · {label} · {recoveryAt}</span>
<strong>{t("pws.capacity.recoveryShare", { percent: formatPercent(window.nextRecoveryPercent) })}</strong>
</div>]
: []
))}
: [];
})}
{showsAggregate && aggregation && aggregation.currentAccount?.quota && (
<div className="pws-capacity-current">
<span className="pws-capacity-label">
Expand Down
18 changes: 17 additions & 1 deletion gui/src/provider-workspace/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions gui/tests/provider-capacity-credits.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
12 changes: 12 additions & 0 deletions gui/tests/provider-capacity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
7 changes: 6 additions & 1 deletion src/providers/quota-wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 9 additions & 6 deletions src/providers/quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProviderQuota | null> {
Expand Down
32 changes: 32 additions & 0 deletions tests/command-code-quota.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions tests/provider-quota.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading