Repository navigation
Description
Summary
With 1M-context models (e.g. deepseek-v4.1-flash, qwen3.8-flash, glm-5.3-flash), automatic
compaction effectively never fires: the trigger ceiling is contextLimit - max(min(outputLimit, 32000), buffer)
= 1000000 - 32000 = 968,000 tokens. The provider starts rejecting the request with HTTP 400 at
about 940k tokens, i.e. before the ceiling. Worse, that 400 is not classified as a context overflow,
so the documented overflow recovery (model call -> overflow -> compact -> retry once) never runs and
the session is stuck.
Environment
- opencode version: 2.0.12 (desktop app
@opencode-aidesktop, channellatest) - OS: Windows_NT 10.0.19041 (win32 x64)
- Terminal: desktop app (Electron);
TERM=xterm-256color,TERM_PROGRAMunset - Shell:
C:\Windows\system32\cmd.exe - Install/channel: desktop app / latest (auto-update)
- Active plugins: none (config has one MCP server, no plugins)
Reproduction
- Select a model advertising a 1,000,000-token context, e.g.
opencode-go/deepseek-v4.1-flash
(also reproducible withqwen3.8-flashandglm-5.3-flash, and with the
alibaba-token-plan-cnprovider). - Work in a single long session until the model's reported cache/input usage reaches roughly
900k–940k tokens. - Send another message.
Expected Behavior
Automatic compaction should create a checkpoint before the request exceeds the provider's real
usable limit. At minimum, when the provider rejects the request as too large, OpenCode should
recognize a context overflow and perform the single automatic compact-and-retry described in the
compaction docs.
Actual Behavior
No automatic compaction occurs while the session grows from ~140k to ~940k tokens. At ~941k input
tokens the provider repeatedly returns HTTP 400 and the run fails. The stored assistant message is a
generic error, not the recognizable context-overflow error:
{"finish":"error","error":{"type":"provider.invalid-request",
"message":"Provider request failed with HTTP 400","status":400}}Compare with the intended friendly message produced by the overflow classifier:
Context overflow: <...>. Please start a new session or use /compact to reduce context.
Evidence
Defaults extracted from the opencode-cli 2.0.12 embedded bundle:
- compaction buffer:
buffer = 20000(h$) - retained recent tokens:
keep.tokens = 15000(A$) - output reserve cap:
32000(x$) - ceiling:
tokens >= min(inputLimit - buffer, contextLimit - max(min(outputLimit, 32000), buffer)) isContextOverflowErrormatches only (case-insensitive)context overflow,
prompt is too long,prompt too long, or (tokensANDmaximum).
Verified against two existing sessions:
| Model | catalog context | computed ceiling | observed behavior |
|---|---|---|---|
opencode/big-pickle |
200,000 (input 160,000) | 160000 − 20000 = 140,000 | all 13 historical compactions fired at 140k–143k |
opencode-go/deepseek-v4.1-flash |
1,000,000 | 1000000 − 32000 = 968,000 | grew to 941,114 / 861,534 with zero compactions |
The big-pickle column is exact agreement between the formula and 13 real compaction events, which
isolates the 1M-model ceiling as the regression source.
The session that reached 941k then produced the generic HTTP 400 above; the classifier did not match,
so no overflow recovery was attempted. (Assistant reasoning in that session even noted
"The user keeps getting HTTP 400 — context is very large.")
Additional Context
Suggested fixes, in priority order:
- Overflow classification gap (bug). The provider response body is not surfaced into the error
message, soisContextOverflowErrorcannot match and the documented compact-and-retry recovery is
skipped. Propagating the providerresponseBodyinto the classifier, and/or treating a generic 400
at very high context usage as a possible overflow, would restore recovery. - Default ceiling for large-context models. Hard-capping the output reserve at 32,000 while a
model advertises a 384,000 output limit, combined with a fixed 20,000 buffer, yields a ceiling at
96.8% of context for 1M models — too close to the real limit. Scaling the reserve with context (or
not capping the output reserve) would help. - Catalog vs. provider reality. models.dev advertises 1,000,000 context for these models, but the
gateway accepted 861k and rejected 941k. Treating the catalog value as a hard budget is optimistic;
a configurable safety factor would help.
Workaround that resolves it locally: override the model's context budget per model so the ceiling
lands lower, e.g.
{
"providers": {
"opencode-go": {
"models": {
"deepseek-v4.1-flash": { "limit": { "context": 800000, "output": 384000 } },
"qwen3.8-flash": { "limit": { "context": 800000, "output": 131072 } },
"glm-5.3-flash": { "limit": { "context": 800000, "output": 131072 } }
}
}
}
}With context = 800000 the ceiling becomes 800000 - 32000 = 768000, which fires well before the
provider rejects. Confirmed to load in 2.0.12 (the resolved model reports limit.context = 800000).
Workaround for already-stuck sessions: manual /compact, or
POST /api/session/<sessionID>/compact, as documented on the compaction page.
Appendix A — Session measurement raw data
Sessions live in ~/.local/share/opencode/opencode.db (session_message table; each row's data
is JSON). "usage" = input + cache.read + cache.write + output + reasoning of the latest assistant
message with usage, which is exactly the quantity the compaction estimator starts from.
Session A (ses_f5823c8a0ffeWgJItubT2KzhMN) — compaction events (all reason=auto, all status=completed):
seq=51 2026-09-16 02:34:30 model=big-pickle usage before=142,467
seq=139 2026-09-16 03:03:01 model=big-pickle usage before=150,033
seq=263 2026-09-16 07:16:42 model=big-pickle usage before=140,411
seq=402 2026-09-16 07:52:18 model=big-pickle usage before=140,071
(no further compaction for the next 6 days)
Model/usage progression after the last compaction (note the model switch to 1M-context models):
seq 404 09-16 07:53 usage=43,440 model=big-pickle
seq 445 09-16 08:07 usage=117,255 model=glm-5.3-flash <- 1M context from here on
seq 484 09-16 08:22 usage=150,274 model=glm-5.3-flash (already past the old 140k ceiling)
seq 691 09-17 03:33 usage=264,492 model=qwen3.8-flash
seq 1353 09-21 05:17 usage=767,706 model=qwen3.8-flash
seq 2062 09-21 09:01 usage=812,593 model=qwen3.8-flash
seq 2852 09-22 01:39 usage=813,047 model=deepseek-v4.1-flash
seq 4147 09-22 08:19 usage=941,272 model=deepseek-v4.1-flash <- last success
seq 4156 09-22 08:19 ERROR: provider HTTP 400
Session B (ses_0491f1787ffeH4whjLI26XNfDH) — same pattern:
9 compactions while on big-pickle, last one 2026-09-18 03:25:29 at usage 140,447
then switched to qwen3.8-flash / deepseek-v4.1-flash (1M), grew to 861,534 with no compaction
Reproduce the measurement (Python 3, read-only):
import sqlite3, os, json
db = os.path.expanduser("~/.local/share/opencode/opencode.db")
con = sqlite3.connect("file:%s?mode=ro" % db.replace("\\", "/"), uri=True)
con.row_factory = sqlite3.Row
sid = "ses_f5823c8a0ffeWgJItubT2KzhMN"
for r in con.execute("SELECT seq,type,time_created,data FROM session_message WHERE session_id=? ORDER BY seq", (sid,)):
d = json.loads(r["data"])
if r["type"] == "compaction":
print("COMPACT", r["seq"], d.get("reason"), d.get("status"))
if r["type"] == "assistant" and d.get("tokens"):
t = d["tokens"]
total = (t.get("input", 0) + t.get("cache", {}).get("read", 0)
+ t.get("cache", {}).get("write", 0) + t.get("output", 0) + t.get("reasoning", 0))
print("usage", total, d.get("model", {}).get("id"))Appendix B — Server log lines for the failures
~/.local/share/opencode/log/opencode.log (run id redacted), repeated every ~1–3 minutes from
2026-09-22 08:05 to 08:19 UTC:
level=ERROR message="Failed to drain Session" cause="AI.Error: Provider request failed with HTTP 400
at SessionStep.attempt (...)
[cause]: AI.Error.InvalidRequest: Provider request failed with HTTP 400" sessionID=ses_f5823c8a0ffeWgJItubT2KzhMN
The response body is not included in the logged error, which is why the classifier cannot match it.
Appendix C — Decompiled snippets from opencode-cli 2.0.12
Defaults and ceiling (h$=buffer, A$=keep.tokens, x$=output cap):
var h$ = 20000, A$ = 15000, x$ = 32000
N = (T) => {
let I = i.get(); // { auto, buffer, tokens, native }
if (!I.auto) return false;
let P = T.messages.at(-1);
if (P?.type === "compaction" && P.status === "completed") return false;
if (T.messages.findLastIndex(K5) < T.messages.findLastIndex(By)) return false;
let U = T.resolved.limit, Y = U.context;
if (Y <= 0) return false;
let te = Math.min(U.output, x$); // output reserve capped at 32000
let ee = Math.min(
U.input === undefined ? Number.POSITIVE_INFINITY : U.input - I.buffer,
Y - Math.max(te, I.buffer)
);
return G$(T) >= ee;
}Overflow classifier (isContextOverflowError):
isContextOverflowError() {
if (this.statusCode !== 400) return false;
let t = this.message?.toLowerCase() || "";
return t.includes("context overflow") || t.includes("prompt is too long")
|| t.includes("prompt too long") || (t.includes("tokens") && t.includes("maximum"));
}Generic-message fallback that drops the provider body (name-mangled pn):
pn = (e, o) => {
let a = o === undefined ? undefined : Decode(dn(o));
return [a?.error?.message, a?.message].find((s) => s?.trim())
?? `Provider request failed with HTTP ${e}`;
}Config schema confirms the per-model workaround exists
(Config.Model.Limit = { context?: number, input?: number, output?: number }, nested under
providers.<id>.models.<model>.limit).
Appendix D — Config workaround applied
~/.config/opencode/opencode.jsonc (verified via the running server's /api/model; resolved
limit.context is now 800000 for these models while big-pickle is untouched at 200000/160000/32000):
{
"$schema": "https://opencode.ai/config.json",
"providers": {
"opencode-go": {
"models": {
"deepseek-v4.1-flash": { "limit": { "context": 800000, "output": 384000 } },
"qwen3.8-flash": { "limit": { "context": 800000, "output": 131072 } },
"glm-5.3-flash": { "limit": { "context": 800000, "output": 131072 } }
}
},
"alibaba-token-plan-cn": {
"models": {
"deepseek-v4.1-flash": { "limit": { "context": 800000, "output": 384000 } },
"qwen3.8-flash": { "limit": { "context": 800000, "output": 131072 } }
}
},
"mcp": {
"chrome-devtools": { "type": "local", "command": ["npx", "-y", "chrome-devtools-mcp@latest"] }
}
}
}
Activity
github-actions commentedon Sep 22, 2026
This issue might be a duplicate of existing issues. Please check:
opencode-go/qwen3.8-flashwith 1M catalog context, gateway rejects at ~148k tokens, HTTP 400 not classified as context overflow, session permanently bricked. Same root cause: catalog limit vs. real gateway limit mismatch + overflow classifier gap.isContextOverflowErrorpatterns, so the one-shot compact-and-retry recovery never engages and the session dead-ends.The two bugs you've identified (overflow classification gap + ceiling too close to real provider limit for 1M-context models) are covered by those reports. Your issue adds useful detail on the decompiled ceiling formula and the systematic session data, so it may still be worth keeping open as a tracking issue — but please cross-reference the above.
Kouskous56 commentedon Oct 2, 2026
Adds a measurement that changes the diagnosis: the preflight ceiling is not merely close to the limit, it is unreachable on token-dense payloads, because the signal being compared against it under-counts by up to ~3.2x.
Token.estimate(packages/core/src/util/token.ts) is a fixedchars / 4. Measured against this provider's own reportedinputcounts on the same endpoint (muse-spark-1.3-contributor-free):chars/4A fixed divisor is wrong at both ends. At the observed failure boundary the estimate is ~400,000 while
context - max(output, buffer)=1,048,576 - 131,072= 917,504, socompactIfNeededreturns false by construction — no tuning of thebufferdefault can make preflight compaction fire. (Worth notingbuffer: 20000is entirely absorbed bymax(output, buffer)on this model, since it advertisesoutput: 131072.)Bracket from the same runs (single hex message,
compaction.auto: false, 2.0.21):invalid_request_errorinvalid_request_errorA real-world instance of the same thing: a 467-message agent session with 291 tool calls and 2,427,013 chars of stored conversation text reached 94% of the catalog window (opencode's own compaction record reports 984,313 input tokens) with zero compaction events. It only compacted after a model switch moved the ceiling from 917,504 down to 872,000 — the estimate happened to land in that ~45k window.
This suggests the fix belongs in the estimator as much as in the reserve: either tokenize for the preflight check, or derive density from the provider's last reported
inputfor the session, so the ceiling is expressed in the same unit the limit is actually enforced in.Related: #46564 covers the opaque 400 not being classified as overflow.
Suixinlei commentedon Oct 3, 2026
Additional concrete reproduction of this issue:
aec0b9a6d8898f68f923aaf08b7306d931fd9d76)glm-5.3-flash{"error":{"code":"1261","message":"Prompt exceeds max length"}}The current classifier does not match this message, so the response is treated as a generic provider error and the context-overflow recovery path is skipped. This is the provider-specific case tracked in #52961. A focused fix would recognize
Prompt exceeds max(imum) lengthwhile excludingmax_tokenserrors and rate limits.opencodejr commentedon Oct 28, 2026
Thanks for the very detailed report. I couldn't reproduce this here: it depends on the exact error body the OpenCode Go gateway sends with its HTTP 400 at ~940k tokens, and that needs a real provider account.
One correction to the analysis: even in 2.0.12, OpenCode checks the raw response body for overflow wording, not just the short message (
classifyProviderFailureinpackages/ai/src/provider-error.tsscans both). So the 400 wasn't treated as an overflow because the gateway's body didn't contain any of the overflow phrases or error codes OpenCode recognises (or it was empty). 2.0.12 only left the body out of the message you saw.Newer releases add the provider's response body to the error message (
Provider request failed with HTTP 400: <body>) and recognise more overflow messages, including one from OpenCode Go. It would help a lot if you could:With that, a maintainer can tell whether this is an overflow message OpenCode should recognise. A maintainer with OpenCode Go access may also look at it directly.
About the 968k threshold for 1M-context models: that is how the compaction formula is designed to work. Making the safety margin larger or configurable for large-context models would be a feature request. Until then, your
limit.contextoverride and manual/compactare the right workarounds.