Repository navigation
provider: opencode-go 400 stub on long sessions varies per attempt (identical bytes replay 200/400/200) #51201
Description
Summary
Long opencode-go/deepseek-v4.1-flash sessions intermittently fail with an unparseable HTTP 400
whose entire body is a 31-byte stub ({"model":"deepseek-v4.1-flash"}). Measurements on a second
platform show the stub is not a stable verdict on the payload: replaying the exact captured
failing request byte-for-byte returned 200, 400, 200 across three consecutive attempts (and
5×200 + 1×400 in a six-attempt loop), so a retry usually recovers — but the client classifies the
400 as non-retryable and ends the turn.
Related: #50761 (same 31-byte stub, route-dependent ceiling analysis), #50446 (provider body never
surfaced), #50574 (compaction ceiling / overflow classification), #48093 (older opaque 400 on
deepseek-v4-flash).
Environment
- opencode version: 2.0.15 (desktop app, bundled server)
- OS: Windows 11 (build 10.0.26200, x64)
- Terminal: desktop app (Electron)
- Shell: PowerShell 7
- Install/channel: desktop app, channel
latest - Active plugins: local plugins including a retry guard that re-frames transient zen/go responses as
retryable 5xx; unrelated to the trigger, used below as the mitigation test - Provider/model:
opencode-go/deepseek-v4.1-flash(variantmax), endpoint
https://opencode.ai/zen/go/v1/chat/completions
Reproduction
Observed organically in a long session (~1,700 messages, ~3.5 MB request body, ~870k prompt tokens
from usage counters), then reduced to direct replays:
- Capture one failing request body client-side after an HTTP 400 carrying the 31-byte stub.
- Replay it byte-faithfully to the same endpoint with the same headers and credential: 200, 400,
200 across three consecutive attempts; 5×200 + 1×400 in a later six-attempt loop. - Control: a minimal two-message body with the same headers returns 200 consistently (22/22).
No request content is needed to reproduce the shape; the stub body and timings are enough.
Expected Behavior
Either the gateway returns a structured, retryable error for this class (with an error.message and
a 5xx or retry hint), or the client treats a 400 whose body carries no error information as
retryable once, and persists the raw body so the cause is visible.
Actual Behavior
- HTTP 400, body exactly
{"model":"deepseek-v4.1-flash"}(31 bytes),content-type: application/json;
edge colos seen for the stub: EWR, BOS, CMH. - Client surfaces only
Provider request failed with HTTP 400, classifiesprovider.invalid-request,
and ends the turn; manually retrying the same turn usually advances one step. - The 31-byte body is dropped when the error is persisted, so nothing on the client can explain the
failure.
Additional Context
- The ~870k-token prompt sits below the ~970k figure reported in provider: opencode-go route-dependent context limits cause unparseable HTTP 400 in long sessions #50761 for one upstream route, so on
this platform the stub also appears below that ceiling. - Concurrent long sessions (2.4–3.8 MB request bodies) in the same installation produced no stub
400s in a ~20-minute window (146/148 observed responses were 200). - Mitigation verified locally: re-framing the exact stub signature (status 400, JSON object with a
modelfield, noerrorobject, no top-levelmessage) as a retryable 5xx with
x-should-retry: trueand a shortretry-after-msrecovered the turn with two consecutive
injected stubs (retries at 3 s and 10 s; the third attempt completed).
Activity
aanbrn commentedon Sep 26, 2026
Still present on 2.0.18 — confirming from a different platform, with the client-side record as evidence.
Environment: opencode 2.0.18 (CLI, Homebrew
opencode-v2), macOS 27.0 (arm64), zsh, no plugins,opencode-go/deepseek-v4.1-flash, endpointhttps://opencode.ai/zen/go/v1/chat/completions.Observed: one session (started 2026-09-24 09:05 UTC) hit 7
provider.invalid-request400s in ~3.5 h —2026-09-25 23:48 → 2026-09-26 03:16 UTC: four on 2.0.16 (23:48, 00:02, 00:40, 00:59) and three after the upgrade on
2.0.18 (01:29, 02:36, 03:16). Requests of the same size succeeded minutes either side (e.g. 534,912- and
535,040-token requests passed around the 03:16 failure), and the session had passed that size for the preceding two
days — so, as your byte-faithful replay shows, this is not a stable verdict on the payload. Continuing the turn always
recovered it; no session was bricked.
One sizing datapoint, to keep this distinct from a pure ceiling: the session did reach 967,680 cached prompt tokens
and a request at that size succeeded (2026-09-25 13:08), ~10 h before the failures — the 400s themselves were at
~350–535k, well below the ~968k figure in #50574.
Persisted error record — confirming the body drop (your Actual Behavior): on 2.0.18 the failed assistant message
carries only
{"type":"provider.invalid-request","message":"Provider request failed with HTTP 400","status":400}with no provider body, so nothing after the fact recovers the stub. To reproduce the record shape (read the DB with the
app running, or append
&immutable=1):SamSpiri commentedon Sep 29, 2026
Built the smallest client-side version of this issue's Expected Behavior, and looking for testers:
https://github.com/SamSpiri/opencode-go-stub-400-retry
Single-file V2 plugin using
session.hook("retry"); copy into~/.config/opencode/plugins/, no config to edit. It retries the bare-400 signature twice (3 s, 9 s) and leaves structured 400s alone, so it does not retry the deterministic classes that present as the same stub — thereasoning_content×reasoning_effortrejection and requests past a smaller upstream's ceiling.The reason I am posting rather than just using it: I cannot validate the trigger. On my account the stub runs at roughly 1 % of turns (4 failures in ~380 turns today), and it cannot be induced on demand, so a clean window after installing proves nothing. It has not fired once yet, which is expected at that rate and tells me nothing either way.
If you are hitting this regularly, the useful report is simply whether the turn now recovers on its own (a retry countdown appears and work continues) or dies the same as before. Both outcomes are worth posting back, and the failure half is the one that would tell maintainers the client-side retry is not sufficient.
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
JohnnyHolmes commentedon Oct 21, 2026
Still present on 2026-10-21, now reproduced from a different client: Claude Code 2.1.306 using the Anthropic-compatible endpoint
POST /zen/go/v1/messages(not/v1/chat/completions). Same 31-byte body, same byte-identical replay behavior.Environment
ANTHROPIC_BASE_URL=https://opencode.ai/zen/go/deepseek-v4.1-flashthinking: adaptive,max_tokens: 32000claude-code-20250219,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-tool-changes-2026-07-01,advisor-tool-2026-03-01,effort-2025-11-24,afk-mode-2026-01-31New measurements
Status sequence:
cf-placement: remote-IAD.Retry behavior
At 12:14:19Z Claude Code retried the identical request 6 times; all 6 returned the 31-byte 400. The next attempt at 12:14:35Z, with a slightly changed body, returned 200. So a short 2–3 retry policy can still lose inside a burst; a retry needs to back off ~30–60 s, or the gateway should return 429/503 with
Retry-Afterfor this class.Gateway log IDs (all valid requests, 400):
Earlier failures from the same client:
I can provide the 30-row CSV (timestamp/status/latency/log-id, no request content or credentials) if useful.
Suggestion
Since this affects any Anthropic-compatible client, not just OpenCode's own, it would help to:
type,message,param) for real validation failures, instead of the 31-byte model echo.Retry-Afterfor the transient class, so clients can back off instead of treating it as a terminal 400.opencodejr commentedon Oct 28, 2026
Thanks for the detailed measurements, especially the byte-identical replay test.
I think this is a duplicate of #50761. It reports the same 31-byte
{"model":"deepseek-v4.1-flash"}HTTP 400 onopencode-go/deepseek-v4.1-flashin long sessions, the sameprovider.invalid-requesthandling that ends the turn, and it already notes that the same step can succeed on a retry with no changes (traced there to thenovita-deepseekupstream route).Possible duplicates and closely related issues, ordered by likelihood:
deepseek-v4-flash(closed). A commenter there shared a client-side retry plugin for this stub.Your replay results (200/400/200 on identical bytes, the stub appearing at ~870k tokens) and the retry-on-stub mitigation would be useful as a comment on #50761.
The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.
opencodejr commentedon Oct 29, 2026
We haven't heard back, so I'm closing this as a duplicate of #50761, which tracks the same intermittent 31-byte
{"model":"deepseek-v4.1-flash"}HTTP 400 onopencode-go.Your replay results (200/400/200 on identical bytes, the stub showing up at ~870k tokens) and the retry-on-stub mitigation would be useful as a comment on #50761. The related #50446 and #50574 cover the missing response body and the overflow and compaction handling.
If your problem turns out to be different, reply here and we can reopen this. Thanks for the thorough report.