Skip to content
2 changes: 1 addition & 1 deletion src/oauth/generic-account-failover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export function rotateGenericOAuthAccountOn429(
// An account whose allowance is provably spent gets a reset-aligned cooldown instead of
// the default minute: retrying it every 60s until the window rolls over is pure waste.
// A Retry-After from upstream still wins — it is the server's own instruction.
const exhausted = parsed === null ? exhaustedCooldownMs(providerName, failedAccountId, now) : null;
const exhausted = parsed === undefined ? exhaustedCooldownMs(providerName, failedAccountId, now) : null;
const cooldownMs = exhausted ?? Math.min(parsed ?? DEFAULT_COOLDOWN_MS, MAX_COOLDOWN_MS);
health.set(healthKey(providerName, failedAccountId), {
cooldownUntil: now + cooldownMs,
Expand Down
34 changes: 34 additions & 0 deletions tests/kiro-pool-rank.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import {
clearGenericFailoverHealth,
eligibleFailoverAccounts,
forgetGenericFailoverRoster,
preferredInitialAccount,
rotateGenericOAuthAccountOn429,
Expand Down Expand Up @@ -106,6 +107,39 @@ describe("headroom ranking", () => {
});

describe("exhaustion cooldown", () => {
test("rotation uses the reset-aligned cooldown when Retry-After is absent", async () => {
const originalHome = process.env.OPENCODEX_HOME;
const home = mkdtempSync(join(tmpdir(), "ocx-kiro-exhaustion-"));
process.env.OPENCODEX_HOME = home;
clearGenericFailoverHealth();
try {
for (let i = 0; i < 2; i++) {
await saveCredential("kiro", {
access: `access-${i}`,
refresh: `refresh-${i}`,
expires: Date.now() + 3_600_000,
accountId: `uuid-${i}`,
} as never, { addAccount: true });
}
const ids = getAccountSet("kiro")?.accounts.map(account => account.id) ?? [];
const now = Date.now();
seedExhausted(ids[0]!, now + 60 * 60_000);
const config = {
providers: {
kiro: { adapter: "openai-chat", authMode: "oauth" },
},
} as unknown as OcxConfig;

expect(rotateGenericOAuthAccountOn429(config, "kiro", ids[0]!, null, now)).toBe(ids[1]);
expect(eligibleFailoverAccounts("kiro", now + 60_001)).toEqual([ids[1]!]);
} finally {
clearGenericFailoverHealth();
if (originalHome === undefined) delete process.env.OPENCODEX_HOME;
else process.env.OPENCODEX_HOME = originalHome;
rmSync(home, { recursive: true, force: true });
}
});

test("a distant reset is clamped to a day", () => {
const now = Date.now();
seedExhausted("a", now + 3 * 24 * 60 * 60_000);
Expand Down
Loading