mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-23 21:16:06 +00:00
feat: add retry logic
This commit is contained in:
parent
f778c685f5
commit
4217091d02
2 changed files with 76 additions and 36 deletions
|
|
@ -1,7 +1,6 @@
|
|||
import type { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { Cause, Clock, Duration, Effect, Schedule } from "effect"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { iife } from "@/util/iife"
|
||||
|
||||
export type Err = ReturnType<NamedError["toObject"]>
|
||||
|
||||
|
|
@ -63,43 +62,19 @@ export function retryable(error: Err) {
|
|||
return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
|
||||
}
|
||||
|
||||
// Check for rate limit patterns in plain text error messages
|
||||
const msg = error.data?.message
|
||||
if (typeof msg === "string") {
|
||||
const lower = msg.toLowerCase()
|
||||
if (
|
||||
lower.includes("rate increased too quickly") ||
|
||||
lower.includes("rate limit") ||
|
||||
lower.includes("too many requests")
|
||||
/rate.?limit|too.?many.?requests|overloaded|exhausted|unavailable|service.?unavailable|429|5\d\d|internal.?error|network.?error|connection.?(?:error|refused|lost|reset)|fetch.?failed|upstream|upstream.?connect|reset before headers|socket.?hang.?up|other.?side.?closed|ended without|timed? out|timeout|terminated|retry delay|provider.?returned.?error|server.?error/i.test(
|
||||
lower,
|
||||
)
|
||||
) {
|
||||
if (lower.includes("overloaded") || lower.includes("exhausted") || lower.includes("unavailable")) return "Provider is overloaded"
|
||||
return msg
|
||||
}
|
||||
}
|
||||
|
||||
const json = iife(() => {
|
||||
try {
|
||||
if (typeof error.data?.message === "string") {
|
||||
const parsed = JSON.parse(error.data.message)
|
||||
return parsed
|
||||
}
|
||||
|
||||
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" && typeof json.error?.code === "string" && json.error.code.includes("rate_limit")) {
|
||||
return "Rate Limited"
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -124,22 +124,22 @@ describe("session.retry.delay", () => {
|
|||
})
|
||||
|
||||
describe("session.retry.retryable", () => {
|
||||
test("maps too_many_requests json messages", () => {
|
||||
test("retries too_many_requests in raw json strings", () => {
|
||||
const error = wrap(JSON.stringify({ type: "error", error: { type: "too_many_requests" } }))
|
||||
expect(SessionRetry.retryable(error)).toBe("Too Many Requests")
|
||||
expect(SessionRetry.retryable(error)).toBe(JSON.stringify({ type: "error", error: { type: "too_many_requests" } }))
|
||||
})
|
||||
|
||||
test("maps overloaded provider codes", () => {
|
||||
test("retries exhausted codes in raw json strings", () => {
|
||||
const error = wrap(JSON.stringify({ code: "resource_exhausted" }))
|
||||
expect(SessionRetry.retryable(error)).toBe("Provider is overloaded")
|
||||
})
|
||||
|
||||
test("does not retry unknown json messages", () => {
|
||||
test("does not retry unknown raw json strings", () => {
|
||||
const error = wrap(JSON.stringify({ error: { message: "no_kv_space" } }))
|
||||
expect(SessionRetry.retryable(error)).toBeUndefined()
|
||||
})
|
||||
|
||||
test("does not throw on numeric error codes", () => {
|
||||
test("does not throw on numeric codes in raw json strings", () => {
|
||||
const error = wrap(JSON.stringify({ type: "error", error: { code: 123 } }))
|
||||
const result = SessionRetry.retryable(error)
|
||||
expect(result).toBeUndefined()
|
||||
|
|
@ -169,6 +169,66 @@ describe("session.retry.retryable", () => {
|
|||
expect(SessionRetry.retryable(error)).toBe(msg)
|
||||
})
|
||||
|
||||
test("retries connection errors in plain text", () => {
|
||||
const msg = "Connection refused"
|
||||
const error = wrap(msg)
|
||||
expect(SessionRetry.retryable(error)).toBe(msg)
|
||||
})
|
||||
|
||||
test("retries timeout errors in plain text", () => {
|
||||
const msg = "Request timed out"
|
||||
const error = wrap(msg)
|
||||
expect(SessionRetry.retryable(error)).toBe(msg)
|
||||
})
|
||||
|
||||
test("retries 500 errors in plain text", () => {
|
||||
const msg = "HTTP 500 Internal Server Error"
|
||||
const error = wrap(msg)
|
||||
expect(SessionRetry.retryable(error)).toBe(msg)
|
||||
})
|
||||
|
||||
test("retries overloaded errors in plain text", () => {
|
||||
const msg = "Provider is overloaded"
|
||||
const error = wrap(msg)
|
||||
expect(SessionRetry.retryable(error)).toBe("Provider is overloaded")
|
||||
})
|
||||
|
||||
test("retries 429 errors in plain text", () => {
|
||||
const msg = "HTTP 429 Too Many Requests"
|
||||
const error = wrap(msg)
|
||||
expect(SessionRetry.retryable(error)).toBe(msg)
|
||||
})
|
||||
|
||||
test("retries provider returned errors", () => {
|
||||
const msg = "Provider returned error: something went wrong"
|
||||
const error = wrap(msg)
|
||||
expect(SessionRetry.retryable(error)).toBe(msg)
|
||||
})
|
||||
|
||||
test("retries other side closed errors", () => {
|
||||
const msg = "Other side closed connection"
|
||||
const error = wrap(msg)
|
||||
expect(SessionRetry.retryable(error)).toBe(msg)
|
||||
})
|
||||
|
||||
test("retries reset before headers errors", () => {
|
||||
const msg = "Connection reset before headers"
|
||||
const error = wrap(msg)
|
||||
expect(SessionRetry.retryable(error)).toBe(msg)
|
||||
})
|
||||
|
||||
test("retries ended without errors", () => {
|
||||
const msg = "Request ended without sending chunks"
|
||||
const error = wrap(msg)
|
||||
expect(SessionRetry.retryable(error)).toBe(msg)
|
||||
})
|
||||
|
||||
test("retries retry delay exceeded errors", () => {
|
||||
const msg = "Retry delay exceeded"
|
||||
const error = wrap(msg)
|
||||
expect(SessionRetry.retryable(error)).toBe(msg)
|
||||
})
|
||||
|
||||
test("does not retry context overflow errors", () => {
|
||||
const error = new MessageV2.ContextOverflowError({
|
||||
message: "Input exceeds context window of this model",
|
||||
|
|
@ -250,9 +310,14 @@ describe("session.retry.retryable", () => {
|
|||
expect(SessionRetry.retryable(error)).toBe("Provider is overloaded")
|
||||
})
|
||||
|
||||
test("maps rate_limit error code in nested json", () => {
|
||||
test("retries rate_limit codes in raw json strings", () => {
|
||||
const error = wrap(JSON.stringify({ type: "error", error: { code: "rate_limit_exceeded" } }))
|
||||
expect(SessionRetry.retryable(error)).toBe("Rate Limited")
|
||||
expect(SessionRetry.retryable(error)).toBe(JSON.stringify({ type: "error", error: { code: "rate_limit_exceeded" } }))
|
||||
})
|
||||
|
||||
test("retries server_error in raw json strings", () => {
|
||||
const error = wrap(JSON.stringify({ type: "error", error: { type: "server_error", code: "server_error" } }))
|
||||
expect(SessionRetry.retryable(error)).toBe(JSON.stringify({ type: "error", error: { type: "server_error", code: "server_error" } }))
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue