Skip to content
Merged
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: 7 additions & 0 deletions packages/opencode/src/session/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Cause, Clock, Duration, Effect, Schedule } from "effect"
import { MessageV2 } from "./message-v2"
import { iife } from "@/util/iife"
import { isRecord } from "@/util/record"
import { ProviderError } from "@/provider/error"

export type Err = ReturnType<NamedError["toObject"]>

Expand Down Expand Up @@ -137,6 +138,12 @@ export function retryable(error: Err, provider: string) {

const json = parseJSON(msg)
if (!json || typeof json !== "object") return undefined
// OpenRouter reports upstream failures inside a 200 stream as {code: <http status>, ...},
// so the status never reaches the APIError branch above. Same rule as there: 429 and 5xx retry,
// except an overflow relayed with a 502 code, which must reach compaction instead of the retry loop.
if (ProviderError.parseStreamError(json)?.type === "context_overflow") return undefined
Comment thread
goleary marked this conversation as resolved.
Resolved
if (json.code === 429) return { message: "Rate Limited" }
if (typeof json.code === "number" && json.code >= 500) return { message: "Provider is overloaded" }
Comment thread
goleary marked this conversation as resolved.
Resolved
const code = typeof json.code === "string" ? json.code : ""

if (json.type === "error" && json.error?.type === "too_many_requests") {
Expand Down
32 changes: 32 additions & 0 deletions packages/opencode/test/session/retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,38 @@ describe("session.retry.retryable", () => {
expect(SessionRetry.retryable(error, retryProvider)).toBeUndefined()
})

test("retries OpenRouter rate limits reported inside a 200 stream", () => {
const error = wrap(
JSON.stringify({
code: 429,
message: "Provider returned error",
metadata: { error_type: "rate_limit_exceeded" },
}),
)
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Rate Limited" })
})

test("retries OpenRouter upstream 5xx reported inside a 200 stream", () => {
const error = wrap(
JSON.stringify({
code: 502,
message: "Upstream error from Together: Stream error: h2 protocol error: error reading a body from connection",
metadata: { error_type: "provider_unavailable" },
}),
)
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Provider is overloaded" })
})

test("does not retry an OpenRouter context overflow relayed with a 502 code", () => {
const error = wrap(JSON.stringify({ code: 502, message: "Your input exceeds the context window of this model" }))
expect(SessionRetry.retryable(error, retryProvider)).toBeUndefined()
})

test("does not retry OpenRouter 4xx request errors reported inside a 200 stream", () => {
const error = wrap(JSON.stringify({ code: 400, message: "Error resolving schema reference '#/$defs/__schema0'" }))
expect(SessionRetry.retryable(error, retryProvider)).toBeUndefined()
})

test("does not throw on numeric error codes", () => {
const error = wrap(JSON.stringify({ type: "error", error: { code: 123 } }))
const result = SessionRetry.retryable(error, retryProvider)
Expand Down
Loading