diff --git a/packages/opencode/src/provider/error.ts b/packages/opencode/src/provider/error.ts index 3877dcb7f304..796294797493 100644 --- a/packages/opencode/src/provider/error.ts +++ b/packages/opencode/src/provider/error.ts @@ -152,6 +152,7 @@ export function parseStreamError(input: unknown): ParsedStreamError | undefined responseBody, } case "server_error": + case "server_is_overloaded": return { type: "api_error", message: typeof body?.error?.message === "string" ? body?.error?.message : "Server error.", diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index e81e1973751f..8d8652a61ae6 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -13,6 +13,11 @@ export const RETRY_INITIAL_DELAY = 2000 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 OVERLOAD_MARKERS = [ + "server_is_overloaded", + "service_unavailable_error", + "servers are currently overloaded", +] function cap(ms: number) { return Math.min(ms, RETRY_MAX_DELAY) @@ -60,7 +65,11 @@ export function retryable(error: Err) { // even when the provider SDK doesn't explicitly mark them as retryable. if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined if (error.data.responseBody?.includes("FreeUsageLimitError")) return GO_UPSELL_MESSAGE - return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message + const text = [error.data.message, error.data.responseBody].filter(Boolean).join(" ").toLowerCase() + if (error.data.message.includes("Overloaded") || OVERLOAD_MARKERS.some((marker) => text.includes(marker))) { + return "Provider is overloaded" + } + return error.data.message } // Check for rate limit patterns in plain text error messages @@ -68,6 +77,7 @@ export function retryable(error: Err) { if (typeof msg === "string") { const lower = msg.toLowerCase() if ( + OVERLOAD_MARKERS.some((marker) => lower.includes(marker)) || lower.includes("rate increased too quickly") || lower.includes("rate limit") || lower.includes("too many requests") @@ -94,6 +104,14 @@ export function retryable(error: Err) { if (json.type === "error" && json.error?.type === "too_many_requests") { return "Too Many Requests" } + const type = typeof json.type === "string" ? json.type : "" + const nestedType = typeof json.error?.type === "string" ? json.error.type : "" + const nestedCode = typeof json.error?.code === "string" ? json.error.code : "" + const nestedMessage = typeof json.error?.message === "string" ? json.error.message : "" + const overloadText = [code, type, nestedType, nestedCode, nestedMessage].join(" ").toLowerCase() + if (OVERLOAD_MARKERS.some((marker) => overloadText.includes(marker))) { + return "Provider is overloaded" + } if (code.includes("exhausted") || code.includes("unavailable")) { return "Provider is overloaded" } diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index a7853be0b8bf..5434ef55fefc 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -1180,6 +1180,29 @@ describe("session.message-v2.fromError", () => { }) }) + test("serializes OpenAI response server_is_overloaded stream chunks as retryable APIError", () => { + const body = { + type: "error", + sequence_number: 2, + error: { + type: "service_unavailable_error", + code: "server_is_overloaded", + message: "Our servers are currently overloaded. Please try again later.", + param: null, + }, + } + const result = MessageV2.fromError({ message: JSON.stringify(body) }, { providerID }) + + expect(result).toStrictEqual({ + name: "APIError", + data: { + message: body.error.message, + isRetryable: true, + responseBody: JSON.stringify(body), + }, + }) + }) + test("detects context overflow from APICallError provider messages", () => { const cases = [ "prompt is too long: 213462 tokens > 200000 maximum", diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 105c772d9735..21cc5e5a639b 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -126,6 +126,39 @@ describe("session.retry.retryable", () => { expect(SessionRetry.retryable(error)).toBe("Provider is overloaded") }) + test("maps OpenAI overloaded stream errors", () => { + const error = wrap( + JSON.stringify({ + type: "error", + error: { + type: "service_unavailable_error", + code: "server_is_overloaded", + message: "Our servers are currently overloaded. Please try again later.", + }, + }), + ) + expect(SessionRetry.retryable(error)).toBe("Provider is overloaded") + }) + + test("maps OpenAI overloaded API response bodies", () => { + const error = MessageV2.APIError.Schema.parse( + new MessageV2.APIError({ + message: "OpenAI request failed", + isRetryable: false, + statusCode: 503, + responseBody: JSON.stringify({ + error: { + type: "service_unavailable_error", + code: "server_is_overloaded", + message: "Our servers are currently overloaded. Please try again later.", + }, + }), + }).toObject(), + ) + + expect(SessionRetry.retryable(error)).toBe("Provider is overloaded") + }) + test("does not retry unknown json messages", () => { const error = wrap(JSON.stringify({ error: { message: "no_kv_space" } })) expect(SessionRetry.retryable(error)).toBeUndefined()