Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/inference-gateway-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,43 @@ describe("normalizeInferenceErrorForRetry", () => {
expect(normalized.retryAfterMs).toBe(7_000);
});

test.each([{ resets_in_seconds: 0 }, { resets_in_seconds: -5 }])(
"Codex 429 with a zero/negative body reset window keeps the header retry-after (CL-10758)",
(window) => {
const normalized = normalizeInferenceErrorForRetry({
...BARE_429,
providerId: "codex/acme-labs",
raw: {
error: {
code: "rate_limit_exceeded",
message: "Too Many Requests",
...window,
},
},
});
expect(normalized.category).toBe("retryable");
expect(normalized.retryAfterMs).toBe(45_000);
},
);

test("Codex 429 with a zero retry_after body key keeps the header retry-after (CL-10758)", () => {
// retry_after is not a Codex body signal (only resets_in_seconds et al.);
// a zero there must never clobber the header floor either.
const normalized = normalizeInferenceErrorForRetry({
...BARE_429,
providerId: "codex/acme-labs",
raw: {
error: {
code: "rate_limit_exceeded",
message: "Too Many Requests",
retry_after: 0,
},
},
});
expect(normalized.category).toBe("retryable");
expect(normalized.retryAfterMs).toBe(45_000);
});

test("non-Codex 429s keep the header retry-after; a body reset is not read (CL-10664)", () => {
const normalized = normalizeInferenceErrorForRetry({
...BARE_429,
Expand Down
8 changes: 5 additions & 3 deletions src/inference-gateway-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,14 +434,16 @@ export function isCodexShortRateLimitInferenceError(
/**
* Codex sends its reset window in the 429 body, not a header; when a bare
* rate-limit body carries one, retry on that window instead of the policy
* default. Exact usage-limit codes never reach here (the skeleton vetoes).
* default. A zero/negative window is not a real ETA — returning 0 would win
* over the header via `??` in the skeleton and retry immediately — so it
* falls through to the header/default floor. Exact usage-limit codes never
* reach here (the skeleton vetoes).
*/
function codexRetryFromBody(error: InferenceErrorLike): number | undefined {
const node = readErrorNode(coerceBody(error.raw));
if (node === undefined) return undefined;
const seconds = resetSecondsFromNode(node);
if (seconds === undefined) return undefined;
if (seconds <= 0) return 0;
if (seconds === undefined || seconds <= 0) return undefined;
return seconds * 1000;
}

Expand Down
Loading