From bfd2d5603cecaa36ec78cd1f240c11fe1ca46354 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Wed, 5 Aug 2026 22:40:33 -0300 Subject: [PATCH] fix(codex): preserve quota window duration (#9222) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates — only pre-existing audit.test.ts flake). --- .../fixes/9222-codex-quota-window-duration.md | 1 + open-sse/services/codexUsageQuotas.ts | 28 ++++++++-- tests/unit/codex-usage-windows.test.ts | 52 +++++++++++++++++++ 3 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 changelog.d/fixes/9222-codex-quota-window-duration.md create mode 100644 tests/unit/codex-usage-windows.test.ts diff --git a/changelog.d/fixes/9222-codex-quota-window-duration.md b/changelog.d/fixes/9222-codex-quota-window-duration.md new file mode 100644 index 0000000000..2f2e7dd2b7 --- /dev/null +++ b/changelog.d/fixes/9222-codex-quota-window-duration.md @@ -0,0 +1 @@ +- **fix(codex):** preserve quota window duration in usage shape. (thanks @HectorBernstorff) diff --git a/open-sse/services/codexUsageQuotas.ts b/open-sse/services/codexUsageQuotas.ts index 4d4ed9c229..e730e9279d 100644 --- a/open-sse/services/codexUsageQuotas.ts +++ b/open-sse/services/codexUsageQuotas.ts @@ -13,6 +13,7 @@ export type CodexUsageQuota = { remaining?: number; resetAt: string | null; unlimited: boolean; + windowSeconds: number | null; displayName?: string; }; @@ -38,6 +39,15 @@ function toNumber(value: unknown, fallback = 0): number { return fallback; } +function toNullableNumber(value: unknown): number | null { + if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "string" && value.trim().length > 0) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; + } + return null; +} + function parseResetTime(resetValue: unknown): string | null { if (!resetValue) return null; try { @@ -81,6 +91,15 @@ function buildPercentageQuota(window: JsonRecord, displayName?: string): CodexUs remaining: 100 - usedPercent, resetAt: parseWindowReset(window), unlimited: false, + windowSeconds: toNullableNumber( + getFieldValue( + window, + "limit_window_seconds", + "limitWindowSeconds", + "window_seconds", + "windowSeconds" + ) + ), ...(displayName ? { displayName } : {}), }; } @@ -105,10 +124,7 @@ function isLatentWindow(window: JsonRecord): boolean { getFieldValue(window, "limit_window_seconds", "limitWindowSeconds"), 0 ); - const resetAfter = toNumber( - getFieldValue(window, "reset_after_seconds", "resetAfterSeconds"), - 0 - ); + const resetAfter = toNumber(getFieldValue(window, "reset_after_seconds", "resetAfterSeconds"), 0); return usedPercent === 0 && limitWindow > 0 && resetAfter >= limitWindow; } @@ -225,7 +241,9 @@ function findCodexReviewRateLimit(data: JsonRecord): JsonRecord { * (issue #5199). */ function parseBankedResetCredits(data: JsonRecord): number | undefined { - const resetCredits = toRecord(getFieldValue(data, "rate_limit_reset_credits", "rateLimitResetCredits")); + const resetCredits = toRecord( + getFieldValue(data, "rate_limit_reset_credits", "rateLimitResetCredits") + ); const availableCount = getFieldValue(resetCredits, "available_count", "availableCount"); const count = toNumber(availableCount, NaN); return Number.isFinite(count) ? count : undefined; diff --git a/tests/unit/codex-usage-windows.test.ts b/tests/unit/codex-usage-windows.test.ts new file mode 100644 index 0000000000..e45c51c562 --- /dev/null +++ b/tests/unit/codex-usage-windows.test.ts @@ -0,0 +1,52 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { buildCodexUsageQuotas } from "../../open-sse/services/codexUsageQuotas"; + +describe("Codex usage windows", () => { + it("preserves upstream durations for session and weekly windows", () => { + const { quotas } = buildCodexUsageQuotas({ + rate_limit: { + primary_window: { + used_percent: 7, + limit_window_seconds: 18_000, + reset_at: 1_785_623_016, + }, + secondary_window: { + used_percent: 19, + limit_window_seconds: 604_800, + reset_at: 1_785_678_428, + }, + }, + }); + + assert.deepEqual(quotas.session, { + used: 7, + total: 100, + remaining: 93, + resetAt: new Date(1_785_623_016_000).toISOString(), + unlimited: false, + windowSeconds: 18_000, + }); + assert.deepEqual(quotas.weekly, { + used: 19, + total: 100, + remaining: 81, + resetAt: new Date(1_785_678_428_000).toISOString(), + unlimited: false, + windowSeconds: 604_800, + }); + }); + + it("accepts camelCase duration variants and nulls invalid values", () => { + const { quotas } = buildCodexUsageQuotas({ + rateLimit: { + primaryWindow: { usedPercent: 3, windowSeconds: "18000" }, + secondaryWindow: { usedPercent: 4, windowSeconds: "not-a-number" }, + }, + }); + + assert.equal(quotas.session.windowSeconds, 18_000); + assert.equal(quotas.weekly.windowSeconds, null); + }); +});