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
7 changes: 5 additions & 2 deletions packages/opencode/src/session/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,11 @@ export function retryable(error: Err, provider: string) {
if (SessionV1.APIError.isInstance(error)) {
const status = error.data.statusCode
// 5xx errors are transient server failures and should always be retried,
// even when the provider SDK doesn't explicitly mark them as retryable.
if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined
// even when the provider SDK doesn't explicitly mark them as retryable. 408 is the one 4xx
// that is transient in the same way: the request timed out before completing, so it is safe
// to send again.
if (!error.data.isRetryable && !(status !== undefined && (status === 408 || status >= 500)))
return undefined
if (error.data.responseBody?.includes("FreeUsageLimitError")) {
return {
message: GO_UPSELL_MESSAGE,
Expand Down
20 changes: 20 additions & 0 deletions packages/opencode/test/session/retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,26 @@ describe("session.retry.retryable", () => {
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Service unavailable" })
})

test("retries 408 request timeout errors", () => {
const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
new SessionV1.APIError({
message: "stream error: stream disconnected before completion",
isRetryable: false,
statusCode: 408,
responseBody: JSON.stringify({
type: "error",
sequence_number: 0,
code: "request_timeout",
message: "stream error: stream disconnected before completion",
}),
}).toObject(),
)

expect(SessionRetry.retryable(error, retryProvider)).toEqual({
message: "stream error: stream disconnected before completion",
})
})

test("does not retry 4xx errors when isRetryable is false", () => {
const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
new SessionV1.APIError({
Expand Down
Loading