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
3 changes: 1 addition & 2 deletions src/codex/quota-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export interface CodexSpendableCredits {
unlimited?: boolean;
balance?: number;
overageLimitReached?: boolean;
allowed?: boolean;
observedAt: number;
}

Expand All @@ -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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[P2] Preserve the credit-specific spending-control refusal. A fresh WHAM response with rate_limit.allowed=false, primary_window.used_percent=100, credits={has_credits:true,balance:"42.5",overage_limit_reached:false}, and spend_control.reached=true is now treated as spendable: parseUsageQuota(...) followed by isCodexQuotaExhausted(quota, "pro", true) returns false. That is not the #6571 case (which has reached=false); the spending control explicitly says the credit limit has been reached. The parser removes the included-plan flag but never reads this separate control, and this predicate no longer has a credit veto. Keep included-plan exhaustion non-vetoing while normalizing and honoring the credit-specific spending verdict; add raw-WHAM/main-admission regressions for reached=false versus true and invalid declared values. Please update the owning structure credit-policy docs too, rather than retaining the checked docs assertion with no doc changes.

|| 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);
}
Expand Down
3 changes: 1 addition & 2 deletions src/codex/quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,7 @@ function readMainPolicyQuota(value: unknown): MainPolicyQuota | null {
const stored = raw.credits as Record<string, unknown>;
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) {
Expand Down Expand Up @@ -926,7 +926,6 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot
...(parsed.hasCredits !== undefined ? { hasCredits: parsed.hasCredits } : {}),
...(parsed.unlimited !== undefined ? { unlimited: parsed.unlimited } : {}),
...(parsed.overageLimitReached !== undefined ? { overageLimitReached: parsed.overageLimitReached } : {}),
...(typeof data.rate_limit?.allowed === "boolean" ? { allowed: data.rate_limit.allowed } : {}),
...(balance !== undefined && Number.isFinite(balance) ? { balance } : {}),
};
}
Expand Down
20 changes: 19 additions & 1 deletion tests/codex-integration/codex-credits-after-limit-main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { reconcileMainCodexAccountRuntimeState, resetMainCodexAccountIdentityTra
import * as mainAccount from "../../src/codex/main-account";
import * as authCollision from "../../src/codex/auth-collision";
import { captureMainQuotaWriter, observeMainQuotaCredential } from "../../src/codex/main-account-cache";
import { clearAccountQuota, setAccountQuotaFromParsed } from "../../src/codex/quota";
import { clearAccountQuota, parseUsageQuota, setAccountQuotaFromParsed } from "../../src/codex/quota";
import { clearCodexUpstreamHealth, clearThreadAccountMap, pickLowestUsageCodexAccount } from "../../src/codex/routing";
import { setIcaclsRunnerForTests } from "../../src/lib/windows-secret-acl";
import type { OcxConfig } from "../../src/types";
Expand Down Expand Up @@ -122,6 +122,24 @@ test("a full main login allowed to use credits keeps serving", async () => {
.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);
Expand Down
6 changes: 3 additions & 3 deletions tests/codex-integration/codex-quota-parser-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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", () => {
Expand Down
Loading