Merge PR #6038: surface Codex banked reset credits per account (#5199)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-03 02:01:17 -03:00
6 changed files with 294 additions and 3 deletions

View File

@@ -49,6 +49,13 @@ export interface CodexDualWindowQuota extends QuotaInfo {
limitReached: boolean;
/** All known Codex quota windows, including Spark when the upstream exposes it. */
allWindows?: Record<string, { percentUsed: number; resetAt: string | null }>;
/**
* Banked reset credits available on the account (display-only, issue #5199).
* Eligibility-gated: absent for most accounts. Never throws when missing.
*/
bankedResetCredits?: number;
/** Which window is currently reported as blocking, when the upstream exposes it. */
rateLimitReachedType?: string;
}
interface CacheEntry {
@@ -295,6 +302,26 @@ function parseCodexWindow(
return { percentUsed, resetAt: parseWindowReset(window) };
}
/**
* Codex "banked reset credits" — same eligibility-gated field parsed in
* codexUsageQuotas.ts (kept in sync manually; the two parsers read the same
* /wham/usage payload independently). DISPLAY ONLY, never throws.
*/
function parseBankedResetCredits(data: Record<string, unknown>): number | undefined {
const resetCredits = toRecord(data["rate_limit_reset_credits"] ?? data["rateLimitResetCredits"]);
const availableCount = resetCredits["available_count"] ?? resetCredits["availableCount"];
const count = toNumber(availableCount, NaN);
return Number.isFinite(count) ? count : undefined;
}
function parseRateLimitReachedType(data: Record<string, unknown>): string | undefined {
const reachedType = data["rate_limit_reached_type"] ?? data["rateLimitReachedType"];
if (typeof reachedType === "string" && reachedType.trim().length > 0) return reachedType.trim();
const reachedTypeObj = toRecord(reachedType);
const type = reachedTypeObj["type"];
return typeof type === "string" && type.trim().length > 0 ? type.trim() : undefined;
}
function findSparkRateLimit(data: Record<string, unknown>): Record<string, unknown> | null {
const additional = data["additional_rate_limits"] ?? data["additionalRateLimits"];
if (!Array.isArray(additional)) return null;
@@ -403,6 +430,9 @@ function parseCodexUsageResponse(
secondary: CODEX_WINDOW_WEEKLY,
});
const bankedResetCredits = parseBankedResetCredits(obj);
const rateLimitReachedType = parseRateLimitReachedType(obj);
return {
used: Math.round(worstPercentUsed * 100),
total: 100,
@@ -419,6 +449,9 @@ function parseCodexUsageResponse(
window5h,
window7d,
limitReached,
// Banked reset credits (display-only, eligibility-gated — issue #5199).
...(bankedResetCredits !== undefined ? { bankedResetCredits } : {}),
...(rateLimitReachedType !== undefined ? { rateLimitReachedType } : {}),
};
}

View File

@@ -160,13 +160,43 @@ function findCodexReviewRateLimit(data: JsonRecord): JsonRecord {
return {};
}
/**
* Codex "banked reset credits" — an eligibility-gated field some ChatGPT plans
* expose on the /wham/usage payload: a count of extra rate-limit resets the
* account has banked (available_count), plus an optional descriptor of which
* window is currently blocking (rate_limit_reached_type). DISPLAY ONLY — this
* reads the field defensively (many accounts will not have it) and never
* throws; redemption is an unofficial mutating endpoint and out of scope
* (issue #5199).
*/
function parseBankedResetCredits(data: JsonRecord): number | undefined {
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;
}
function parseRateLimitReachedType(data: JsonRecord): string | undefined {
const reachedType = getFieldValue(data, "rate_limit_reached_type", "rateLimitReachedType");
if (typeof reachedType === "string" && reachedType.trim().length > 0) return reachedType.trim();
const reachedTypeObj = toRecord(reachedType);
const type = getFieldValue(reachedTypeObj, "type");
return typeof type === "string" && type.trim().length > 0 ? type.trim() : undefined;
}
export function buildCodexUsageQuotas(dataValue: unknown): {
rateLimit: JsonRecord;
quotas: Record<string, CodexUsageQuota>;
/** Banked reset credits available on the account (undefined when absent/not eligible). */
bankedResetCredits?: number;
/** Which window is currently reported as blocking, when the upstream exposes it. */
rateLimitReachedType?: string;
} {
const data = toRecord(dataValue);
const rateLimit = toRecord(getFieldValue(data, "rate_limit", "rateLimit"));
const quotas: Record<string, CodexUsageQuota> = {};
const bankedResetCredits = parseBankedResetCredits(data);
const rateLimitReachedType = parseRateLimitReachedType(data);
const primaryWindow = toRecord(getFieldValue(rateLimit, "primary_window", "primaryWindow"));
if (Object.keys(primaryWindow).length > 0) quotas.session = buildPercentageQuota(primaryWindow);
@@ -231,5 +261,10 @@ export function buildCodexUsageQuotas(dataValue: unknown): {
);
}
return { rateLimit, quotas };
return {
rateLimit,
quotas,
...(bankedResetCredits !== undefined ? { bankedResetCredits } : {}),
...(rateLimitReachedType !== undefined ? { rateLimitReachedType } : {}),
};
}

View File

@@ -57,12 +57,17 @@ export async function getCodexUsage(
const data = await response.json();
const { rateLimit, quotas } = buildCodexUsageQuotas(data);
const { rateLimit, quotas, bankedResetCredits, rateLimitReachedType } =
buildCodexUsageQuotas(data);
return {
plan: String(getFieldValue(data, "plan_type", "planType") || "unknown"),
limitReached: Boolean(getFieldValue(rateLimit, "limit_reached", "limitReached")),
quotas,
// Banked reset credits (display-only, eligibility-gated — issue #5199).
// Absent for most accounts; never throws when the upstream omits it.
...(bankedResetCredits !== undefined ? { bankedResetCredits } : {}),
...(rateLimitReachedType !== undefined ? { rateLimitReachedType } : {}),
};
} catch (error) {
return { message: `Failed to fetch Codex usage: ${(error as Error).message}` };

View File

@@ -110,13 +110,30 @@ function parseAntigravity(data: any) {
.filter(Boolean);
}
/**
* Codex "banked reset credits" — an eligibility-gated field (issue #5199):
* a count of extra rate-limit resets the account has banked. DISPLAY ONLY
* (redemption is an unofficial mutating endpoint, out of scope). Most
* accounts won't have this field; only render it when the count is positive.
*/
function buildBankedResetCreditsQuota(count: number) {
return buildCreditsQuota("banked_reset_credits", count, 100, { currency: "" });
}
function parseCodex(data: any) {
return quotaEntries(data).map(([quotaType, quota]) =>
const quotas = quotaEntries(data).map(([quotaType, quota]) =>
normalizeQuotaEntry(quotaType, quota, {
displayName: quota?.displayName,
isPercentageOnly: true,
})
);
const bankedResetCredits = Number(data?.bankedResetCredits);
if (Number.isFinite(bankedResetCredits) && bankedResetCredits > 0) {
quotas.push(buildBankedResetCreditsQuota(bankedResetCredits));
}
return quotas;
}
function parseClaude(data: any) {

View File

@@ -33,6 +33,7 @@ const QUOTA_LABEL_MAP: Record<string, string> = {
"Monthly Tools": "Monthly Tools",
tokens: "Tokens",
time_limit: "Time Limit",
banked_reset_credits: "Banked Reset Credits",
};
function toRecord(value: unknown): Record<string, unknown> {

View File

@@ -0,0 +1,200 @@
/**
* Regression test for Codex "banked reset credits" (issue #5199).
*
* DISPLAY ONLY: OmniRoute already calls the ChatGPT backend
* `/backend-api/wham/usage` endpoint for quota tracking. Some eligibility-gated
* accounts additionally expose `rate_limit_reset_credits.available_count` (a
* count of extra rate-limit resets banked on the account) and, optionally,
* `rate_limit_reached_type` (which window is currently blocking). This test
* verifies:
* 1. The field is parsed and surfaced additively when present, across both
* independent parsers that read this payload (codexUsageQuotas.ts used by
* the dashboard usage fetcher, and codexQuotaFetcher.ts used by the
* preflight/monitor fetcher).
* 2. Existing quota parsing is completely unaffected when the field is
* absent (fail-open — no throw, no regression to session/weekly/etc).
*
* Redemption of banked reset credits is an unofficial, mutating upstream
* endpoint and is explicitly OUT OF SCOPE — this only reads and surfaces data
* already present in the existing usage-fetch response.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { buildCodexUsageQuotas } from "../../open-sse/services/codexUsageQuotas.ts";
import { getCodexUsage } from "../../open-sse/services/usage/codex.ts";
import {
fetchCodexQuota,
invalidateCodexQuotaCache,
registerCodexConnection,
unregisterCodexConnection,
} from "../../open-sse/services/codexQuotaFetcher.ts";
const originalFetch = globalThis.fetch;
test.afterEach(() => {
globalThis.fetch = originalFetch;
});
// ─── codexUsageQuotas.ts (dashboard usage-fetch path) ──────────────────────
test("buildCodexUsageQuotas surfaces bankedResetCredits when present", () => {
const { quotas, bankedResetCredits, rateLimitReachedType } = buildCodexUsageQuotas({
rate_limit: {
primary_window: { used_percent: 10 },
secondary_window: { used_percent: 20 },
},
rate_limit_reset_credits: { available_count: 3 },
rate_limit_reached_type: { type: "secondary_window" },
});
assert.equal(bankedResetCredits, 3);
assert.equal(rateLimitReachedType, "secondary_window");
// Existing quotas remain intact.
assert.equal(quotas.session?.used, 10);
assert.equal(quotas.weekly?.used, 20);
});
test("buildCodexUsageQuotas tolerates camelCase field shape", () => {
const { bankedResetCredits, rateLimitReachedType } = buildCodexUsageQuotas({
rateLimit: { primaryWindow: { usedPercent: 5 } },
rateLimitResetCredits: { availableCount: 7 },
rateLimitReachedType: "primary_window",
});
assert.equal(bankedResetCredits, 7);
assert.equal(rateLimitReachedType, "primary_window");
});
test("buildCodexUsageQuotas leaves bankedResetCredits undefined when absent (fail-open)", () => {
const result = buildCodexUsageQuotas({
rate_limit: {
primary_window: { used_percent: 10 },
secondary_window: { used_percent: 20 },
},
});
assert.equal(result.bankedResetCredits, undefined);
assert.equal(result.rateLimitReachedType, undefined);
// Existing quota parsing is unaffected — no throw, no missing windows.
assert.equal(result.quotas.session?.used, 10);
assert.equal(result.quotas.weekly?.used, 20);
});
test("buildCodexUsageQuotas never throws on a garbage rate_limit_reset_credits shape", () => {
assert.doesNotThrow(() => {
const result = buildCodexUsageQuotas({
rate_limit: { primary_window: { used_percent: 1 } },
rate_limit_reset_credits: "not-an-object",
rate_limit_reached_type: 12345,
});
assert.equal(result.bankedResetCredits, undefined);
assert.equal(result.rateLimitReachedType, undefined);
});
});
// ─── usage/codex.ts (getCodexUsage — full dashboard fetch) ─────────────────
test("getCodexUsage threads bankedResetCredits through additively", async () => {
globalThis.fetch = async () =>
new Response(
JSON.stringify({
plan_type: "plus",
rate_limit: {
primary_window: { used_percent: 15 },
secondary_window: { used_percent: 25 },
},
rate_limit_reset_credits: { available_count: 2 },
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
const usage = await getCodexUsage("token", { workspaceId: "ws-1" });
assert.equal((usage as any).plan, "plus");
assert.equal((usage as any).bankedResetCredits, 2);
assert.equal((usage as any).quotas.session.used, 15);
assert.equal((usage as any).quotas.weekly.used, 25);
});
test("getCodexUsage omits bankedResetCredits and stays intact when absent", async () => {
globalThis.fetch = async () =>
new Response(
JSON.stringify({
plan_type: "plus",
rate_limit: {
primary_window: { used_percent: 15 },
secondary_window: { used_percent: 25 },
},
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
const usage = await getCodexUsage("token", { workspaceId: "ws-1" });
assert.equal("bankedResetCredits" in (usage as any), false);
assert.equal((usage as any).quotas.session.used, 15);
assert.equal((usage as any).quotas.weekly.used, 25);
});
// ─── codexQuotaFetcher.ts (preflight/monitor path) ─────────────────────────
test("fetchCodexQuota surfaces bankedResetCredits from the dual-window parser", async () => {
const connectionId = `codex-banked-${Date.now()}`;
globalThis.fetch = async () =>
new Response(
JSON.stringify({
rate_limit: {
primary_window: { used_percent: 70, reset_after_seconds: 45 },
secondary_window: { used_percent: 20, reset_after_seconds: 300 },
},
rate_limit_reset_credits: { available_count: 4 },
rate_limit_reached_type: { type: "primary_window" },
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
const quota = await fetchCodexQuota(connectionId, {
accessToken: "token",
providerSpecificData: { workspaceId: "ws" },
});
assert.ok(quota);
assert.equal(quota?.bankedResetCredits, 4);
assert.equal(quota?.rateLimitReachedType, "primary_window");
// Existing dual-window parsing stays intact.
assert.equal(quota?.window5h.percentUsed, 0.7);
assert.equal(quota?.window7d.percentUsed, 0.2);
invalidateCodexQuotaCache(connectionId);
unregisterCodexConnection(connectionId);
});
test("fetchCodexQuota omits bankedResetCredits when the payload does not have it (fail-open)", async () => {
const connectionId = `codex-nobanked-${Date.now()}`;
registerCodexConnection(connectionId, { accessToken: "token" });
globalThis.fetch = async () =>
new Response(
JSON.stringify({
rate_limit: {
primary_window: { used_percent: 30 },
secondary_window: { used_percent: 10 },
},
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
const quota = await fetchCodexQuota(connectionId);
assert.ok(quota);
assert.equal(quota?.bankedResetCredits, undefined);
assert.equal(quota?.rateLimitReachedType, undefined);
assert.equal(quota?.window5h.percentUsed, 0.3);
assert.equal(quota?.window7d.percentUsed, 0.1);
invalidateCodexQuotaCache(connectionId);
unregisterCodexConnection(connectionId);
});