tweak: retry logic to catch certain provider problems
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import type { NamedError } from "@opencode-ai/util/error"
|
import type { NamedError } from "@opencode-ai/util/error"
|
||||||
import { MessageV2 } from "./message-v2"
|
import { MessageV2 } from "./message-v2"
|
||||||
|
import { iife } from "@/util/iife"
|
||||||
|
|
||||||
export namespace SessionRetry {
|
export namespace SessionRetry {
|
||||||
export const RETRY_INITIAL_DELAY = 2000
|
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
|
return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof error.data?.message === "string") {
|
const json = iife(() => {
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(error.data.message)
|
if (typeof error.data?.message === "string") {
|
||||||
if (json.type === "error" && json.error?.type === "too_many_requests") {
|
const parsed = JSON.parse(error.data.message)
|
||||||
return "Too Many Requests"
|
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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import type { NamedError } from "@opencode-ai/util/error"
|
||||||
import { APICallError } from "ai"
|
import { APICallError } from "ai"
|
||||||
import { SessionRetry } from "../../src/session/retry"
|
import { SessionRetry } from "../../src/session/retry"
|
||||||
import { MessageV2 } from "../../src/session/message-v2"
|
import { MessageV2 } from "../../src/session/message-v2"
|
||||||
@@ -11,6 +12,10 @@ function apiError(headers?: Record<string, string>): MessageV2.APIError {
|
|||||||
}).toObject() as MessageV2.APIError
|
}).toObject() as MessageV2.APIError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wrap(message: unknown): ReturnType<NamedError["toObject"]> {
|
||||||
|
return { data: { message } } as ReturnType<NamedError["toObject"]>
|
||||||
|
}
|
||||||
|
|
||||||
describe("session.retry.delay", () => {
|
describe("session.retry.delay", () => {
|
||||||
test("caps delay at 30 seconds when headers missing", () => {
|
test("caps delay at 30 seconds when headers missing", () => {
|
||||||
const error = apiError()
|
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", () => {
|
describe("session.message-v2.fromError", () => {
|
||||||
test.concurrent(
|
test.concurrent(
|
||||||
"converts ECONNRESET socket errors to retryable APIError",
|
"converts ECONNRESET socket errors to retryable APIError",
|
||||||
|
|||||||
Reference in New Issue
Block a user