Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/opencode/src/session/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,26 @@ export namespace SessionRetry {
}

export function retryable(error: ReturnType<NamedError["toObject"]>) {
const rateLimited = (input: string | undefined) => {
if (!input) return false
return /too many requests|rate[_\s-]?limit/i.test(input)
}
const quotaLimited = (input: string | undefined) => {
if (!input) return false
return /insufficient[_\s-]?quota|quota exceeded|freeusagelimiterror|usage_not_included/i.test(input)
}

// context overflow errors should not be retried
if (MessageV2.ContextOverflowError.isInstance(error)) return undefined
if (MessageV2.APIError.isInstance(error)) {
if (
error.data.statusCode === 429 &&
!quotaLimited(error.data.message) &&
!quotaLimited(error.data.responseBody)
) {
if (rateLimited(error.data.message) || rateLimited(error.data.responseBody)) return "Too Many Requests"
return error.data.message || "Too Many Requests"
}
if (!error.data.isRetryable) return undefined
if (error.data.responseBody?.includes("FreeUsageLimitError"))
return `Free usage exceeded, add credits https://opencode.ai/zen`
Expand Down
22 changes: 22 additions & 0 deletions packages/opencode/test/session/retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,28 @@ describe("session.retry.retryable", () => {
expect(SessionRetry.retryable(error)).toBeUndefined()
})

test("retries 429 API errors even when isRetryable is false", () => {
const error = new MessageV2.APIError({
message: "Rate limit reached for requests per min",
statusCode: 429,
isRetryable: false,
responseBody: '{"error":{"message":"Too many requests"}}',
}).toObject() as ReturnType<NamedError["toObject"]>

expect(SessionRetry.retryable(error)).toBe("Too Many Requests")
})

test("does not retry 429 quota exhaustion errors", () => {
const error = new MessageV2.APIError({
message: "insufficient_quota",
statusCode: 429,
isRetryable: false,
responseBody: '{"error":{"code":"insufficient_quota"}}',
}).toObject() as ReturnType<NamedError["toObject"]>

expect(SessionRetry.retryable(error)).toBeUndefined()
})

test("does not retry context overflow errors", () => {
const error = new MessageV2.ContextOverflowError({
message: "Input exceeds context window of this model",
Expand Down
Loading