diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 6d1125c66..5b45dc14d 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -333,9 +333,9 @@ export namespace SessionProcessor { error: e, }) const error = MessageV2.fromError(e, { providerID: input.providerID }) - if (error?.name === "APIError" && error.data.isRetryable) { + if ((error?.name === "APIError" && error.data.isRetryable) || error.data.message.includes("Overloaded")) { attempt++ - const delay = SessionRetry.delay(error, attempt) + const delay = SessionRetry.delay(attempt, error.name === "APIError" ? error : undefined) SessionStatus.set(input.sessionID, { type: "retry", attempt, diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 75472b568..4ad81ea08 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -19,32 +19,34 @@ export namespace SessionRetry { }) } - export function delay(error: MessageV2.APIError, attempt: number) { - const headers = error.data.responseHeaders - if (headers) { - const retryAfterMs = headers["retry-after-ms"] - if (retryAfterMs) { - const parsedMs = Number.parseFloat(retryAfterMs) - if (!Number.isNaN(parsedMs)) { - return parsedMs + export function delay(attempt: number, error?: MessageV2.APIError) { + if (error) { + const headers = error.data.responseHeaders + if (headers) { + const retryAfterMs = headers["retry-after-ms"] + if (retryAfterMs) { + const parsedMs = Number.parseFloat(retryAfterMs) + if (!Number.isNaN(parsedMs)) { + return parsedMs + } } - } - const retryAfter = headers["retry-after"] - if (retryAfter) { - const parsedSeconds = Number.parseFloat(retryAfter) - if (!Number.isNaN(parsedSeconds)) { - // convert seconds to milliseconds - return Math.ceil(parsedSeconds * 1000) + const retryAfter = headers["retry-after"] + if (retryAfter) { + const parsedSeconds = Number.parseFloat(retryAfter) + if (!Number.isNaN(parsedSeconds)) { + // convert seconds to milliseconds + return Math.ceil(parsedSeconds * 1000) + } + // Try parsing as HTTP date format + const parsed = Date.parse(retryAfter) - Date.now() + if (!Number.isNaN(parsed) && parsed > 0) { + return Math.ceil(parsed) + } } - // Try parsing as HTTP date format - const parsed = Date.parse(retryAfter) - Date.now() - if (!Number.isNaN(parsed) && parsed > 0) { - return Math.ceil(parsed) - } - } - return RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1) + return RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1) + } } return Math.min(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1), RETRY_MAX_DELAY_NO_HEADERS) diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index dc7470f0a..b685eae95 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -13,49 +13,49 @@ function apiError(headers?: Record): MessageV2.APIError { describe("session.retry.delay", () => { test("caps delay at 30 seconds when headers missing", () => { const error = apiError() - const delays = Array.from({ length: 10 }, (_, index) => SessionRetry.delay(error, index + 1)) + const delays = Array.from({ length: 10 }, (_, index) => SessionRetry.delay(index + 1, error)) expect(delays).toStrictEqual([2000, 4000, 8000, 16000, 30000, 30000, 30000, 30000, 30000, 30000]) }) test("prefers retry-after-ms when shorter than exponential", () => { const error = apiError({ "retry-after-ms": "1500" }) - expect(SessionRetry.delay(error, 4)).toBe(1500) + expect(SessionRetry.delay(4, error)).toBe(1500) }) test("uses retry-after seconds when reasonable", () => { const error = apiError({ "retry-after": "30" }) - expect(SessionRetry.delay(error, 3)).toBe(30000) + expect(SessionRetry.delay(3, error)).toBe(30000) }) test("accepts http-date retry-after values", () => { const date = new Date(Date.now() + 20000).toUTCString() const error = apiError({ "retry-after": date }) - const d = SessionRetry.delay(error, 1) + const d = SessionRetry.delay(1, error) expect(d).toBeGreaterThanOrEqual(19000) expect(d).toBeLessThanOrEqual(20000) }) test("ignores invalid retry hints", () => { const error = apiError({ "retry-after": "not-a-number" }) - expect(SessionRetry.delay(error, 1)).toBe(2000) + expect(SessionRetry.delay(1, error)).toBe(2000) }) test("ignores malformed date retry hints", () => { const error = apiError({ "retry-after": "Invalid Date String" }) - expect(SessionRetry.delay(error, 1)).toBe(2000) + expect(SessionRetry.delay(1, error)).toBe(2000) }) test("ignores past date retry hints", () => { const pastDate = new Date(Date.now() - 5000).toUTCString() const error = apiError({ "retry-after": pastDate }) - expect(SessionRetry.delay(error, 1)).toBe(2000) + expect(SessionRetry.delay(1, error)).toBe(2000) }) test("uses retry-after values even when exceeding 10 minutes with headers", () => { const error = apiError({ "retry-after": "50" }) - expect(SessionRetry.delay(error, 1)).toBe(50000) + expect(SessionRetry.delay(1, error)).toBe(50000) const longError = apiError({ "retry-after-ms": "700000" }) - expect(SessionRetry.delay(longError, 1)).toBe(700000) + expect(SessionRetry.delay(1, longError)).toBe(700000) }) })