From a4087afa2208ca80c1397c7f6a8235e54971e5c6 Mon Sep 17 00:00:00 2001 From: Sungyong Cho Date: Sun, 4 Oct 2026 12:46:43 +0200 Subject: [PATCH] fix(codex): permit spendable credits when subscription rate limit is exhausted --- src/codex/quota-types.ts | 3 +-- src/codex/quota.ts | 3 +-- .../codex-credits-after-limit-main.test.ts | 20 ++++++++++++++++++- .../codex-quota-parser-parity.test.ts | 6 +++--- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/codex/quota-types.ts b/src/codex/quota-types.ts index b159402d7d6..0270645dbed 100644 --- a/src/codex/quota-types.ts +++ b/src/codex/quota-types.ts @@ -30,7 +30,6 @@ export interface CodexSpendableCredits { unlimited?: boolean; balance?: number; overageLimitReached?: boolean; - allowed?: boolean; observedAt: number; } @@ -42,7 +41,7 @@ export function hasSpendableCodexCredits( if (!credits || !Number.isFinite(credits.observedAt)) return false; const age = now - credits.observedAt; if (age < 0 || age > CODEX_CREDITS_FRESHNESS_MS - || credits.allowed === false || credits.overageLimitReached === true || credits.hasCredits === false) return false; + || credits.overageLimitReached === true || credits.hasCredits === false) return false; return credits.unlimited === true || (credits.hasCredits === true && typeof credits.balance === "number" && Number.isFinite(credits.balance) && credits.balance > 0); } diff --git a/src/codex/quota.ts b/src/codex/quota.ts index 16e5bf65985..42e8fc5db70 100644 --- a/src/codex/quota.ts +++ b/src/codex/quota.ts @@ -722,7 +722,7 @@ function readMainPolicyQuota(value: unknown): MainPolicyQuota | null { const stored = raw.credits as Record; if (typeof stored.observedAt === "number" && Number.isFinite(stored.observedAt) && stored.observedAt >= 0) { quota.credits = { observedAt: stored.observedAt }; - for (const key of ["hasCredits", "unlimited", "overageLimitReached", "allowed"] as const) { + for (const key of ["hasCredits", "unlimited", "overageLimitReached"] as const) { if (typeof stored[key] === "boolean") quota.credits[key] = stored[key]; } if (typeof stored.balance === "number" && Number.isFinite(stored.balance) && stored.balance >= 0) { @@ -926,7 +926,6 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit { .resolves.toMatchObject({ kind: "main-pool", accountId: MAIN }); }); +test("main account with spendable credits parsed from WHAM with rate_limit.allowed false keeps serving (#6571)", async () => { + const cfg = config(); + setCodexAccountCreditsAfterLimit(cfg, MAIN, true); + const writer = captureMainQuotaWriter(accountId)!; + const parsed = parseUsageQuota({ + plan_type: "pro", + rate_limit: { + allowed: false, + limit_reached: true, + primary_window: { used_percent: 100, limit_window_seconds: 604800, reset_at: Math.floor((Date.now() + DAY_MS) / 1000) }, + }, + credits: { has_credits: true, unlimited: false, overage_limit_reached: false, balance: "42.5" }, + }); + setAccountQuotaFromParsed(MAIN, parsed, undefined, writer); + await expect(resolveCodexAuthContext(new Headers(), cfg, "pool")) + .resolves.toMatchObject({ kind: "main-pool", accountId: MAIN }); +}); + test("main credit consent without a fresh balance cannot release a full window", async () => { const cfg = config(); setCodexAccountCreditsAfterLimit(cfg, MAIN, true); diff --git a/tests/codex-integration/codex-quota-parser-parity.test.ts b/tests/codex-integration/codex-quota-parser-parity.test.ts index ee9b9f9c46b..8d346c33e3d 100644 --- a/tests/codex-integration/codex-quota-parser-parity.test.ts +++ b/tests/codex-integration/codex-quota-parser-parity.test.ts @@ -15,7 +15,7 @@ import { computeCodexUsageScore } from "../../src/codex/routing/cooldown-math"; import type { WhamUsageResponse } from "../../src/codex/quota"; describe("consumable credits at an included usage limit", () => { - const wham = (credits: unknown, allowed = true): WhamUsageResponse => ({ + const wham = (credits: unknown, allowed = false): WhamUsageResponse => ({ plan_type: "pro", rate_limit: { allowed, primary_window: { used_percent: 100, limit_window_seconds: 604800 } }, rate_limit_reset_credits: { available_count: 0 }, @@ -63,8 +63,8 @@ describe("consumable credits at an included usage limit", () => { expect(computeCodexUsageScore(quota, "pro", Date.now(), true)).toBe(100); }); - it("honors an explicit upstream refusal even with a positive balance", () => { - expect(isCodexQuotaExhausted(parseUsageQuota(wham(available, false)), "pro", true)).toBe(true); + it("keeps an account with spendable credits selectable when the included limit is exhausted (#6571)", () => { + expect(isCodexQuotaExhausted(parseUsageQuota(wham(available, false)), "pro", true)).toBe(false); }); it("reset tickets alone do not grant automatic spending headroom", () => {