fix(llm): cap codex retry delays

This commit is contained in:
Peter Steinberger
2026-05-30 02:17:30 -04:00
parent 5aa2bd7921
commit be2c43ee3e
2 changed files with 40 additions and 4 deletions
@@ -304,4 +304,40 @@ describe("streamOpenAICodexResponses transport", () => {
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 1000);
},
);
it("caps oversized Retry-After delays before sleeping", async () => {
const fetchMock = vi
.fn<typeof fetch>()
.mockResolvedValueOnce(
new Response("rate limited", {
status: 429,
headers: { "retry-after": String(Number.MAX_SAFE_INTEGER) },
}),
)
.mockRejectedValueOnce(new Error("usage limit: stop after retry delay"));
vi.stubGlobal("fetch", fetchMock);
const setTimeoutSpy = vi
.spyOn(globalThis, "setTimeout")
.mockImplementation((callback: TimerHandler) => {
if (typeof callback === "function") {
callback();
}
return 0 as unknown as ReturnType<typeof setTimeout>;
});
const stream = streamOpenAICodexResponses(model, context, {
apiKey: createJwt({
"https://api.openai.com/auth": {
chatgpt_account_id: "acct-1",
},
}),
transport: "sse",
});
const result = await stream.result();
expect(result.stopReason).toBe("error");
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS);
});
});
+4 -4
View File
@@ -20,7 +20,7 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version
});
}
import { resolveTimerTimeoutMs } from "../../shared/number-coercion.js";
import { resolveTimerTimeoutMs, clampTimerTimeoutMs } from "../../shared/number-coercion.js";
import { getEnvApiKey } from "../env-api-keys.js";
import { clampThinkingLevel } from "../model-utils.js";
import { registerSessionResourceCleanup } from "../session-resources.js";
@@ -343,7 +343,7 @@ export const streamOpenAICodexResponses: StreamFunction<
const trimmedRetryAfterMs = retryAfterMs.trim();
const millis = Number(trimmedRetryAfterMs);
if (/^\d+(?:\.\d+)?$/.test(trimmedRetryAfterMs) && Number.isFinite(millis)) {
delayMs = Math.max(0, millis);
delayMs = clampTimerTimeoutMs(millis, 0) ?? delayMs;
}
} else {
const retryAfter = response.headers.get("retry-after");
@@ -351,11 +351,11 @@ export const streamOpenAICodexResponses: StreamFunction<
const trimmedRetryAfter = retryAfter.trim();
const seconds = Number(trimmedRetryAfter);
if (/^\d+$/.test(trimmedRetryAfter) && Number.isFinite(seconds)) {
delayMs = Math.max(0, seconds * 1000);
delayMs = clampTimerTimeoutMs(seconds * 1000, 0) ?? delayMs;
} else if (RETRY_AFTER_HTTP_DATE_RE.test(trimmedRetryAfter)) {
const date = Date.parse(trimmedRetryAfter);
if (!Number.isNaN(date)) {
delayMs = Math.max(0, date - Date.now());
delayMs = clampTimerTimeoutMs(date - Date.now(), 0) ?? delayMs;
}
}
}