(feat) expontential back-off when encountering rate limit errors (#153)

...and try to parse the suggested time from the error message while we
don't yet have this in a structured way

---------

Signed-off-by: Thibault Sottiaux <tibo@openai.com>
This commit is contained in:
Thibault Sottiaux
2025-04-16 17:37:12 -07:00
committed by GitHub
parent b8aea2340d
commit 47c683480f
2 changed files with 51 additions and 25 deletions

View File

@@ -1,12 +1,12 @@
{ {
"name": "@openai/codex", "name": "@openai/codex",
"version": "0.1.04161241", "version": "0.1.2504161510",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@openai/codex", "name": "@openai/codex",
"version": "0.1.04161241", "version": "0.1.2504161510",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@inkjs/ui": "^2.0.0", "@inkjs/ui": "^2.0.0",

View File

@@ -24,7 +24,7 @@ import OpenAI, { APIConnectionTimeoutError } from "openai";
// Wait time before retrying after rate limit errors (ms). // Wait time before retrying after rate limit errors (ms).
const RATE_LIMIT_RETRY_WAIT_MS = parseInt( const RATE_LIMIT_RETRY_WAIT_MS = parseInt(
process.env["OPENAI_RATE_LIMIT_RETRY_WAIT_MS"] || "15000", process.env["OPENAI_RATE_LIMIT_RETRY_WAIT_MS"] || "2500",
10, 10,
); );
@@ -569,11 +569,6 @@ export class AgentLoop {
); );
continue; continue;
} }
const isRateLimit =
status === 429 ||
errCtx.code === "rate_limit_exceeded" ||
errCtx.type === "rate_limit_exceeded" ||
/rate limit/i.test(errCtx.message ?? "");
const isTooManyTokensError = const isTooManyTokensError =
(errCtx.param === "max_tokens" || (errCtx.param === "max_tokens" ||
@@ -597,30 +592,61 @@ export class AgentLoop {
return; return;
} }
const isRateLimit =
status === 429 ||
errCtx.code === "rate_limit_exceeded" ||
errCtx.type === "rate_limit_exceeded" ||
/rate limit/i.test(errCtx.message ?? "");
if (isRateLimit) { if (isRateLimit) {
if (attempt < MAX_RETRIES) { if (attempt < MAX_RETRIES) {
// Exponential backoff: base wait * 2^(attempt-1), or use suggested retry time
// if provided.
let delayMs = RATE_LIMIT_RETRY_WAIT_MS * 2 ** (attempt - 1);
// Parse suggested retry time from error message, e.g., "Please try again in 1.3s"
const msg = errCtx?.message ?? "";
const m = /retry again in ([\d.]+)s/i.exec(msg);
if (m && m[1]) {
const suggested = parseFloat(m[1]) * 1000;
if (!Number.isNaN(suggested)) {
delayMs = suggested;
}
}
log( log(
`OpenAI rate limit exceeded (attempt ${attempt}/${MAX_RETRIES}), retrying in ${RATE_LIMIT_RETRY_WAIT_MS} ms...`, `OpenAI rate limit exceeded (attempt ${attempt}/${MAX_RETRIES}), retrying in ${Math.round(
delayMs,
)} ms...`,
); );
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => await new Promise((resolve) => setTimeout(resolve, delayMs));
setTimeout(resolve, RATE_LIMIT_RETRY_WAIT_MS),
);
continue; continue;
} else {
// We have exhausted all retry attempts. Surface a message so the user understands
// why the request failed and can decide how to proceed (e.g. wait and retry later
// or switch to a different model / account).
const errorDetails = [
`Status: ${status || "unknown"}`,
`Code: ${errCtx.code || "unknown"}`,
`Type: ${errCtx.type || "unknown"}`,
`Message: ${errCtx.message || "unknown"}`,
].join(", ");
this.onItem({
id: `error-${Date.now()}`,
type: "message",
role: "system",
content: [
{
type: "input_text",
text: `⚠️ Rate limit reached. Error details: ${errorDetails}. Please try again later.`,
},
],
});
this.onLoading(false);
return;
} }
this.onItem({
id: `error-${Date.now()}`,
type: "message",
role: "system",
content: [
{
type: "input_text",
text: "⚠️ Rate limit reached while contacting OpenAI. Please try again later.",
},
],
});
this.onLoading(false);
return;
} }
const isClientError = const isClientError =