diff --git a/packages/ai/src/providers/openai-chatgpt-responses.retry.test.ts b/packages/ai/src/providers/openai-chatgpt-responses.retry.test.ts index 48acf2dde03d..8c3852261bee 100644 --- a/packages/ai/src/providers/openai-chatgpt-responses.retry.test.ts +++ b/packages/ai/src/providers/openai-chatgpt-responses.retry.test.ts @@ -98,6 +98,38 @@ describe("streamOpenAICodexResponses retry classification", () => { expect(fetchMock).toHaveBeenCalledTimes(2); }); + it.each([ + { label: "unparseable", retryAfterMs: "not-a-number" }, + { label: "empty", retryAfterMs: "" }, + ])("honors retry-after when retry-after-ms is $label", async ({ retryAfterMs }) => { + const fetchMock = vi + .fn() + .mockResolvedValueOnce( + new Response("rate limited", { + status: 429, + headers: { "retry-after-ms": retryAfterMs, "retry-after": "7" }, + }), + ) + .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; + }); + + await streamOpenAICodexResponses(model, context, { + apiKey: jwt, + transport: "sse", + }).result(); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 7_000); + }); + it("does not retry a bodyless 304 response", async () => { const fetchMock = vi .fn() diff --git a/packages/ai/src/providers/openai-chatgpt-responses.ts b/packages/ai/src/providers/openai-chatgpt-responses.ts index f2ea78210a9a..81e6002d3cca 100644 --- a/packages/ai/src/providers/openai-chatgpt-responses.ts +++ b/packages/ai/src/providers/openai-chatgpt-responses.ts @@ -147,12 +147,12 @@ function isRetryableError(status: number, errorText: string): boolean { function resolveHttpRetryDelayMs(response: Response, attempt: number): number { const fallbackMs = BASE_DELAY_MS * 2 ** attempt; const retryAfterMs = response.headers.get("retry-after-ms"); - if (retryAfterMs !== null) { + if (retryAfterMs) { const trimmed = retryAfterMs.trim(); const millis = Number(trimmed); - return /^\d+(?:\.\d+)?$/.test(trimmed) && Number.isFinite(millis) - ? (clampTimerTimeoutMs(millis, 0) ?? fallbackMs) - : fallbackMs; + if (/^\d+(?:\.\d+)?$/.test(trimmed) && Number.isFinite(millis)) { + return clampTimerTimeoutMs(millis, 0) ?? fallbackMs; + } } const retryAfter = response.headers.get("retry-after");