Skip to content

provider: opencode-go 400 stub on long sessions varies per attempt (identical bytes replay 200/400/200) #51201

Closed as duplicate

Description

@4Liberty

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 (variant max), 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:

  1. Capture one failing request body client-side after an HTTP 400 carrying the 31-byte stub.
  2. 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.
  3. 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, classifies provider.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
    model field, no error object, no top-level message) as a retryable 5xx with
    x-should-retry: true and a short retry-after-ms recovered the turn with two consecutive
    injected stubs (retries at 3 s and 10 s; the third attempt completed).

Activity

  1. aanbrn commented on Sep 26, 2026

    @aanbrn

    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, endpoint https://opencode.ai/zen/go/v1/chat/completions.

    Observed: one session (started 2026-09-24 09:05 UTC) hit 7 provider.invalid-request 400s 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):

    sqlite3 "file:$HOME/.local/share/opencode/opencode.db?mode=ro" \
      "SELECT datetime(time_created/1000,'unixepoch'), json_extract(data,'\$.error') FROM session_message
       WHERE session_id='<ses_…>' AND json_extract(data,'\$.error') IS NOT NULL ORDER BY seq DESC LIMIT 5;"
  2. SamSpiri commented on Sep 29, 2026

    @SamSpiri

    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 — the reasoning_content × reasoning_effort rejection 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.

  3. SamSpiri commented on Oct 2, 2026

    @SamSpiri

    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

    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.

    Updated plugin: https://github.com/SamSpiri/opencode-go-stub-400-retry

  4. JohnnyHolmes commented on Oct 21, 2026

    @JohnnyHolmes

    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

    • Claude Code 2.1.306, Linux
    • ANTHROPIC_BASE_URL=https://opencode.ai/zen/go/
    • model deepseek-v4.1-flash
    • request shape: 89 messages, 12 tools, ~268 KB body, thinking: adaptive, max_tokens: 32000
    • context at failure: 86k–160k tokens, well below the 1M catalog figure
    • beta headers: claude-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-31

    New measurements

    • Real session: 18 × 400 / 184 requests (9.8%) in one day; errors at 10:32–12:41 UTC, contexts 86k–160k.
    • Controlled replay: same valid body, 30 requests, 1 s apart, 2026-10-21 13:50–13:56 UTC → 9 × 400 (30%), 20 × 200, 1 × 60 s connection drop.

    Status sequence:

    200 200 200 200 400 400 400 200 200 200 200 200 200 000
    200 200 200 400 400 400 200 200 200 400 200 200 200 200 400 400
    
    • Six failure windows; four contained consecutive 400s; window duration 3.7–8.9 s; gaps between windows 5.8–95.6 s; longest clean run 6 requests.
    • 200 latency: median 11.4 s, p90 22.0 s. 400 latency: median 3.5 s (faster than success, so not a timeout).
    • Edge POPs SEA/SAN/PDX all showed the stub; 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-After for this class.

    Gateway log IDs (all valid requests, 400):

    01a15eb9-f630-7363-8e73-3e4b511c40e6  13:50:05
    01a15eba-03da-7e56-8f43-69ecc2554222  13:50:10
    01a15eba-1864-757d-9b42-70df47ea7eff  13:50:14
    01a15ebc-b11c-7754-9cbe-e5ebd3da96f5  13:53:03
    01a15ebc-c1fe-7fee-b355-ae51a5059601  13:53:09
    01a15ebc-d1a2-7b84-9f06-417502f5f97c  13:53:13
    01a15ebd-b2b4-7be9-a6da-2590e4e05074  13:54:09
    01a15ebe-f08d-7da8-b90b-0364565c8017  13:55:32
    01a15ebf-06f7-7456-9360-8926f6d1937f  13:55:36
    

    Earlier failures from the same client:

    01a15e7a-f7dc-7606-8c26-3e3be33f20f5
    01a15e7a-ffdd-7d83-9d0b-66386b54d1f1
    

    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:

    1. Return a structured error (type, message, param) for real validation failures, instead of the 31-byte model echo.
    2. Return 429/503 + Retry-After for the transient class, so clients can back off instead of treating it as a terminal 400.
    3. Check the log IDs above for which backend/validation path produced the stub.
  5. opencodejr commented on Oct 28, 2026

    @opencodejr

    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 on opencode-go/deepseek-v4.1-flash in long sessions, the same provider.invalid-request handling that ends the turn, and it already notes that the same step can succeed on a retry with no changes (traced there to the novita-deepseek upstream route).

    Possible duplicates and closely related issues, ordered by likelihood:

    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.

  6. opencodejr commented on Oct 29, 2026

    @opencodejr

    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 on opencode-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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions