Repository navigation
Description
Summary
A session on opencode-go/qwen3.8-flash became permanently bricked by HTTP 400 responses: the Go gateway rejects requests whose prompt exceeds roughly 148k input tokens, while the model catalog advertises a 1,000,000-token context — so automatic compaction never fires, the 400 is not classified as context overflow (no one-shot compact-and-retry recovery), and every subsequent prompt re-sends the over-limit history and fails identically. The provider's 400 response body is not persisted anywhere, which makes the failure undiagnosable from the client side.
Environment
- opencode version: 2.0.12
- OS: Darwin 27.0.0 (macOS, arm64)
- Terminal: tmux (
TERM_PROGRAM=tmux,TERM=xterm-256color) - Shell: /bin/zsh
- Install/channel: global install (
@opencode/cli),channel=latest - Active plugins: none (
"plugin": []in global config, no project plugins) - Provider/model:
opencode-go/qwen3.8-flash, protocolanthropic-messages, endpointhttps://opencode.ai/zen/go/v1
Reproduction
Observed organically (a long session with large tool payloads grew past the gateway's real ceiling), but the mechanism is deterministic:
- Use
opencode-go/qwen3.8-flashwith default compaction settings (compaction.auto: true,buffer: 20000, output reserve capped at 32k). - Grow the session's prompt input until it exceeds the gateway's real accept limit (~147–148k tokens, see numbers below) — e.g. keep a normal working session going; the last successful step here used 146,430 prompt tokens (
input: 6+cache.read: 144,122+cache.write: 2,302), the very next step (~148.3k, history plus ~1.8k of new tool calls and results) failed. - Observe
HTTP 400on that step and on every retry: new user prompts keep failing with the identical error because the stored history is permanently over the gateway's limit.
Server-side evidence: Failed to drain Session in opencode.log, cause AI.Error.InvalidRequest: Provider request failed with HTTP 400 (4 identical failures over ~3 minutes; no session.compaction.* events, no retry with compaction). Stored as {"type":"provider.invalid-request","message":"Provider request failed with HTTP 400","status":400} — no provider body.
Expected Behavior
One of (in order of preference):
- The catalog limit for
opencode-go/qwen3.8-flashmatches what the gateway actually accepts, so preflight auto-compaction triggers well before the hard wall. Per the compaction docs the ceiling ismin(input limit − buffer, context limit − max(output reserve, buffer))≈ 968k with the current{"context": 1000000, "output": 131072}— unreachable for a gateway that stops at ~148k. - Or the 400 is classified as a context overflow, so the documented one-shot
model call → context overflow → compact → retryrecovery engages. - Or at minimum the provider's 400 response body is surfaced to the user and persisted (log + session message), since it presumably contains the real rejection reason.
Actual Behavior
- The session is unrecoverable in place: preflight never compacts (catalog says 1M), the overflow recovery never engages (error not recognized as overflow), and manual
/compactis expected to fail the same way because the summarization request carries the same oversized history (cf. Manual /compact sends the full history to the compaction model without a context-fit check #46164). - The user sees only "Provider request failed with HTTP 400" on every message; the only way out is abandoning the session.
Additional Context
Current model definition returned by the running server (GET /api/model, provider section trimmed):
{"id": "qwen3.8-flash", "providerID": "opencode-go",
"package": "@opencode/ai/providers/anthropic",
"settings": {"baseURL": "https://opencode.ai/zen/go/v1"},
"limit": {"context": 1000000, "output": 131072}}- So the gateway's true ceiling is somewhere in
(146,430 … ~148,500]prompt tokens — consistent with a hidden per-plan input cap rather than the model's advertised window. If the Go plan intentionally caps inputs lower than theopencode(inference) provider, the per-provider limit entry needs to reflect that. - The misclassification angle ("provider error is an overflow but not recognized as one") mirrors Image-count 400s (Azure: "Exceeded maximum number of images (50)") are not classified as context overflow, so media-strip compaction never triggers #39677 (Azure image-count 400s) — here with a plain text-prompt 400 from
zen/go/v1. - Frequency: deterministic; every request in that session failed for the same reason until abandoned. No workaround found from the client besides starting a new session.
- Session ID for server log lookup:
ses_f3a278574ffeXanrj4LBuzq4FK; failures at 2026-09-21T21:44:48Z, 21:45:17Z, 21:47:05Z, 21:47:35Z.
Activity
opatry commentedon Sep 21, 2026
FTR, issue analyzed by OpenCode itself using same model setup and bug report made by OpenCode itself as well.
I have low skill in that area, so felt more accurate to rely on AI analysis rather than mine.
kaantokgozsec-hash commentedon Sep 30, 2026
Confirming this class of failure on
opencode-go/deepseek-v4.1-flash, with numbers that may help pin down the real limit. Reporter: Kaan Tokgoz (@kaantokgozsec-hash).What happened. A long working session became permanently bricked. Every request after that point, including plain text like
hi, returns:{"type":"provider.invalid-request","message":"Provider request failed with HTTP 400","status":400}The log only records
Failed to drain Sessionwith the same generic message, atlevel=INFO. The provider body is never persisted, so the actual reason is undiagnosable from the client. The failure is deterministic from that point on.Environment. opencode desktop 2.0.18.0, Windows 11 Pro (10.0.26200),
opencode-go/deepseek-v4.1-flash(build agent). No plugins.Numbers.
opencode-go/deepseek-v4.1-flash:limit.context = 1000000,limit.output = 384000,modalities.input = [text, image].tokens = {"input": 257, "cache": {"read": 375040}}→ prompt ≈ 375K tokens. The very next step failed. So the rejection happens well below the advertised 1,000,000, and well below the ~940K figure cited earlier in this thread for the qwen route.readresults retained images as fulldata:image/png;base64,...URIs withtruncated: false, never evicted or downscaled. The largest is a single 1,065,334-character URI (from a 799 KB PNG).session.compaction.*events anywhere in the session. No compact-and-retry. No overflow classification.Two candidate triggers I cannot separate from the client side, because the body is dropped:
Why it never recovers. The oversized history is replayed on every subsequent request, and the 400 is not classified as context overflow, so the documented one-shot compact-and-retry never runs. Result: a permanently unusable session with a misleading error.
Possible directions. Surface the provider body (related: #52145), and/or classify these as context overflow so compaction/retry can run. A cap or downscale on image data retained in tool results would also prevent sessions from inflating this way in the first place.
I can provide the session id, the cache-read growth table, and the image inventory on request. Happy to test a candidate fix against this session if useful.
aanbrn commentedon Oct 28, 2026
Related case, plus one thing that changed since this was filed.
I am reporting a 400 of the same shape from a different model and a different trigger — the turn ends, with my
client classifying it
provider.invalid-request— so this is not a claim that your defect and mine are one:opencode-go/deepseek-v4.1-flashover/zen/go/v1/chat/completions, where the whole response body was the 31-byte{"model":"deepseek-v4.1-flash"}. On 2.0.18 it surfaced intermittently in long sessions (my report, #51201, isbeing closed as a duplicate of #50761); the failures I recorded sat at ~350–535k prompt tokens, well above the ~148k
input this issue reports, so treat the two as separate reports sharing a failure mode rather than as one incident.
The part that changed. SamSpiri reports in #51201 that on 2.0.21 the provider body is now persisted, at
error.response.body, and the message text becomesProvider request failed with HTTP 400: {"model":"deepseek-v4.1-flash"}. This updates the relateddeepseek-v4.1-flashcase; it does not establish that body surfacing has changed for theqwen3.8-flashcase reportedhere.
What persists is the body, not a recovery. The 400 still ends the turn, and the body does not say what kind of
rejection it is: the identical 31-byte stub also appears in #50761 for requests above one route's limit (~612k and
~970k prompt tokens), where compacting and retrying is the response that makes sense — so recognising the signature is
not the same as choosing a recovery. Distinguishing a transient rejection from a size rejection still needs a
classification the client does not perform.
Flagging it here because this issue's title — "400 unrecoverable, body never surfaced" — is the open description of
that client-side half. For the related
deepseek-v4.1-flashcase, body persistence is reported on 2.0.21. That alonedoes not establish automatic recovery, and I have not verified either behavior on
qwen3.8-flash.opencodejr commentedon Oct 28, 2026
Thanks for the detailed report. I couldn't reproduce this here: the main problem depends on how the real OpenCode Go gateway handles
qwen3.8-flash, and this sandbox has no provider account.From reading the code (not running it):
error.message/message). Any other body was dropped, which leaves justProvider request failed with HTTP 400. On the latestdev, a plain-text body is now shown in the error. An empty or HTML body still gives the bare message.context_length_exceeded). A bare 400 doesn't match, so compaction and retry never run.What's needed is someone with Go access to check the gateway's real input limit for this model against the catalog's 1M context, and what the gateway actually sends with that 400. I've flagged that for the maintainers.
In the meantime, a possible workaround is to set a lower context limit for this model in your config, so automatic compaction starts well before ~148k. I haven't tested this.
aanbrn commentedon Oct 28, 2026
Thanks for the code-level reading — noted as yours, not as something I verified. It supplies the mechanism behind
what my comment above said from the other side: the body a bare 400 persists still does not tell a reader which kind
of rejection arrived.
The one thing that would help most is a signal the client can act on: a reliably distinguishable, ideally
machine-readable, marker for a confirmed overflow — so that compaction and retry run on that and only on that. Not a
retry for every bare 400; I would not want the other rejections read as overflow either.
On the workaround: I would not derive a context limit for a model from a ~148k failure point. If one does get tested,
it would be against real failures on the affected model, with the setting confirmed applied.