Skip to content
Merged
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
21 changes: 16 additions & 5 deletions gui/src/codex-quota-utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
export interface AccountQuota {
weeklyPercent?: number;
fiveHourPercent?: number;
/** Codex account API aliases for the same five-hour window. */
shortPercent?: number;
monthlyPercent?: number;
weeklyResetAt?: number;
fiveHourResetAt?: number;
shortResetAt?: number;
monthlyResetAt?: number;
customWindows?: { label: string; percent: number; resetAt?: number }[];
resetCredits?: number;
Expand All @@ -16,11 +19,19 @@ export function isThirtyDayOnlyPlan(plan: string | null | undefined): boolean {
}

export function normalizeQuotaForPlan(quota: AccountQuota | null, plan: string | null | undefined): AccountQuota | null {
if (!quota || !isThirtyDayOnlyPlan(plan)) return quota;
if (!quota) return null;
const normalized = quota.shortPercent === undefined && quota.shortResetAt === undefined
? quota
: {
...quota,
fiveHourPercent: quota.fiveHourPercent ?? quota.shortPercent,
fiveHourResetAt: quota.fiveHourResetAt ?? quota.shortResetAt,
Comment on lines +27 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify the short-window duration before labeling it five-hour

When WHAM supplies any explicitly sub-day primary window, src/codex/quota.ts stores it as shortPercent and retains shortWindowSeconds precisely because the slot is not duration-stable; this mapping instead converts every such value to the GUI's fixed “five-hour” row. A one-hour or twelve-hour limit would consequently be shown with the wrong duration. Carry shortWindowSeconds into AccountQuota and only use the five-hour alias when the duration is five hours, or render a duration-aware burst label.

AGENTS.md reference: gui/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

};
if (!isThirtyDayOnlyPlan(plan)) return normalized;
return {
...(quota.monthlyPercent !== undefined ? { monthlyPercent: quota.monthlyPercent } : {}),
...(quota.monthlyResetAt !== undefined ? { monthlyResetAt: quota.monthlyResetAt } : {}),
...(quota.resetCredits !== undefined ? { resetCredits: quota.resetCredits } : {}),
updatedAt: quota.updatedAt,
...(normalized.monthlyPercent !== undefined ? { monthlyPercent: normalized.monthlyPercent } : {}),
...(normalized.monthlyResetAt !== undefined ? { monthlyResetAt: normalized.monthlyResetAt } : {}),
...(normalized.resetCredits !== undefined ? { resetCredits: normalized.resetCredits } : {}),
Comment on lines +30 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve burst windows for 30-day plans

For a Go or Free account whose API quota contains both a healthy monthly window and an exhausted shortPercent, this branch discards the canonicalized short window even though src/codex/auth-api.ts deliberately preserves it and isCodexQuotaExhausted treats it as independently blocking on every plan. The account card therefore hides the limit responsible for exclusion or 429 responses and can appear healthy; retain the short/five-hour fields in the 30-day projection and replace the regression assertion that currently requires stripping them.

AGENTS.md reference: gui/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

updatedAt: normalized.updatedAt,
};
}
37 changes: 37 additions & 0 deletions tests/rate-limit-reset-credits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,43 @@ describe("rate-limit reset credits", () => {
expect(normalizeQuotaForPlan(quota, "pro")).toBe(quota);
});

/**
* The five-hour window reaches the GUI under TWO names (#2616).
*
* `QuotaBars` reads `fiveHour*`, which is what the per-account provider probe reports. The
* Codex pool declares the same window as `short*` (auth-api.ts, and `account-api.ts` says
* outright that "the two surfaces name the same idea differently and both reach this DTO").
* A Codex account card therefore had a five-hour quota upstream and rendered no five-hour
* row at all.
*
* The canonical name wins when both are present: `short*` is an alias to fall back to, not
* an override, or a pool snapshot could quietly replace a probe reading.
*/
it("renders the Codex pool's short window as the five-hour window (#2616)", async () => {
const { normalizeQuotaForPlan } = await import("../gui/src/codex-quota-utils");

expect(normalizeQuotaForPlan({ shortPercent: 71, shortResetAt: 555, updatedAt: 1 }, "pro"))
.toMatchObject({ fiveHourPercent: 71, fiveHourResetAt: 555 });

// Canonical values win; the alias does not overwrite a probe reading.
expect(normalizeQuotaForPlan(
{ fiveHourPercent: 10, fiveHourResetAt: 111, shortPercent: 71, shortResetAt: 555, updatedAt: 1 },
"pro",
)).toMatchObject({ fiveHourPercent: 10, fiveHourResetAt: 111 });

// A quota carrying neither alias is still returned by identity, so the common path adds
// no allocation and no behavior change.
const untouched = { weeklyPercent: 5, updatedAt: 2 };
expect(normalizeQuotaForPlan(untouched, "pro")).toBe(untouched);

// A 30-day plan still strips non-monthly windows, alias or not — #1791's burst-window
// carve-out lives on the server DTO, not in this GUI normalizer.
expect(normalizeQuotaForPlan(
{ shortPercent: 71, shortResetAt: 555, monthlyPercent: 12, updatedAt: 3 },
"go",
)).toEqual({ monthlyPercent: 12, updatedAt: 3 });
});

it("does not exclude team or workspace plans from ticket badges", async () => {
const [pool, helpers] = await Promise.all([
Bun.file("gui/src/components/CodexAccountPool.tsx").text(),
Expand Down
Loading