fix(auth): rotate codex accounts before model fallback

This commit is contained in:
cagedbird043
2026-07-08 01:39:41 +08:00
parent 4a48a03506
commit 0e259bb640
5 changed files with 82 additions and 7 deletions
+3 -1
View File
@@ -1,6 +1,7 @@
import type { OAuthAccess } from "./auth-storage";
import * as AIError from "./error";
import { isAuthRetryableError } from "./error/auth-classify";
import { isUsageLimit } from "./error/flags";
/**
* Context passed to an {@link ApiKeyResolver} on each resolution attempt.
@@ -92,7 +93,8 @@ export async function resolveRetryKey(
previousKey?: string,
): Promise<string | undefined> {
try {
return (await resolver({ lastChance, error, signal, previousKey })) || undefined;
const rotateSibling = lastChance || (!lastChance && isUsageLimit(error));
return (await resolver({ lastChance: rotateSibling, error, signal, previousKey })) || undefined;
} catch {
return undefined;
}
+8 -6
View File
@@ -1390,12 +1390,14 @@ export class AuthStorage {
const credentialId = this.#getStoredCredentials(provider)[credentialIndex]?.id;
if (credentialId === undefined) return blockedUntil;
const persistedGlobalBlockedUntil = this.#readPersistedCredentialBlock(credentialId, providerKey, "");
if (
persistedGlobalBlockedUntil !== undefined &&
(blockedUntil === undefined || persistedGlobalBlockedUntil > blockedUntil)
) {
blockedUntil = persistedGlobalBlockedUntil;
if (!blockScope || provider !== "openai-codex") {
const persistedGlobalBlockedUntil = this.#readPersistedCredentialBlock(credentialId, providerKey, "");
if (
persistedGlobalBlockedUntil !== undefined &&
(blockedUntil === undefined || persistedGlobalBlockedUntil > blockedUntil)
) {
blockedUntil = persistedGlobalBlockedUntil;
}
}
if (blockScope) {
const persistedScopedBlockedUntil = this.#readPersistedCredentialBlock(credentialId, providerKey, blockScope);
+3
View File
@@ -505,6 +505,9 @@ export const openaiCodexUsageProvider: UsageProvider = {
const FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
export const codexRankingStrategy: CredentialRankingStrategy = {
blockScope() {
return "shared";
},
findWindowLimits(report) {
const findLimit = (key: "primary" | "secondary"): UsageLimit | undefined => {
const direct = report.limits.find(l => l.id === `openai-codex:${key}`);
+22
View File
@@ -99,6 +99,28 @@ describe("withAuth", () => {
]);
});
it("switches accounts before refreshing the same account on usage limits", async () => {
const keys: string[] = [];
const contexts: ApiKeyResolveContext[] = [];
const result = await withAuth(
ctx => {
contexts.push(ctx);
return ctx.error === undefined ? "k0" : ctx.lastChance ? "k2" : "k1";
},
async key => {
keys.push(key);
if (key === "k2") return "success";
throw usageLimitError();
},
);
expect(result).toBe("success");
expect(keys).toEqual(["k0", "k2"]);
expect(contexts.map(ctx => ({ lastChance: ctx.lastChance, hasError: ctx.error !== undefined }))).toEqual([
{ lastChance: false, hasError: false },
{ lastChance: true, hasError: true },
]);
});
it("stops retrying when the resolver returns undefined", async () => {
const keys: string[] = [];
const original = authError();
@@ -643,6 +643,52 @@ describe("AuthStorage codex oauth ranking", () => {
expect(await authStorage.getApiKey("openai-codex", sessionId)).toBe("api-acct-plus");
});
test("ignores legacy global Codex blocks when a scoped quota window has fresh siblings", async () => {
if (!authStorage || !store) throw new Error("test setup failed");
await authStorage.set("openai-codex", [
{ type: "oauth", ...createCredential("acct-k12", "k12@example.com") },
{ type: "oauth", ...createCredential("acct-plus", "plus@example.com") },
]);
usageByAccount.set(
"acct-k12",
createCodexUsageReport({
accountId: "acct-k12",
primary: { usedFraction: 1, resetInMs: FIVE_HOUR_MS },
secondary: { usedFraction: 1, resetInMs: WEEK_MS },
}),
);
usageByAccount.set(
"acct-plus",
createCodexUsageReport({
accountId: "acct-plus",
primary: { usedFraction: 0.2, resetInMs: FIVE_HOUR_MS },
secondary: { usedFraction: 0.74, resetInMs: WEEK_MS },
}),
);
const plus = store
.listAuthCredentials("openai-codex")
.find(row => row.credential.type === "oauth" && row.credential.accountId === "acct-plus");
if (!plus || !store.upsertCredentialBlock) throw new Error("missing plus credential row");
store.upsertCredentialBlock({
credentialId: plus.id,
providerKey: "openai-codex:oauth",
blockScope: "",
blockedUntilMs: Date.now() + WEEK_MS,
});
const k12 = store
.listAuthCredentials("openai-codex")
.find(row => row.credential.type === "oauth" && row.credential.accountId === "acct-k12");
if (!k12 || !store.upsertCredentialBlock) throw new Error("missing k12 credential row");
store.upsertCredentialBlock({
credentialId: k12.id,
providerKey: "openai-codex:oauth",
blockScope: "shared",
blockedUntilMs: Date.now() + HOUR_MS,
});
expect(await authStorage.getApiKey("openai-codex", "session-with-legacy-global-block")).toBe("api-acct-plus");
});
});
// ─────────────────────────────────────────────────────────────────────────────