From 8b5dde5536b5725e68eb43933d23dae22c298b35 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 26 Jan 2026 16:12:41 -0500 Subject: [PATCH] tweak: retry logic to catch certain provider problems --- packages/opencode/src/session/retry.ts | 49 ++++++++++++-------- packages/opencode/test/session/retry.test.ts | 27 +++++++++++ 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index dd0fe2380..8d3f72844 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -1,5 +1,6 @@ import type { NamedError } from "@opencode-ai/util/error" import { MessageV2 } from "./message-v2" +import { iife } from "@/util/iife" export namespace SessionRetry { export const RETRY_INITIAL_DELAY = 2000 @@ -63,28 +64,36 @@ export namespace SessionRetry { return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message } - if (typeof error.data?.message === "string") { + const json = iife(() => { try { - const json = JSON.parse(error.data.message) - if (json.type === "error" && json.error?.type === "too_many_requests") { - return "Too Many Requests" + if (typeof error.data?.message === "string") { + const parsed = JSON.parse(error.data.message) + return parsed } - if (json.code.includes("exhausted") || json.code.includes("unavailable")) { - return "Provider is overloaded" - } - if (json.type === "error" && json.error?.code?.includes("rate_limit")) { - return "Rate Limited" - } - if ( - json.error?.message?.includes("no_kv_space") || - (json.type === "error" && json.error?.type === "server_error") || - !!json.error - ) { - return "Provider Server Error" - } - } catch {} - } - return undefined + return JSON.parse(error.data.message) + } catch { + return undefined + } + }) + 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 "Too Many Requests" + } + if (code.includes("exhausted") || code.includes("unavailable")) { + return "Provider is overloaded" + } + if (json.type === "error" && json.error?.code?.includes("rate_limit")) { + return "Rate Limited" + } + if ( + json.error?.message?.includes("no_kv_space") || + (json.type === "error" && json.error?.type === "server_error") || + !!json.error + ) { + return "Provider Server Error" + } } } diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 22ffb0cb1..6866b03a1 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test" +import type { NamedError } from "@opencode-ai/util/error" import { APICallError } from "ai" import { SessionRetry } from "../../src/session/retry" import { MessageV2 } from "../../src/session/message-v2" @@ -11,6 +12,10 @@ function apiError(headers?: Record): MessageV2.APIError { }).toObject() as MessageV2.APIError } +function wrap(message: unknown): ReturnType { + return { data: { message } } as ReturnType +} + describe("session.retry.delay", () => { test("caps delay at 30 seconds when headers missing", () => { const error = apiError() @@ -81,6 +86,28 @@ describe("session.retry.delay", () => { }) }) +describe("session.retry.retryable", () => { + test("maps too_many_requests json messages", () => { + const error = wrap(JSON.stringify({ type: "error", error: { type: "too_many_requests" } })) + expect(SessionRetry.retryable(error)).toBe("Too Many Requests") + }) + + test("maps overloaded provider codes", () => { + const error = wrap(JSON.stringify({ code: "resource_exhausted" })) + expect(SessionRetry.retryable(error)).toBe("Provider is overloaded") + }) + + test("handles json messages without code", () => { + const error = wrap(JSON.stringify({ error: { message: "no_kv_space" } })) + expect(SessionRetry.retryable(error)).toBe("Provider Server Error") + }) + + test("returns undefined for non-json message", () => { + const error = wrap("not-json") + expect(SessionRetry.retryable(error)).toBeUndefined() + }) +}) + describe("session.message-v2.fromError", () => { test.concurrent( "converts ECONNRESET socket errors to retryable APIError",