You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Empty response with finish_reason "length" and zero tokens is treated as success #50949
When a provider returns a well-formed but empty stream (finish_reason: "length", content: "", no usage), opencode ends the turn with no message and no error. The user sees no output and no error, and the run exits successfully. This makes provider-side failures impossible to diagnose from the TUI/CLI.
I hit this with OpenAI GPT-family models via OpenRouter whenever a tool schema contained "enum": []. The bad tool schema is the root trigger (reported separately to the MCP server), but opencode should not treat a degenerate empty response as a successful completion.
Steps to reproduce
Configure an OpenRouter provider and a remote MCP server that exposes a tool with "enum": [] in its parameters.
Minimal: a single tool whose parameter schema is {"type": "string", "enum": []}.
Run:
opencode run -m openrouter/openai/gpt-6-luna "What is the capital of France?"
Observe: no output, no error, exit success.
The same request without the empty enum returns a normal answer.
Expected
Either a normal answer, or an explicit error such as:
Surface an error for empty completions. When a step finishes with finishReason = "length", zero output tokens, and no content, raise a visible error instead of completing successfully. Right now it is indistinguishable from a valid (if terse) answer.
Sanitize outgoing tool schemas. Strip empty enum arrays (and other invalid JSON Schema constructs) before sending tools to a provider. opencode builds the tool list, so it is the right place to defend against malformed MCP schemas. This would have prevented the failure entirely.
There are at least two places where this guard can be placed:
A. Session runner (packages/core/src/session/runner/llm.ts) – here the system will be able to make the decision regarding the usefulness of the turn’s outcome.
B. LLM core (packages/llm/src/) – this would be the most centralized solution, but it would place the decision-making regarding whether the turn was useful on the provider-agnostic layer of the LLM which is more session/agent-loop level policy.
A looks better to me. Prior to implementation, I will investigate the issue of whether reasoning-only responses should be considered as a valid output in #37372.
As to the actual fix, I expect the degenerate response to generate explicit error/failure via proper execution failure path (ideally, with the finish reason (e.g. model returned no content with finish reason: length)) and not session.execution.succeeded. If the fixtures are able to show the degenerate response, then I will consider adding a regression test.
Silent turn-end with reasoning-only finish_reason=length — simpler repro, and a gap in the #49061 approach
Hit on v2.0.23 against an OpenAI-compatible endpoint (corporate LiteLLM in front of self-hosted vLLM vllm-0.26.1rc0), but the repro below needs nothing exotic.
Minimal repro: any model config with limit.output smaller than the model's thinking appetite; reasoning_effort: high; a prompt that induces long thinking. E.g. output cap 2000:
opencode run --format=json --thinking --model=<provider>/<model>#high \
--auto -- "Think through <long derivation>, spend a very long time thinking."
Observed (deterministic): stream yields step_start → one reasoning event (cut mid-thought at the cap) → end of stream. No step_finish, no text, no error event. opencode run exits 0. The turn is silently empty — no retry, no error surfaced to the agent loop or the user.
Wire-level check: the raw SSE from the endpoint is protocol-clean: reasoning_content deltas, then finish_reason: "length", then [DONE]. So this is not a malformed stream — it is the degenerate-shape case: reasoning present, content absent, finish length. (A separate malformed-stream case — a trailing content chunk after finish_reason, rejected by the guard from #40798 — is what PR #49167 targets; that one we have only observed occasionally on deepseek-v4.1-flash.)
Why this matters for #49061: its guard treats a stream as "generated" when any text-delta/reasoning-delta/tool event occurred. Here reasoning deltas do occur, so under that guard this case would still end silently. Suggest the empty-response guard count content/tool deltas only, or additionally treat finish=length with zero content and zero tool calls as retryable. Happy to provide full trajectory JSON if useful.
Impact in benchmark harnesses: under Harbor / Terminal-Bench this records reward 0 with no exception, so score loss is invisible unless one digs into the trajectory. Observed repeatedly with high-reasoning models on hard agentic tasks (e.g. GLM-5.3-Flash emitting ~110k-char thinking blocks that exhausted a 32768 max_completion_tokens budget) before the output cap was raised past the thinking appetite.
Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.
Activity
Yash-912 commentedon Sep 29, 2026
There are at least two places where this guard can be placed:
A. Session runner (packages/core/src/session/runner/llm.ts) – here the system will be able to make the decision regarding the usefulness of the turn’s outcome.
B. LLM core (packages/llm/src/) – this would be the most centralized solution, but it would place the decision-making regarding whether the turn was useful on the provider-agnostic layer of the LLM which is more session/agent-loop level policy.
A looks better to me. Prior to implementation, I will investigate the issue of whether reasoning-only responses should be considered as a valid output in #37372.
As to the actual fix, I expect the degenerate response to generate explicit error/failure via proper execution failure path (ideally, with the finish reason (e.g. model returned no content with finish reason: length)) and not session.execution.succeeded. If the fixtures are able to show the degenerate response, then I will consider adding a regression test.
bacigal commentedon Oct 5, 2026
Silent turn-end with reasoning-only
finish_reason=length— simpler repro, and a gap in the #49061 approachHit on v2.0.23 against an OpenAI-compatible endpoint (corporate LiteLLM in front of self-hosted vLLM
vllm-0.26.1rc0), but the repro below needs nothing exotic.Minimal repro: any model config with
limit.outputsmaller than the model's thinking appetite;reasoning_effort: high; a prompt that induces long thinking. E.g. output cap 2000:Observed (deterministic): stream yields
step_start→ onereasoningevent (cut mid-thought at the cap) → end of stream. Nostep_finish, notext, noerrorevent.opencode runexits 0. The turn is silently empty — no retry, no error surfaced to the agent loop or the user.Wire-level check: the raw SSE from the endpoint is protocol-clean:
reasoning_contentdeltas, thenfinish_reason: "length", then[DONE]. So this is not a malformed stream — it is the degenerate-shape case: reasoning present, content absent, finishlength. (A separate malformed-stream case — a trailing content chunk afterfinish_reason, rejected by the guard from #40798 — is what PR #49167 targets; that one we have only observed occasionally ondeepseek-v4.1-flash.)Why this matters for #49061: its guard treats a stream as "generated" when any
text-delta/reasoning-delta/tool event occurred. Here reasoning deltas do occur, so under that guard this case would still end silently. Suggest the empty-response guard count content/tool deltas only, or additionally treatfinish=lengthwith zero content and zero tool calls as retryable. Happy to provide full trajectory JSON if useful.Impact in benchmark harnesses: under Harbor / Terminal-Bench this records reward 0 with no exception, so score loss is invisible unless one digs into the trajectory. Observed repeatedly with high-reasoning models on hard agentic tasks (e.g. GLM-5.3-Flash emitting ~110k-char thinking blocks that exhausted a 32768
max_completion_tokensbudget) before the output cap was raised past the thinking appetite.thdxr commentedon Oct 11, 2026
Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (
opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊