Skip to content

Commit 53beeac

Browse files
committed
fix: harden rate-limit and wrapper recovery
1 parent 441c6a2 commit 53beeac

14 files changed

Lines changed: 504 additions & 82 deletions

index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1858,16 +1858,18 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
18581858
break;
18591859
}
18601860

1861-
if (rateLimit) {
1861+
if (response.status === 429) {
18621862
runtimeMetrics.rateLimitedResponses++;
1863+
const retryAfterMs =
1864+
rateLimit?.retryAfterMs ?? 60_000;
18631865
const { attempt, delayMs } = getRateLimitBackoff(
18641866
account.index,
18651867
quotaKey,
1866-
rateLimit.retryAfterMs,
1868+
retryAfterMs,
18671869
);
18681870
const cooldownMs = Math.max(
18691871
delayMs,
1870-
rateLimit.retryAfterMs,
1872+
retryAfterMs,
18711873
);
18721874
preemptiveQuotaScheduler.markRateLimited(
18731875
quotaScheduleKey,
@@ -1900,7 +1902,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
19001902
account,
19011903
cooldownMs,
19021904
modelFamily,
1903-
parseRateLimitReason(rateLimit.code),
1905+
parseRateLimitReason(rateLimit?.code),
19041906
model,
19051907
);
19061908
accountManager.recordRateLimit(
@@ -2183,6 +2185,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
21832185
modelFamily,
21842186
model,
21852187
);
2188+
accountManager.saveToDiskDebounced();
21862189
} else {
21872190
accountManager.recordFailure(
21882191
fallbackAccount,

lib/config.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ async function readConfigRecordForSave(
481481
return { status: "invalid", errorMessage };
482482
}
483483
return { status: "ok", record: parsed };
484-
} catch (error) {
484+
} catch (error) {
485485
const code = (error as NodeJS.ErrnoException | undefined)?.code;
486486
if (code === "ENOENT") {
487487
return { status: "missing" };
@@ -498,7 +498,10 @@ async function readConfigRecordForSave(
498498
error instanceof Error ? error.message : String(error)
499499
}`;
500500
logConfigWarnOnce(errorMessage);
501-
if (typeof code === "string" && RETRYABLE_CONFIG_READ_CODES.has(code)) {
501+
if (error instanceof SyntaxError) {
502+
return { status: "invalid", errorMessage };
503+
}
504+
if (typeof code === "string") {
502505
return { status: "unreadable", errorMessage };
503506
}
504507
return { status: "invalid", errorMessage };

lib/request/fetch-helpers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,13 @@ function parseRetryAfterMs(
12401240
if (normalized !== null) {
12411241
return normalized;
12421242
}
1243+
const parsedDate = Date.parse(retryAfterHeader);
1244+
if (Number.isFinite(parsedDate)) {
1245+
const normalizedDate = normalizeRetryAfterMs(parsedDate - Date.now());
1246+
if (normalizedDate !== null) {
1247+
return normalizedDate;
1248+
}
1249+
}
12431250
}
12441251

12451252
const resetAfterSecondsHeaders = [

lib/storage/flagged-storage-io.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,25 @@ import type { FlaggedAccountStorageV1 } from "../storage.js";
44

55
const RETRYABLE_UNLINK_CODES = new Set(["EBUSY", "EAGAIN", "EPERM"]);
66

7+
function isValidFlaggedStorageCandidate(
8+
data: unknown,
9+
storage: FlaggedAccountStorageV1,
10+
): boolean {
11+
if (
12+
!data ||
13+
typeof data !== "object" ||
14+
!Object.hasOwn(data, "version") ||
15+
!Object.hasOwn(data, "accounts")
16+
) {
17+
return false;
18+
}
19+
const candidate = data as { version?: unknown; accounts?: unknown };
20+
if (candidate.version !== 1 || !Array.isArray(candidate.accounts)) {
21+
return false;
22+
}
23+
return candidate.accounts.length === 0 || storage.accounts.length > 0;
24+
}
25+
726
/**
827
* Return the ordered backup paths consulted for flagged-account recovery.
928
*/
@@ -55,6 +74,13 @@ export async function loadFlaggedAccountsState(params: {
5574
const backupContent = await fs.readFile(backupPath, "utf-8");
5675
const backupData = JSON.parse(backupContent) as unknown;
5776
const recovered = params.normalizeFlaggedStorage(backupData);
77+
if (!isValidFlaggedStorageCandidate(backupData, recovered)) {
78+
params.logError("Skipping invalid flagged account backup payload", {
79+
from: backupPath,
80+
to: params.path,
81+
});
82+
continue;
83+
}
5884
if (existsSync(params.resetMarkerPath)) {
5985
return empty;
6086
}
@@ -79,6 +105,9 @@ export async function loadFlaggedAccountsState(params: {
79105
const content = await fs.readFile(params.path, "utf-8");
80106
const data = JSON.parse(content) as unknown;
81107
const loaded = params.normalizeFlaggedStorage(data);
108+
if (!isValidFlaggedStorageCandidate(data, loaded)) {
109+
throw new Error("Invalid flagged account storage payload");
110+
}
82111
if (existsSync(params.resetMarkerPath)) {
83112
return empty;
84113
}

lib/unified-settings.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,14 @@ function parseSettingsRecord(content: string): JsonRecord {
6868
* cannot be parsed into the expected object shape.
6969
*/
7070
function readSettingsRecordSyncFromPath(filePath: string): JsonRecord | null {
71-
if (!existsSync(filePath)) {
72-
return null;
71+
try {
72+
return parseSettingsRecord(readFileSync(filePath, "utf8"));
73+
} catch (error) {
74+
if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {
75+
return null;
76+
}
77+
throw error;
7378
}
74-
return parseSettingsRecord(readFileSync(filePath, "utf8"));
7579
}
7680

7781
/**
@@ -80,10 +84,14 @@ function readSettingsRecordSyncFromPath(filePath: string): JsonRecord | null {
8084
async function readSettingsRecordAsyncFromPath(
8185
filePath: string,
8286
): Promise<JsonRecord | null> {
83-
if (!existsSync(filePath)) {
84-
return null;
87+
try {
88+
return parseSettingsRecord(await fs.readFile(filePath, "utf8"));
89+
} catch (error) {
90+
if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {
91+
return null;
92+
}
93+
throw error;
8594
}
86-
return parseSettingsRecord(await fs.readFile(filePath, "utf8"));
8795
}
8896

8997
/**

scripts/codex.js

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
existsSync,
88
mkdirSync,
99
mkdtempSync,
10+
renameSync,
1011
readFileSync,
1112
rmSync,
1213
statSync,
@@ -215,6 +216,47 @@ const REASONING_FALLBACKS = {
215216
};
216217

217218
const KNOWN_REASONING_EFFORTS = new Set(Object.keys(REASONING_FALLBACKS));
219+
const REQUESTED_MODEL_ALIASES = new Map();
220+
221+
function addRequestedModelAlias(alias, normalizedModel) {
222+
REQUESTED_MODEL_ALIASES.set(alias, normalizedModel);
223+
}
224+
225+
function addRequestedModelReasoningAliases(alias, normalizedModel) {
226+
addRequestedModelAlias(alias, normalizedModel);
227+
for (const effort of KNOWN_REASONING_EFFORTS) {
228+
addRequestedModelAlias(`${alias}-${effort}`, normalizedModel);
229+
}
230+
}
231+
232+
function seedRequestedModelAliases() {
233+
addRequestedModelReasoningAliases("gpt-5.4", "gpt-5.4");
234+
addRequestedModelReasoningAliases("gpt-5.4-pro", "gpt-5.4-pro");
235+
addRequestedModelReasoningAliases("gpt-5.2-pro", "gpt-5.2-pro");
236+
addRequestedModelReasoningAliases("gpt-5-pro", "gpt-5-pro");
237+
addRequestedModelReasoningAliases("gpt-5.2", "gpt-5.2");
238+
addRequestedModelReasoningAliases("gpt-5.1", "gpt-5.1");
239+
addRequestedModelReasoningAliases("gpt-5", "gpt-5");
240+
addRequestedModelReasoningAliases("gpt-5-mini", "gpt-5-mini");
241+
addRequestedModelReasoningAliases("gpt-5-nano", "gpt-5-nano");
242+
addRequestedModelReasoningAliases("gpt-5.1-chat-latest", "gpt-5.1");
243+
addRequestedModelReasoningAliases("gpt-5-chat-latest", "gpt-5");
244+
addRequestedModelReasoningAliases("gpt-5.4-mini", "gpt-5-mini");
245+
addRequestedModelReasoningAliases("gpt-5.4-nano", "gpt-5-nano");
246+
addRequestedModelReasoningAliases("gpt-5-codex", "gpt-5-codex");
247+
addRequestedModelReasoningAliases("gpt-5.3-codex-spark", "gpt-5-codex");
248+
addRequestedModelReasoningAliases("gpt-5.3-codex", "gpt-5-codex");
249+
addRequestedModelReasoningAliases("gpt-5.2-codex", "gpt-5-codex");
250+
addRequestedModelReasoningAliases("gpt-5.1-codex", "gpt-5-codex");
251+
addRequestedModelAlias("gpt_5_codex", "gpt-5-codex");
252+
addRequestedModelReasoningAliases("codex-max", "gpt-5.1-codex-max");
253+
addRequestedModelReasoningAliases("gpt-5.1-codex-max", "gpt-5.1-codex-max");
254+
addRequestedModelAlias("codex-mini-latest", "gpt-5.1-codex-mini");
255+
addRequestedModelReasoningAliases("gpt-5-codex-mini", "gpt-5.1-codex-mini");
256+
addRequestedModelReasoningAliases("gpt-5.1-codex-mini", "gpt-5.1-codex-mini");
257+
}
258+
259+
seedRequestedModelAliases();
218260

219261
function stripProviderPrefix(model) {
220262
return typeof model === "string" && model.includes("/")
@@ -226,6 +268,10 @@ function normalizeRequestedModel(model) {
226268
const stripped = stripProviderPrefix(model ?? "");
227269
const normalized = stripped.trim().toLowerCase();
228270
if (normalized.length === 0) return "";
271+
const exactMatch = REQUESTED_MODEL_ALIASES.get(normalized);
272+
if (exactMatch) {
273+
return exactMatch;
274+
}
229275

230276
if (
231277
normalized.includes("gpt-5.1-codex-max") ||
@@ -442,6 +488,47 @@ function ensureTrailingNewline(value) {
442488
return value.endsWith("\n") ? value : `${value}\n`;
443489
}
444490

491+
function captureShadowHomeState(filePath) {
492+
try {
493+
if (!existsSync(filePath)) {
494+
return { exists: false, content: null };
495+
}
496+
return {
497+
exists: true,
498+
content: readFileSync(filePath, "utf8"),
499+
};
500+
} catch {
501+
return { exists: true, content: null, unreadable: true };
502+
}
503+
}
504+
505+
function shadowHomeStateMatches(left, right) {
506+
return (
507+
left.exists === right.exists &&
508+
left.content === right.content &&
509+
Boolean(left.unreadable) === Boolean(right.unreadable)
510+
);
511+
}
512+
513+
function syncShadowHomeStateFile(sourcePath, destinationPath) {
514+
const tempPath = join(
515+
dirname(destinationPath),
516+
`.${basename(destinationPath)}.codex-multi-auth-sync-${process.pid}.tmp`,
517+
);
518+
try {
519+
mkdirSync(dirname(destinationPath), { recursive: true });
520+
copyFileSync(sourcePath, tempPath);
521+
renameSync(tempPath, destinationPath);
522+
} catch (error) {
523+
try {
524+
rmSync(tempPath, { force: true });
525+
} catch {
526+
// Best-effort cleanup only.
527+
}
528+
throw error;
529+
}
530+
}
531+
445532
function rewriteConfigTomlReasoningEffort(rawConfig, requestedModel) {
446533
const lineEnding = rawConfig.includes("\r\n") ? "\r\n" : "\n";
447534
let changed = false;
@@ -503,6 +590,12 @@ function createCompatibilityCodexHome(
503590
if (!existsSync(configPath)) {
504591
return { args: processedArgs, env: baseEnv, cleanup: undefined };
505592
}
593+
const originalShadowHomeState = new Map(
594+
SHADOW_HOME_STATE_FILES.map((name) => [
595+
name,
596+
captureShadowHomeState(join(originalCodexHome, name)),
597+
]),
598+
);
506599

507600
const rawConfig = readFileSync(configPath, "utf8");
508601
const compatConfig = rewriteConfigTomlReasoningEffort(
@@ -531,20 +624,23 @@ function createCompatibilityCodexHome(
531624
const syncShadowHomeStateBack = () => {
532625
for (const name of SHADOW_HOME_STATE_FILES) {
533626
const shadowPath = join(shadowCodexHome, name);
534-
if (!existsSync(shadowPath)) {
627+
const shadowState = captureShadowHomeState(shadowPath);
628+
if (!shadowState.exists || shadowState.unreadable) {
535629
continue;
536630
}
537631

538632
try {
539-
const shadowStats = statSync(shadowPath);
540633
const originalPath = join(originalCodexHome, name);
541-
if (existsSync(originalPath)) {
542-
const originalStats = statSync(originalPath);
543-
if (originalStats.isFile() && originalStats.mtimeMs > shadowStats.mtimeMs) {
544-
continue;
545-
}
634+
const originalSnapshot =
635+
originalShadowHomeState.get(name) ?? { exists: false, content: null };
636+
const currentOriginalState = captureShadowHomeState(originalPath);
637+
if (!shadowHomeStateMatches(currentOriginalState, originalSnapshot)) {
638+
continue;
639+
}
640+
if (shadowHomeStateMatches(shadowState, originalSnapshot)) {
641+
continue;
546642
}
547-
copyFileSync(shadowPath, originalPath);
643+
syncShadowHomeStateFile(shadowPath, originalPath);
548644
tightenShadowHomePermissions(originalPath);
549645
} catch {
550646
// Best-effort only; runtime auth refreshes should not fail cleanup.

0 commit comments

Comments
 (0)