diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 6d057f539f81..6a32852c5353 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -59,9 +59,26 @@ export namespace SessionRetry { } export function retryable(error: ReturnType) { + 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` diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 621ad99e9b47..27b0ddfef368 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -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 + + 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 + + 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",