What
opencode requests at most 32 000 output tokens from every model, regardless of
what that model can actually produce. On a reasoning model the same budget covers
reasoning and text, so a verbose thinker can be truncated before it emits
anything at all.
// opencode core
var OUTPUT_TOKEN_MAX = 32e3;
function maxOutputTokens(model, outputTokenMax = OUTPUT_TOKEN_MAX) {
return Math.min(model.limit.output, outputTokenMax) || outputTokenMax;
}
Real limit.output per models.dev: anthropic 128 000, zai 131 072, gemini 65 536.
min(128000, 32000) = 32 000 — we ask for a quarter of what is available.
Evidence
Every finish: "length" assistant message across our session stores sums
output + reasoning to 31 995 … 32 000 — 87 of them, spanning four providers
and eight models. A single cap applied uniformly; not a model property, not a
provider quirk.
Two distinct failure shapes, both silent:
- Reasoning eats the budget. Two
zai-coding-plan/glm-5.3* grunts:
finish=length, reasoning 31 991 / 31 998, output 9 / 2. ~133 KB of real
drafted work stranded in the reasoning channel, no file written. The task
tool reports state="completed" with an empty <task_result>, so the
orchestrator saw only "" and concluded the provider had refused — which
nothing in the session supported.
- A tool call is truncated mid-JSON. Five grunts streaming a large file into
one write call: the arguments were cut at the cap, the call never parsed,
and the part reads tool=write status=error input={} error="Tool execution aborted". Four of the five killed the subagent session outright. That looks
exactly like a manual cancel, which is why it went unnoticed for months.
Of the 87 truncations, 20 ended their session with the work lost.
Why it is not configurable
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX is the only override. It is read as
flags.outputTokenMax through Effect Config from the environment; there is no
key for it in opencode.json (the config schema has no experimental section),
so a project or global config file cannot set it.
Raising provider.<id>.models.<mid>.limit.output does not help either — the
min still clamps to 32 000. The override only works downwards.
Options
- (a) Environment variable, e.g.
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX=131072.
The min is preserved, so one global number still gives every model its own
real ceiling (Claude 128k, GLM 131k, Gemini 65k). No code. Downside: the flag
is marked EXPERIMENTAL, and it is all-or-nothing across models and agents.
- (b) A
chat.params hook in this plugin. That hook receives
maxOutputTokens as a writable output alongside agent and model, so the
cap can be lifted per model or only for grunts, without depending on an
experimental flag.
Cost of fixing it
Both options remove a brake. Today's 32 000 cap is a crude one — it kills good
work along with runaway work — but it is a bound. Lifted, an agent that spirals
burns up to 131 k output tokens before it stops. Worth pairing with the per-agent
steps cap the roster already supports, and with reasoning variants, which
bound the reasoning share directly.
Not this
Suppressing reasoning is not the fix and should not be considered one: on a
reasoning model it trades a truncation bug for a quality regression. Setting a
variant bounds an unbounded reasoner; it does not switch reasoning off.
What
opencode requests at most 32 000 output tokens from every model, regardless of
what that model can actually produce. On a reasoning model the same budget covers
reasoning and text, so a verbose thinker can be truncated before it emits
anything at all.
Real
limit.outputper models.dev: anthropic 128 000, zai 131 072, gemini 65 536.min(128000, 32000)= 32 000 — we ask for a quarter of what is available.Evidence
Every
finish: "length"assistant message across our session stores sumsoutput + reasoningto 31 995 … 32 000 — 87 of them, spanning four providersand eight models. A single cap applied uniformly; not a model property, not a
provider quirk.
Two distinct failure shapes, both silent:
zai-coding-plan/glm-5.3*grunts:finish=length,reasoning31 991 / 31 998,output9 / 2. ~133 KB of realdrafted work stranded in the reasoning channel, no file written. The
tasktool reports
state="completed"with an empty<task_result>, so theorchestrator saw only
""and concluded the provider had refused — whichnothing in the session supported.
one
writecall: the arguments were cut at the cap, the call never parsed,and the part reads
tool=write status=error input={} error="Tool execution aborted". Four of the five killed the subagent session outright. That looksexactly like a manual cancel, which is why it went unnoticed for months.
Of the 87 truncations, 20 ended their session with the work lost.
Why it is not configurable
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAXis the only override. It is read asflags.outputTokenMaxthrough EffectConfigfrom the environment; there is nokey for it in
opencode.json(the config schema has noexperimentalsection),so a project or global config file cannot set it.
Raising
provider.<id>.models.<mid>.limit.outputdoes not help either — theminstill clamps to 32 000. The override only works downwards.Options
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX=131072.The
minis preserved, so one global number still gives every model its ownreal ceiling (Claude 128k, GLM 131k, Gemini 65k). No code. Downside: the flag
is marked EXPERIMENTAL, and it is all-or-nothing across models and agents.
chat.paramshook in this plugin. That hook receivesmaxOutputTokensas a writable output alongsideagentandmodel, so thecap can be lifted per model or only for grunts, without depending on an
experimental flag.
Cost of fixing it
Both options remove a brake. Today's 32 000 cap is a crude one — it kills good
work along with runaway work — but it is a bound. Lifted, an agent that spirals
burns up to 131 k output tokens before it stops. Worth pairing with the per-agent
stepscap the roster already supports, and with reasoningvariants, whichbound the reasoning share directly.
Not this
Suppressing reasoning is not the fix and should not be considered one: on a
reasoning model it trades a truncation bug for a quality regression. Setting a
variantbounds an unbounded reasoner; it does not switch reasoning off.