Skip to content

Commit 4217091

Browse files
committed
feat: add retry logic
1 parent f778c68 commit 4217091

2 files changed

Lines changed: 76 additions & 36 deletions

File tree

packages/opencode/src/session/retry.ts

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { NamedError } from "@opencode-ai/shared/util/error"
22
import { Cause, Clock, Duration, Effect, Schedule } from "effect"
33
import { MessageV2 } from "./message-v2"
4-
import { iife } from "@/util/iife"
54

65
export type Err = ReturnType<NamedError["toObject"]>
76

@@ -63,43 +62,19 @@ export function retryable(error: Err) {
6362
return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
6463
}
6564

66-
// Check for rate limit patterns in plain text error messages
6765
const msg = error.data?.message
6866
if (typeof msg === "string") {
6967
const lower = msg.toLowerCase()
7068
if (
71-
lower.includes("rate increased too quickly") ||
72-
lower.includes("rate limit") ||
73-
lower.includes("too many requests")
69+
/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(
70+
lower,
71+
)
7472
) {
73+
if (lower.includes("overloaded") || lower.includes("exhausted") || lower.includes("unavailable")) return "Provider is overloaded"
7574
return msg
7675
}
7776
}
7877

79-
const json = iife(() => {
80-
try {
81-
if (typeof error.data?.message === "string") {
82-
const parsed = JSON.parse(error.data.message)
83-
return parsed
84-
}
85-
86-
return JSON.parse(error.data.message)
87-
} catch {
88-
return undefined
89-
}
90-
})
91-
if (!json || typeof json !== "object") return undefined
92-
const code = typeof json.code === "string" ? json.code : ""
93-
94-
if (json.type === "error" && json.error?.type === "too_many_requests") {
95-
return "Too Many Requests"
96-
}
97-
if (code.includes("exhausted") || code.includes("unavailable")) {
98-
return "Provider is overloaded"
99-
}
100-
if (json.type === "error" && typeof json.error?.code === "string" && json.error.code.includes("rate_limit")) {
101-
return "Rate Limited"
102-
}
10378
return undefined
10479
}
10580

packages/opencode/test/session/retry.test.ts

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,22 +124,22 @@ describe("session.retry.delay", () => {
124124
})
125125

126126
describe("session.retry.retryable", () => {
127-
test("maps too_many_requests json messages", () => {
127+
test("retries too_many_requests in raw json strings", () => {
128128
const error = wrap(JSON.stringify({ type: "error", error: { type: "too_many_requests" } }))
129-
expect(SessionRetry.retryable(error)).toBe("Too Many Requests")
129+
expect(SessionRetry.retryable(error)).toBe(JSON.stringify({ type: "error", error: { type: "too_many_requests" } }))
130130
})
131131

132-
test("maps overloaded provider codes", () => {
132+
test("retries exhausted codes in raw json strings", () => {
133133
const error = wrap(JSON.stringify({ code: "resource_exhausted" }))
134134
expect(SessionRetry.retryable(error)).toBe("Provider is overloaded")
135135
})
136136

137-
test("does not retry unknown json messages", () => {
137+
test("does not retry unknown raw json strings", () => {
138138
const error = wrap(JSON.stringify({ error: { message: "no_kv_space" } }))
139139
expect(SessionRetry.retryable(error)).toBeUndefined()
140140
})
141141

142-
test("does not throw on numeric error codes", () => {
142+
test("does not throw on numeric codes in raw json strings", () => {
143143
const error = wrap(JSON.stringify({ type: "error", error: { code: 123 } }))
144144
const result = SessionRetry.retryable(error)
145145
expect(result).toBeUndefined()
@@ -169,6 +169,66 @@ describe("session.retry.retryable", () => {
169169
expect(SessionRetry.retryable(error)).toBe(msg)
170170
})
171171

172+
test("retries connection errors in plain text", () => {
173+
const msg = "Connection refused"
174+
const error = wrap(msg)
175+
expect(SessionRetry.retryable(error)).toBe(msg)
176+
})
177+
178+
test("retries timeout errors in plain text", () => {
179+
const msg = "Request timed out"
180+
const error = wrap(msg)
181+
expect(SessionRetry.retryable(error)).toBe(msg)
182+
})
183+
184+
test("retries 500 errors in plain text", () => {
185+
const msg = "HTTP 500 Internal Server Error"
186+
const error = wrap(msg)
187+
expect(SessionRetry.retryable(error)).toBe(msg)
188+
})
189+
190+
test("retries overloaded errors in plain text", () => {
191+
const msg = "Provider is overloaded"
192+
const error = wrap(msg)
193+
expect(SessionRetry.retryable(error)).toBe("Provider is overloaded")
194+
})
195+
196+
test("retries 429 errors in plain text", () => {
197+
const msg = "HTTP 429 Too Many Requests"
198+
const error = wrap(msg)
199+
expect(SessionRetry.retryable(error)).toBe(msg)
200+
})
201+
202+
test("retries provider returned errors", () => {
203+
const msg = "Provider returned error: something went wrong"
204+
const error = wrap(msg)
205+
expect(SessionRetry.retryable(error)).toBe(msg)
206+
})
207+
208+
test("retries other side closed errors", () => {
209+
const msg = "Other side closed connection"
210+
const error = wrap(msg)
211+
expect(SessionRetry.retryable(error)).toBe(msg)
212+
})
213+
214+
test("retries reset before headers errors", () => {
215+
const msg = "Connection reset before headers"
216+
const error = wrap(msg)
217+
expect(SessionRetry.retryable(error)).toBe(msg)
218+
})
219+
220+
test("retries ended without errors", () => {
221+
const msg = "Request ended without sending chunks"
222+
const error = wrap(msg)
223+
expect(SessionRetry.retryable(error)).toBe(msg)
224+
})
225+
226+
test("retries retry delay exceeded errors", () => {
227+
const msg = "Retry delay exceeded"
228+
const error = wrap(msg)
229+
expect(SessionRetry.retryable(error)).toBe(msg)
230+
})
231+
172232
test("does not retry context overflow errors", () => {
173233
const error = new MessageV2.ContextOverflowError({
174234
message: "Input exceeds context window of this model",
@@ -250,9 +310,14 @@ describe("session.retry.retryable", () => {
250310
expect(SessionRetry.retryable(error)).toBe("Provider is overloaded")
251311
})
252312

253-
test("maps rate_limit error code in nested json", () => {
313+
test("retries rate_limit codes in raw json strings", () => {
254314
const error = wrap(JSON.stringify({ type: "error", error: { code: "rate_limit_exceeded" } }))
255-
expect(SessionRetry.retryable(error)).toBe("Rate Limited")
315+
expect(SessionRetry.retryable(error)).toBe(JSON.stringify({ type: "error", error: { code: "rate_limit_exceeded" } }))
316+
})
317+
318+
test("retries server_error in raw json strings", () => {
319+
const error = wrap(JSON.stringify({ type: "error", error: { type: "server_error", code: "server_error" } }))
320+
expect(SessionRetry.retryable(error)).toBe(JSON.stringify({ type: "error", error: { type: "server_error", code: "server_error" } }))
256321
})
257322
})
258323

0 commit comments

Comments
 (0)