Skip to content

Commit ae3860b

Browse files
committed
fix: retry codex overloaded stream errors
1 parent b70e270 commit ae3860b

3 files changed

Lines changed: 51 additions & 4 deletions

File tree

packages/opencode/src/provider/error.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export function parseStreamError(input: unknown): ParsedStreamError | undefined
152152
responseBody,
153153
}
154154
case "server_error":
155+
case "server_is_overloaded":
155156
return {
156157
type: "api_error",
157158
message: typeof body?.error?.message === "string" ? body?.error?.message : "Server error.",

packages/opencode/src/session/retry.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export function retryable(error: Err) {
6060
// even when the provider SDK doesn't explicitly mark them as retryable.
6161
if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined
6262
if (error.data.responseBody?.includes("FreeUsageLimitError")) return GO_UPSELL_MESSAGE
63-
return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
63+
return error.data.message.toLowerCase().includes("overloaded") ? "Provider is overloaded" : error.data.message
6464
}
6565

6666
// Check for rate limit patterns in plain text error messages
@@ -89,15 +89,22 @@ export function retryable(error: Err) {
8989
}
9090
})
9191
if (!json || typeof json !== "object") return undefined
92-
const code = typeof json.code === "string" ? json.code : ""
92+
const codes = [
93+
typeof json.code === "string" ? json.code : "",
94+
typeof json.error?.code === "string" ? json.error.code : "",
95+
typeof json.error?.type === "string" ? json.error.type : "",
96+
]
9397

9498
if (json.type === "error" && json.error?.type === "too_many_requests") {
9599
return "Too Many Requests"
96100
}
97-
if (code.includes("exhausted") || code.includes("unavailable")) {
101+
if (codes.some((code) => code.includes("exhausted") || code.includes("unavailable") || code.includes("overloaded"))) {
98102
return "Provider is overloaded"
99103
}
100-
if (json.type === "error" && typeof json.error?.code === "string" && json.error.code.includes("rate_limit")) {
104+
if (
105+
json.type === "error" &&
106+
codes.some((code) => code.includes("rate_limit") || code.includes("too_many_requests"))
107+
) {
101108
return "Rate Limited"
102109
}
103110
return undefined

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,22 @@ describe("session.retry.retryable", () => {
126126
expect(SessionRetry.retryable(error)).toBe("Provider is overloaded")
127127
})
128128

129+
test("maps nested overloaded provider errors", () => {
130+
const error = wrap(
131+
JSON.stringify({
132+
type: "error",
133+
sequence_number: 2,
134+
error: {
135+
type: "service_unavailable_error",
136+
code: "server_is_overloaded",
137+
message: "Our servers are currently overloaded. Please try again later.",
138+
param: null,
139+
},
140+
}),
141+
)
142+
expect(SessionRetry.retryable(error)).toBe("Provider is overloaded")
143+
})
144+
129145
test("does not retry unknown json messages", () => {
130146
const error = wrap(JSON.stringify({ error: { message: "no_kv_space" } }))
131147
expect(SessionRetry.retryable(error)).toBeUndefined()
@@ -323,4 +339,27 @@ describe("session.message-v2.fromError", () => {
323339
expect(result.data.isRetryable).toBe(true)
324340
expect(SessionRetry.retryable(result)).toBe("An error occurred while processing your request.")
325341
})
342+
343+
test("converts OpenAI server_is_overloaded stream chunks to retryable APIError", () => {
344+
const result = MessageV2.fromError(
345+
{
346+
message: JSON.stringify({
347+
type: "error",
348+
sequence_number: 2,
349+
error: {
350+
type: "service_unavailable_error",
351+
code: "server_is_overloaded",
352+
message: "Our servers are currently overloaded. Please try again later.",
353+
param: null,
354+
},
355+
}),
356+
},
357+
{ providerID: ProviderID.make("openai") },
358+
)
359+
360+
expect(MessageV2.APIError.isInstance(result)).toBe(true)
361+
if (!MessageV2.APIError.isInstance(result)) throw new Error("expected APIError")
362+
expect(result.data.isRetryable).toBe(true)
363+
expect(SessionRetry.retryable(result)).toBe("Provider is overloaded")
364+
})
326365
})

0 commit comments

Comments
 (0)