Skip to content
Closed
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
6 changes: 4 additions & 2 deletions packages/opencode/src/session/message-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,6 @@ export function fromError(
{ cause: e },
).toObject()
case e instanceof Error:
return new NamedError.Unknown({ message: errorMessage(e) }, { cause: e }).toObject()
default:
try {
const parsed = ProviderError.parseStreamError(e)
Expand All @@ -726,7 +725,10 @@ export function fromError(
).toObject()
}
} catch {}
return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject()
return new NamedError.Unknown(
{ message: e instanceof Error ? errorMessage(e) : JSON.stringify(e) },
{ cause: e },
).toObject()
}
}

Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/session/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,15 @@ export function retryable(error: Err, provider: string) {
return { message: error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message }
}

// Check for rate limit patterns in plain text error messages
// Check for retryable patterns in plain text error messages
const msg = isRecord(error.data) ? error.data.message : undefined
if (typeof msg === "string") {
const lower = msg.toLowerCase()
if (
lower.includes("rate increased too quickly") ||
lower.includes("rate limit") ||
lower.includes("too many requests")
lower.includes("too many requests") ||
lower.includes("our servers are currently overloaded")
) {
return { message: msg }
}
Expand Down
40 changes: 40 additions & 0 deletions packages/opencode/test/session/retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,4 +437,44 @@ describe("session.message-v2.fromError", () => {
message: "An error occurred while processing your request.",
})
})

test("converts overloaded stream errors wrapped in Error to retryable APIError", () => {
const error = new Error(
JSON.stringify({
type: "error",
error: {
type: "service_unavailable_error",
code: "server_is_overloaded",
message: "The server is overloaded. Please try again later.",
},
}),
)
const result = MessageV2.fromError(error, { providerID: ProviderV2.ID.make("openai") })

expect(SessionV1.APIError.isInstance(result)).toBe(true)
if (!SessionV1.APIError.isInstance(result)) throw new Error("expected APIError")
expect(result.data.isRetryable).toBe(true)
expect(SessionRetry.retryable(result, retryProvider)).toEqual({
message: "The server is overloaded. Please try again later.",
})
})

test("preserves unknown Error messages as UnknownError", () => {
const result = MessageV2.fromError(new Error("Unknown error"), { providerID })

expect(result).toStrictEqual({
name: "UnknownError",
data: {
message: "Unknown error",
},
})
})

test("retries flattened OpenAI-compatible overload Errors", () => {
const error = new Error("Our servers are currently overloaded. Please try again later.")
const result = MessageV2.fromError(error, { providerID: ProviderV2.ID.make("openai") })

expect(result.name).toBe("UnknownError")
expect(SessionRetry.retryable(result, retryProvider)).toEqual({ message: error.message })
})
})
Loading