Repository navigation
provider: opencode-go route-dependent context limits cause unparseable HTTP 400 in long sessions #50761
Description
Summary
opencode-go/deepseek-v4.1-flash is served by at least two upstream providers behind the Go gateway
with different context limits, so long sessions fail intermittently with HTTP 400 depending on
which upstream a request is routed to. The failing route returns a 31-byte body
({"model":"deepseek-v4.1-flash"}) with no message, so the client can only surface
Provider request failed with HTTP 400, and because that 4xx is not classified as a context overflow,
the documented compact-and-retry recovery never runs and the session keeps failing.
Related: #50446 (same class of failure on qwen3.8-flash, deterministic there), #50574 (compaction
ceiling and overflow classification).
Environment
- opencode version: 2.0.14 (desktop app; background server
opencode-cli serve2.0.14) - OS: Darwin 25.6.0 (macOS, arm64)
- Terminal: desktop app (Electron);
TERM=xterm-256color,TERM_PROGRAMunset - Shell: /bin/zsh
- Install/channel: desktop app, channel
latest - Active plugins: local plugins only (
normalize-eol.js,protected-mobile-guard.jsunder
~/.config/opencode/plugins/); unrelated to this issue - Provider/model:
opencode-go/deepseek-v4.1-flash, endpointhttps://opencode.ai/zen/go/v1,
protocolopenai-compatible
Reproduction
Observed organically in a long session, then reduced to direct gateway requests.
Direct (no OpenCode involved): send the same request shape to
POST https://opencode.ai/zen/go/v1/chat/completions with a stable x-opencode-session header and
read x-opencode-endpoint-id from the response.
{
"model": "deepseek-v4.1-flash",
"messages": [
{ "role": "system", "content": "You are a coding agent harness request. Ignore filler." },
{ "role": "user", "content": "Only reply with the single word: ok\n\n<filler to reach N tokens>" }
],
"max_tokens": 16
}Measured results (filler text, same shape for every row):
route (x-opencode-endpoint-id) |
upstream model id | prompt tokens | result |
|---|---|---|---|
deepseek |
deepseek-flash |
969,752 | 200 |
deepseek |
deepseek-flash |
1,163,547 | 400 with a full message (see below) |
novita-deepseek |
deepseek/deepseek-v4.1-flash |
323,100 / 409,358 / 517,290 / 538,578 / 582,016 | 200 |
novita-deepseek |
deepseek/deepseek-v4.1-flash |
~612,000 / 969,752 | 400, body: {"model":"deepseek-v4.1-flash"} (31 bytes) |
The deepseek route reports its real ceiling itself:
{"error":{"param":null,"type":"invalid_request_error","code":"invalid_request_error","message":"Upstream request failed: [invalid_request_error] This model's maximum context length is 1048576 tokens. However, you requested 1163563 tokens (1163547 in the messages, 16 in the completion). Please reduce the length of the messages or completion."}}The novita-deepseek route rejects somewhere between 582,016 and ~612,000 tokens and returns only:
{"model":"deepseek-v4.1-flash"}Example failing request header: x-opencode-endpoint-id: novita-deepseek,
x-opencode-log-id: d924591e-61a3-4073-bfcf-d6843328250b.
Routing appears session-pinned (x-session-affinity): one session id consistently resolved to
novita-deepseek across small/300k/400k/500k/525k/550k/600k/950k requests, while two other session
ids consistently resolved to deepseek.
In the affected OpenCode session:
- Use
opencode-go/deepseek-v4.1-flashwith default compaction settings. - Keep working until the session's prompt input reaches roughly 590k tokens and beyond (this session
reached ~893k–900k tokens; the model catalog advertises a 1,000,000-token context, so the
preflight compaction ceiling is 968,000 and never fires). - Continue the session. Steps alternate between success and failure: success when a request is
routed todeepseek, the generic HTTP 400 when it is routed tonovita-deepseek. - "Continue from where you left" sometimes succeeds (different route) and sometimes fails again, so
the session makes slow, unreliable progress rather than being fully bricked.
Expected Behavior
One of:
- The gateway accepts for a given model whatever the catalog advertises (1,000,000 tokens here), or
the catalog/limits reflect the smallest limit among the upstreams a request can be routed to. - When an upstream rejects a request, its response body is surfaced so the client can classify the
failure and run the documentedoverflow -> compact -> retry oncerecovery.
Actual Behavior
-
Requests routed to the smaller-limit upstream fail with HTTP 400 and an unparseable body.
-
The client stores:
{"finish":"error","error":{"type":"provider.invalid-request","message":"Provider request failed with HTTP 400","status":400}} -
No
classification:"context-overflow"is produced (the body has no recognizable overflow text),
so no compaction/retry recovery is attempted (time_compactingstays null, no
session.compaction.*events). -
The failure is route-dependent, so the same step can succeed on a retry with no changes.
Additional Context
- Client-side cause of the generic message: the error extractor looks for
error.message/
messagein the parsed body and otherwise falls back toProvider request failed with HTTP ${status}. - Client-side cause of the missing recovery: the 4xx fallback returns a plain
AI.Error.InvalidRequest(provider.invalid-request) when no overflow pattern matches, and the
overflow classifier only treats a 4xx as overflow when the body/message matches overflow patterns
or is empty. - Workaround used: manual
/compacton the affected session (the summary request itself failed once
when it landed on the smaller-limit route, then succeeded on retry). - Filler content in the probes is synthetic; no user data was sent.
Activity
Guillote29 commentedon Sep 25, 2026
very fastidiuos bug
627-hub commentedon Sep 27, 2026
+1 with evidence from the CLI build (the report is on the desktop 2.0.14 build).
Environment
opencode-ai1.18.32 (CLI), macOS arm64, zshopencode-go/deepseek-v4.1-flash, endpointhttps://opencode.ai/zen/go/v1build, modeprimarySame signature
Both sessions returned the identical 31-byte body on the
novita-deepseekroute:Response headers from one failure:
ses_f1cc3346fffeaXY6T9nv1JwKoF: 1 failure (14:59:09Z).ses_f31c32216ffe5FQkd7ZhbRJoTg: 8 failures over ~64 min on2026-09-27(12:56:42Z → 14:00:13Z), then the user aborted.Context sizes at each f31c failure (previous turn's total tokens)
The same session then succeeded at ~494k–505k tokens (14:10–14:14Z), so this is not a pure message-size ceiling — consistent with "messages + requested completion budget" crossing the novita route limit, and/or a fluctuating route limit.
Direct gateway probes
Sending plain-text filler to
/zen/go/v1/chat/completionswithmax_tokens: 16and a stablex-opencode-sessionreturned 200 onnovita-deepseekfor inputs of ~1.2M / 1.8M / 2.4M characters. So a large message list alone does not reproduce it; the real harness request shape (large requested completion budget and/or tool calls + streaming) seems required. Corroborates the "route-dependent context limit" theory in the title.Impact
The 400 comes back as
AI_APICallError: Bad RequestwithisRetryable: false, so the documented compact-and-retry recovery never runs; the session keeps failing until manually aborted.z99733543-cell commentedon Oct 17, 2026
Still reproducible as of 2026-10-17 on OpenCode Desktop 2.0.32 (
opencode-go/deepseek-v4.1-flash#max), now with two distinct route-dependent symptoms. Both wedge the session the same way (full history resent; every request keeps failing until a manualPOST /api/session/{id}/compact, which recovers it):Limits: one session got
400 [unsupported_request] This text request is too large for the model's configured input limit.right after a 270,421-token success (fit estimate ≈276k prompt tokens at rejection), then — after a manual compaction — stalled again right after a 298,182-token success (≈302k). Meanwhile multiple sessions on the same model/day ran successfully at 272k–900k tokens (max observed 899,929; one prompt at 884,448 fully uncached), so the wall clearly depends on the upstream route.Capabilities: on the failing route, any request containing an image/file part is rejected with
[unsupported_request] Message content supports text content only. Images, audio and files are not enabled.— while the catalog advertisescapabilities.input: ["text","image"], and another session on the same model that day ran fine with 6 image attachments in context. So at least some upstreams behind the Go gateway have non-text input disabled.Same recovery gap as the original 31-byte-body case: these 400s aren't classified for compact-and-retry, so the session stays stuck until manual compaction. Sibling report with a mock-reproduced version of that gap: #56296.
aanbrn commentedon Oct 28, 2026
Adding measurements from #51201 — my report, which is being closed as a duplicate of this issue — because my failures
sit below both route ceilings measured here and I did not record which route served them.
What I measured
opencode-go/deepseek-v4.1-flash(opencode 2.0.18, macOS 27.0,/zen/go/v1/chat/completions), 400s arrived at~350–535k prompt tokens. The two routes measured in this issue accept more than that (
novita-deepseek582,016,deepseek969,752), so I cannot tell from my data whether your route explanation covers these. A 534,912-token and a535,040-token request both passed minutes either side of one failure.
successes above 582k are real, and consistent with landing on
deepseek.novita-deepseek's 582,016 acceptance, which is why I am not claiming a ceiling cannot explain some of them.Measurements from another client
/zen/go/v1/messages— a different client over adifferent protocol on the same model — 18 × 400 / 184 requests in one day, contexts 86k–160k tokens, ~268 KB body.
3.7–8.9 s and were separated by 5.8–95.6 s gaps, and the 400s answered faster than the 200s (median 3.5 s vs
11.4 s).
Identical bytes, different verdict
across three consecutive attempts, then 5×200 + 1×400 across a later six-attempt loop.
{"type":"provider.invalid-request","message":"Provider request failed with HTTP 400","status":400}.On retry
3 s and 10 s.
200.
injected rather than organic.
Caveat I want to keep explicit: I have not established a cause. The same bytes produced different results on the
same endpoint, and one failure carried as little as ~350k tokens — well under either route ceiling measured here.
Whether that is a transient rejection, a third route with a lower limit, or something else, my data does not decide.
Full details and the mitigation test are in #51201.
opencodejr commentedon Oct 28, 2026
Thanks for the detailed measurements. I couldn't reproduce this here because it needs a real OpenCode Go account, which this sandbox doesn't have. The failing part is in the Go gateway: the
novita-deepseekroute rejects prompts well below the advertised 1M context and replies with only{"model":"deepseek-v4.1-flash"}. That isn't something we can see from the client code in this repository.On the client side, from reading the code:
Provider request failed with HTTP 400: {"model":"deepseek-v4.1-flash"}(provider: opencode-go 400 stub on long sessions varies per attempt (identical bytes replay 200/400/200) #51201 reports it being saved since 2.0.21).The log id you gave (
d924591e-61a3-4073-bfcf-d6843328250b) should help a maintainer with gateway access trace the failing route. Related issues:qwen3.8-flash, where it happens every timedeepseek-v4.1-flashstub-body failure, closed as a duplicate of this issueaanbrn commentedon Oct 28, 2026
Two corrections to my comment above. Both make the evidence weaker rather than stronger.
{"type":"provider.invalid-request","message":"Provider request failed with HTTP 400","status":400}— was 2.0.15's.It is the 2.0.18 record, reported in provider: opencode-go 400 stub on long sessions varies per attempt (identical bytes replay 200/400/200) #51201. On 2.0.15 the observation is only that the 31-byte body "is dropped
when the error is persisted", with no record quoted.
body". The byte-identical evidence is our replay series — 200/400/200, then 5×200 + 1×400 — not that attempt.
Neither changes what I concluded, but both are what a reader checking the thread will find, so they belong here rather
than left to be discovered.
aanbrn commentedon Oct 28, 2026
Adding gateway trace IDs from JohnnyHolmes's report in #51201 for the 86k–160k-token failures over
/zen/go/v1/messages.From the reported failing burst:
01a15eb9-f630-7363-8e73-3e4b511c40e601a15ebf-06f7-7456-9360-8926f6d1937fTwo earlier ones from the same client:
01a15e7a-f7dc-7606-8c26-3e3be33f20f501a15e7a-ffdd-7d83-9d0b-66386b54d1f1The full burst list — nine IDs — is in #51201. The serving route was not recorded. These are from that report, not
from my byte-identical replay series.
Could these be traced alongside the original
d924591e-61a3-4073-bfcf-d6843328250b? Whether these 86k–160k failureswere themselves context-limit rejections is not established: the model-only body does not distinguish them.