From efccad2684cf83265a6c82e04465db0fd8b6efcc Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 5 Aug 2026 11:17:35 -0500 Subject: [PATCH] fix(opencode): expand transient retry matching --- packages/opencode/src/session/retry.ts | 41 +++++++++----------- packages/opencode/test/session/retry.test.ts | 16 ++++++++ 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 4139665bd2b..3e3dbeb65b2 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -28,6 +28,12 @@ export const RETRY_BACKOFF_FACTOR = 2 export const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds export const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout +const RETRYABLE_MESSAGE = [ + /server[_\s-]?error|internal[_\s-]?error|service[_\s-]?unavailable|overloaded|provider returned error|too many requests|rate increased too quickly|rate[_\s-]?limit/i, + /fetch failed|network error|upstream connect|connection (?:error|refused|lost)|socket connection was closed|socket hang up|reset before headers|getaddrinfo|ENOTFOUND|EAI_AGAIN|timeout|timed? out|terminated/i, + /resource[_\s-]?exhausted|please retry your request|you can retry your request|try your request again/i, +] + function cap(ms: number) { return Math.min(ms, RETRY_MAX_DELAY) } @@ -122,32 +128,21 @@ export function retryable(error: Err, provider: string) { return { message: error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message } } - // Check for rate limit patterns in plain text error messages const msg = isRecord(error.data) ? error.data.message : undefined - if (typeof msg === "string") { - const lower = msg.toLowerCase() - if ( - lower.includes("rate increased too quickly") || - lower.includes("rate limit") || - lower.includes("too many requests") - ) { - return { message: msg } + const json = parseJSON(msg) + if (json && typeof json === "object") { + const code = typeof json.code === "string" ? json.code : "" + if (json.type === "error" && json.error?.type === "too_many_requests") { + return { message: "Too Many Requests" } + } + if (code.includes("exhausted") || code.includes("unavailable")) { + return { message: "Provider is overloaded" } + } + if (json.type === "error" && typeof json.error?.code === "string" && json.error.code.includes("rate_limit")) { + return { message: "Rate Limited" } } } - - const json = parseJSON(msg) - if (!json || typeof json !== "object") return undefined - const code = typeof json.code === "string" ? json.code : "" - - if (json.type === "error" && json.error?.type === "too_many_requests") { - return { message: "Too Many Requests" } - } - if (code.includes("exhausted") || code.includes("unavailable")) { - return { message: "Provider is overloaded" } - } - if (json.type === "error" && typeof json.error?.code === "string" && json.error.code.includes("rate_limit")) { - return { message: "Rate Limited" } - } + if (typeof msg === "string" && RETRYABLE_MESSAGE.some((pattern) => pattern.test(msg))) return { message: msg } return undefined } diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 30ac879a6a9..fcfa3ee7844 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -163,6 +163,22 @@ describe("session.retry.retryable", () => { expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: msg }) }) + test.each([ + "server_error", + "Internal server error", + "Service Unavailable", + "Provider is overloaded", + "Provider returned error", + "fetch failed", + "connection timed out", + "socket hang up", + "getaddrinfo ENOTFOUND api.example.com", + "ResourceExhausted", + "You can retry your request", + ])("retries transient plain text errors: %s", (msg) => { + expect(SessionRetry.retryable(wrap(msg), retryProvider)).toEqual({ message: msg }) + }) + test("retries transport timeout errors", () => { const request = MessageV2.fromError(new ProviderError.HeaderTimeoutError(10000), { providerID }) expect(SessionV1.APIError.isInstance(request)).toBe(true)