Skip to content

fix(opencode): retry OpenRouter upstream errors reported inside a 200 stream - #47

Merged
goleary merged 3 commits into
devfrom
gabe-retry-openrouter-stream-errors
Sep 15, 2026
Merged

goleary merged 3 commits into
devfrom
gabe-retry-openrouter-stream-errors

Conversation

@goleary

@goleary goleary commented Sep 15, 2026 •

Copy link
Copy Markdown

A sandbox turn that hits an upstream rate limit or a dropped stream on an OpenRouter-routed model now retries instead of dying on the first hit. Every customer build on those models today ends at the first provider hiccup; this makes them survive the ordinary ones.

Context

OpenRouter forwards upstream failures as a JSON object inside the HTTP 200 SSE body, e.g. {"code":502,"message":"Upstream error from Together: Stream error: h2 protocol error…","metadata":{"error_type":"provider_unavailable"}}. The OpenRouter SDK emits that as a stream error rather than an APICallError, so opencode wraps it as UnknownError with the JSON as its message and no statusCode. retryable() then never reaches the status branch that retries any 5xx, and its JSON branch only reads code when it is a string, so OpenRouter's numeric code matches nothing. Result: no retry, turn fails.

Observed across the cheap-model bakeoff in andytown: every DeepSeek V4.1 Flash unit died on a 429 last week and seven Together stream drops killed three of four units today (bakeoff-r5-grok-ds41), none retried. Same-model retry is the only recovery path in production; the model-fallback config the coordinator ships is not executed by anything (REPL-31522). Upstream has the identical gap and closed it as not planned: anomalyco#22448.

Change

In the JSON branch of retryable(), apply the rule the APIError branch already uses: a numeric code of 429 retries as rate-limited, a numeric code of 500 or above retries as a transient provider failure. Nothing else changes: same backoff, same attempt cap, and 4xx request errors (for example a rejected tool schema) still do not retry.

Testing Done

Three new cases in test/session/retry.test.ts using the real payloads from the failed sessions: the 429 rate_limit_exceeded, the Together 502 provider_unavailable, and a 400 schema rejection that must not retry. bun test test/session/retry.test.ts: 36 pass, 0 fail.

Follow-up once tagged: bump OPENCODE_VERSION in andytown's snapshot Dockerfile and rerun the DeepSeek rows.

🤖 Generated with Claude Code


Devin Review

Summary by cubic

Retries OpenRouter upstream errors that arrive as JSON inside a 200 SSE stream, so sandbox turns survive rate limits and provider 5xxs instead of dying on the first hiccup. Previously these errors were treated as unknown with no numeric status, so they never triggered a retry. 4xx request errors (like a rejected tool schema) still don't retry.

  • Applies the same rule as the APIError branch: numeric 429 retries as rate-limited, numeric 5xx retries as a transient provider failure.
  • Excludes context-overflow errors relayed with a 502 code so they reach compaction instead of the retry loop.
  • Adds tests covering the 429, provider 5xx, overflow exclusion, and 4xx non-retry cases.

Written for commit c357efb. Summary will update on new commits.

Review in cubic

goleary and others added 2 commits September 15, 2026 11:49
…stream

OpenRouter forwards upstream 429s and 5xxs as a JSON object in the SSE
body, so they arrive as UnknownError with no statusCode and the retry
classifier never saw them. Apply the same rule the APIError branch uses.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

1 similar comment
@github-actions

Copy link
Copy Markdown

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions

Copy link
Copy Markdown

The following comment was made by an LLM, it may be inaccurate:

devin-ai-integration[bot]

This comment was marked as resolved.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Re-trigger cubic

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@goleary

goleary commented Sep 15, 2026

Copy link
Copy Markdown
Author

/devin review

@devin-ai-integration

Copy link
Copy Markdown

Starting Devin Review.

Devin Review

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/opencode/src/session/retry.ts Resolved
@goleary
goleary merged commit 9512fd2 into dev Sep 15, 2026
15 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant