소스 검색

tweak: retry logic to catch certain provider problems

Aiden Cline 3 주 전
부모
커밋
8b5dde5536
2개의 변경된 파일56개의 추가작업 그리고 20개의 파일을 삭제
  1. 29 20
      packages/opencode/src/session/retry.ts
  2. 27 0
      packages/opencode/test/session/retry.test.ts

+ 29 - 20
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"
+    }
   }
 }

+ 27 - 0
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<string, string>): MessageV2.APIError {
   }).toObject() as MessageV2.APIError
 }
 
+function wrap(message: unknown): ReturnType<NamedError["toObject"]> {
+  return { data: { message } } as ReturnType<NamedError["toObject"]>
+}
+
 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",