Compare commits

...

3 Commits

Author SHA1 Message Date
Aiden Cline 4fa56a8a71 fix(opencode): expand retryable error patterns 2026-08-05 13:13:19 -05:00
Aiden Cline b76df6c359 test(opencode): cover serialized rate limits 2026-08-05 12:31:32 -05:00
Aiden Cline e3366180ae refactor(opencode): simplify retry error matching 2026-08-05 12:30:50 -05:00
2 changed files with 33 additions and 28 deletions
+13 -26
View File
@@ -28,6 +28,13 @@ 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 RETRYABLE_MESSAGE = [
/\b(?:server[_\s-]?error|internal[_\s-]?error|service[_\s-]?unavailable|overloaded|too many requests|rate increased too quickly|provider[_\s-]?returned[_\s-]?error)\b|\brate[_\s-]?limit/i,
/\b(?:fetch failed|network error|upstream connect|connection (?:error|refused|lost)|socket connection was closed|socket hang up|reset before headers|getaddrinfo|enotfound|eai_again)\b|^timeout$|\b(?:request|response|connection|network|stream|read) (?:timeout|timed? out)\b/i,
/\b(?:resource[_\s-]?exhausted|please retry your request|you can retry your request|try your request again)\b/i,
/\b(?:429|500|502|503|504|524)\b/,
]
function cap(ms: number) {
return Math.min(ms, RETRY_MAX_DELAY)
}
@@ -122,32 +129,12 @@ export function retryable(error: Err, provider: string) {
return { message: error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message }
}
// Check for rate limit patterns in plain text error messages
const msg = isRecord(error.data) ? error.data.message : undefined
if (typeof msg === "string") {
const lower = msg.toLowerCase()
if (
lower.includes("rate increased too quickly") ||
lower.includes("rate limit") ||
lower.includes("too many requests")
) {
return { message: msg }
}
}
const json = parseJSON(msg)
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 { message: "Too Many Requests" }
}
if (code.includes("exhausted") || code.includes("unavailable")) {
return { message: "Provider is overloaded" }
}
if (json.type === "error" && typeof json.error?.code === "string" && json.error.code.includes("rate_limit")) {
return { message: "Rate Limited" }
}
const message = isRecord(error.data) ? error.data.message : undefined
if (typeof message !== "string") return undefined
const lower = message.toLowerCase()
if (lower.includes("too_many_requests")) return { message: "Too Many Requests" }
if (lower.includes("exhausted") || lower.includes("unavailable")) return { message: "Provider is overloaded" }
if (RETRYABLE_MESSAGE.some((pattern) => pattern.test(message))) return { message }
return undefined
}
+20 -2
View File
@@ -118,16 +118,21 @@ describe("session.retry.delay", () => {
})
describe("session.retry.retryable", () => {
test("maps too_many_requests json messages", () => {
test("retries serialized too_many_requests messages", () => {
const error = wrap(JSON.stringify({ type: "error", error: { type: "too_many_requests" } }))
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Too Many Requests" })
})
test("maps overloaded provider codes", () => {
test("retries serialized overloaded provider codes", () => {
const error = wrap(JSON.stringify({ code: "resource_exhausted" }))
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Provider is overloaded" })
})
test("retries serialized rate_limit messages", () => {
const message = JSON.stringify({ type: "error", error: { code: "rate_limit_exceeded" } })
expect(SessionRetry.retryable(wrap(message), retryProvider)).toEqual({ message })
})
test("does not retry unknown json messages", () => {
const error = wrap(JSON.stringify({ error: { message: "no_kv_space" } }))
expect(SessionRetry.retryable(error, retryProvider)).toBeUndefined()
@@ -163,6 +168,19 @@ describe("session.retry.retryable", () => {
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: msg })
})
test.each([
"Internal server error",
"Provider returned error",
"fetch failed",
"connection refused",
"EAI_AGAIN",
"response timed out",
"Please retry your request",
"upstream returned status 524",
])("retries matching API error text: %s", (message) => {
expect(SessionRetry.retryable(wrap(message), retryProvider)).toEqual({ message })
})
test("retries transport timeout errors", () => {
const request = MessageV2.fromError(new ProviderError.HeaderTimeoutError(10000), { providerID })
expect(SessionV1.APIError.isInstance(request)).toBe(true)