You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
opencode-go/deepseek-v4.1-flash randomly rejects requests with 400/403 #51434
I'm hitting random failures with deepseek-v4.1-flash on the opencode-go provider. The session just stops with:
Bad Request: {"model":"deepseek-v4.1-flash"}
Sometimes I also see it come back as a 403 with error code: 1010 when I replay the same request by hand.
It happens after a few turns where the assistant made tool calls (MCP tools, all of them completed fine). Nothing special happens right before it - no interrupt, no crash. It just fails on an ordinary tool call turn.
I pulled the message history for the failed turn straight out of ~/.local/share/opencode/opencode.db and sent the exact same messages array to https://opencode.ai/zen/go/v1/chat/completions with a valid key and session header. The gateway still rejects it, so it's not a client-side bug.
Happens on both my machines with the same setup. I'm on the latest version (1.18.32), so there's nothing to upgrade to.
Plugins
none
OpenCode version
1.18.32
Steps to reproduce
Start a session on opencode-go with deepseek-v4.1-flash, with a few MCP servers connected (I used railway).
Let it run through several turns where the assistant calls tools and gets results back normally.
On one of the next turns the request fails with Bad Request: {"model":"deepseek-v4.1-flash"}.
Open ~/.local/share/opencode/opencode.db (sqlite), pull the messages/parts for the failed assistant message, and rebuild the same messages[] array.
POST it to https://opencode.ai/zen/go/v1/chat/completions with model: deepseek-v4.1-flash, a valid Authorization header and x-opencode-session.
Gateway returns 403 error code: 1010 (or 400 with just the model name in the body).
Failing message in my case: msg_0db775f66001N2FWEzAPquvNsT.
One thing I checked — every tool part in that history has state.status: "completed". So this isn't a dangling or half-finished tool call.
Screenshot and/or share link
No response
Operating System
Windows 11
Terminal
Windows Terminal (PowerShell)
Reacted by Purushottam Singram, Yangguang Li, euwbah and Simon Spiridonov
It implements the second paragraph of the Expected Behavior above: a 400 whose body carries no error information is retried a couple of times instead of ending the turn. Structured 400s keep the built-in policy, so deterministic rejections are not retried pointlessly.
Single file, no dependencies. Install by copying it into ~/.config/opencode/plugins/; it uses the V2 session.hook("retry") API, and OpenCode discovers files in that directory automatically, so there is no config to edit. It matches only: provider opencode-go + model deepseek-v4-flash / deepseek-v4.1-flash + status 400 + provider.invalid-request + an opaque message (the Provider request failed with HTTP 400 fallback, a bare Bad Request, or the raw {"model":"..."} body). Two extra attempts, 3 s then 9 s.
Worth stating plainly:
This is a workaround for the client-side classification gap, not a fix for the gateway emitting the stub. The stray {"model":"..."} body is still unexplained and still unrecoverable client-side, which is the part that actually needs maintainer eyes — worth noting again here that the body is dropped when the error is persisted, so nothing after the fact can recover it.
The attempt count is a heuristic constant, not a measurement.
I have not observed it fire on a live stub, since that can't be induced on demand. What I did verify: the matcher against the shapes an empty-body 400 can take, and that the surfaced message really is the client fallback rather than a gateway string — from the 2.0.19 provider-error path, where the status-derived Provider request failed with HTTP ${status} is used whenever the parsed body yields no message/code.
If an upstream change is preferred over a plugin, the smallest version is #50574's item 1: propagate the provider response body into the classifier so isContextOverflowError can see it, and treat a 400 with no error information as retryable once.
My first version of this did not fire, and the reason is worth recording: the message is not stable across versions. On 2.0.19 a bodyless 400 persisted as the bare Provider request failed with HTTP 400. On 2.0.21 it arrives as
Provider request failed with HTTP 400: {"model":"deepseek-v4.1-flash"}
— the raw body appended — and the stub is additionally persisted at error.response.body. A matcher anchored ^...$ on the bare form silently no-ops on the second shape. That is what happened on a machine of mine that hit this today, plugin loaded and active.
Fixed, and now verified end to end rather than assumed. Pointing a throwaway provider at a local server that answers 400 with {"model":"deepseek-v4.1-flash"} for the first three requests:
plugin
requests observed
outcome
disabled
3, all 400, same second
turn fails: Provider request failed with HTTP 400
enabled
4 — the three, then one 9.1 s later returning 200
turn completes
Without it the turn dies exactly as reported here; with it the retry carries the turn through. That is a direct demonstration of the retryability claim in this issue, on a client that otherwise treats the 4xx as terminal.
Two points that may help the actual fix:
On 2.0.21 the stub is retained, at error.response.body on the persisted assistant message. The client can therefore distinguish an empty-body 400 from a structured one without any gateway change — which is most of what this issue's Expected Behavior asks for.
Anything keying off the message text should not anchor on the bare fallback form, since it is version-dependent.
Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.
Activity
github-actions commentedon Sep 26, 2026
This issue might be a duplicate of existing issues. Please check:
{"model":"deepseek-v4.1-flash"}response; replaying the identical request alternated between 200 and 400.deepseek-v4.1-flash.deepseek-v4.1-flashwith large payloads and high reasoning effort.chinarma commentedon Sep 27, 2026
every 8-10 step happens 。
SamSpiri commentedon Sep 29, 2026
Client-side workaround for the retry gap described here.
https://github.com/SamSpiri/opencode-go-stub-400-retry
It implements the second paragraph of the Expected Behavior above: a 400 whose body carries no error information is retried a couple of times instead of ending the turn. Structured 400s keep the built-in policy, so deterministic rejections are not retried pointlessly.
Single file, no dependencies. Install by copying it into
~/.config/opencode/plugins/; it uses the V2session.hook("retry")API, and OpenCode discovers files in that directory automatically, so there is no config to edit. It matches only: provideropencode-go+ modeldeepseek-v4-flash/deepseek-v4.1-flash+ status 400 +provider.invalid-request+ an opaque message (theProvider request failed with HTTP 400fallback, a bareBad Request, or the raw{"model":"..."}body). Two extra attempts, 3 s then 9 s.Worth stating plainly:
{"model":"..."}body is still unexplained and still unrecoverable client-side, which is the part that actually needs maintainer eyes — worth noting again here that the body is dropped when the error is persisted, so nothing after the fact can recover it.reasoning_content×reasoning_effortrejection (Console Go relay: HTTP 400 (empty body) when request combines reasoning_content echo-back with reasoning_effort on deepseek-v4-flash #48180) and requests past a smaller upstream's ceiling (provider: opencode-go route-dependent context limits cause unparseable HTTP 400 in long sessions #50761, compaction: 1M-context sessions never auto-compact before the provider returns HTTP 400, and the 400 is not treated as context overflow #50574). Matching on an opaque message excludes them, and retrying them would be a no-op rather than a fix.I have not observed it fire on a live stub, since that can't be induced on demand. What I did verify: the matcher against the shapes an empty-body 400 can take, and that the surfaced message really is the client fallback rather than a gateway string — from the 2.0.19 provider-error path, where the status-derived
Provider request failed with HTTP ${status}is used whenever the parsed body yields nomessage/code.If an upstream change is preferred over a plugin, the smallest version is #50574's item 1: propagate the provider response body into the classifier so
isContextOverflowErrorcan see it, and treat a 400 with no error information as retryable once.SamSpiri commentedon Oct 2, 2026
Correction to my earlier comment, with results.
My first version of this did not fire, and the reason is worth recording: the message is not stable across versions. On 2.0.19 a bodyless 400 persisted as the bare
Provider request failed with HTTP 400. On 2.0.21 it arrives as— the raw body appended — and the stub is additionally persisted at
error.response.body. A matcher anchored^...$on the bare form silently no-ops on the second shape. That is what happened on a machine of mine that hit this today, plugin loaded and active.Fixed, and now verified end to end rather than assumed. Pointing a throwaway provider at a local server that answers 400 with
{"model":"deepseek-v4.1-flash"}for the first three requests:Provider request failed with HTTP 400Without it the turn dies exactly as reported here; with it the retry carries the turn through. That is a direct demonstration of the retryability claim in this issue, on a client that otherwise treats the 4xx as terminal.
Two points that may help the actual fix:
error.response.bodyon the persisted assistant message. The client can therefore distinguish an empty-body 400 from a structured one without any gateway change — which is most of what this issue's Expected Behavior asks for.Updated plugin: https://github.com/SamSpiri/opencode-go-stub-400-retry
thdxr commentedon Oct 11, 2026
Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (
opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊