Description
When a provider stops a turn because it hit the output-token limit, the stream reports finishReason: "length". The session loop treats that as a normal stop.
Two checks in packages/opencode/src/session/prompt.ts exclude only "tool-calls" (and "unknown" in the second) — never "length":
:1113 — the entry gate that decides whether a session already has work to do
:1295 — the in-loop decision after a provider turn
Nothing anywhere in src branches on "length". The message is stored with finish: "length" and no error, so downstream sees a turn that ended cleanly.
The worst case is a turn that produced nothing usable. A subagent on an OpenAI-compatible gateway spent its entire output budget inside the reasoning channel — the stored parts were step-start, reasoning (129,961 characters), step-finish, with no text and no tool call, and finish: "length". The task completed with status ok and empty output. From the caller's side that is indistinguishable from a subagent that had nothing to say.
MessageOutputLengthError already exists for this case and has no producer:
packages/schema/src/v1/session.ts:36
packages/core/src/v1/session.ts:48
packages/opencode/src/session/message-error.ts:4
It is already a member of the assistant error union, and packages/opencode/src/acp/service.ts:842 already maps it to stopReason: "max_tokens". The plumbing was built for this and never connected to a source.
Steps to reproduce
- Use a model whose output limit is reachable in one turn (the 32K
OUTPUT_TOKEN_MAX clamp in packages/opencode/src/provider/transform.ts:18 makes this common).
- Ask for output large enough to hit the limit — a large file write, or a long reasoning chain on a model that bills reasoning inside the output count.
- Observe the session end. The stored assistant message has
finish: "length" and no error; a task dispatch reports ok.
Reproducible in a test by queueing a reply with finishReason: "length" against the test LLM server.
OpenCode version
dev @ 1882c33
Expected
A truncated turn should be distinguishable from a completed one. A turn that produced text or tool calls before truncating is worth continuing; a turn that produced nothing usable should surface an error rather than reporting success with empty output.
Notes
This is item 4 in the root-cause chain of #18108 (P1 in that issue's checklist), filed here on its own because #18108 covers five interacting problems and this one is separable — it needs no change to repairToolCall, the thinking-budget coordination, the doom-loop detector, or OUTPUT_TOKEN_MAX.
Related: #38747 covers the same problem class on the V2 side (streams that end before their protocol completion boundary). #26167 touches the neighbouring code in prompt.ts for empty-stream truncation retries.
I have a fix with tests and will link it here.
Description
When a provider stops a turn because it hit the output-token limit, the stream reports
finishReason: "length". The session loop treats that as a normal stop.Two checks in
packages/opencode/src/session/prompt.tsexclude only"tool-calls"(and"unknown"in the second) — never"length"::1113— the entry gate that decides whether a session already has work to do:1295— the in-loop decision after a provider turnNothing anywhere in
srcbranches on"length". The message is stored withfinish: "length"and no error, so downstream sees a turn that ended cleanly.The worst case is a turn that produced nothing usable. A subagent on an OpenAI-compatible gateway spent its entire output budget inside the reasoning channel — the stored parts were
step-start,reasoning(129,961 characters),step-finish, with no text and no tool call, andfinish: "length". The task completed with statusokand empty output. From the caller's side that is indistinguishable from a subagent that had nothing to say.MessageOutputLengthErroralready exists for this case and has no producer:packages/schema/src/v1/session.ts:36packages/core/src/v1/session.ts:48packages/opencode/src/session/message-error.ts:4It is already a member of the assistant error union, and
packages/opencode/src/acp/service.ts:842already maps it tostopReason: "max_tokens". The plumbing was built for this and never connected to a source.Steps to reproduce
OUTPUT_TOKEN_MAXclamp inpackages/opencode/src/provider/transform.ts:18makes this common).finish: "length"and no error; ataskdispatch reportsok.Reproducible in a test by queueing a reply with
finishReason: "length"against the test LLM server.OpenCode version
dev @ 1882c33
Expected
A truncated turn should be distinguishable from a completed one. A turn that produced text or tool calls before truncating is worth continuing; a turn that produced nothing usable should surface an error rather than reporting success with empty output.
Notes
This is item 4 in the root-cause chain of #18108 (P1 in that issue's checklist), filed here on its own because #18108 covers five interacting problems and this one is separable — it needs no change to
repairToolCall, the thinking-budget coordination, the doom-loop detector, orOUTPUT_TOKEN_MAX.Related: #38747 covers the same problem class on the V2 side (streams that end before their protocol completion boundary). #26167 touches the neighbouring code in
prompt.tsfor empty-stream truncation retries.I have a fix with tests and will link it here.