Skip to content

Empty response with finish_reason "length" and zero tokens is treated as success #50949

Closed as not planned

Description

@ImDevinC

Summary

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

  1. 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": []}.

  2. Run:

    opencode run -m openrouter/openai/gpt-6-luna "What is the capital of France?"
    
  3. 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:

model returned no content (finish reason: length)

Actual

The stored assistant message is degenerate:

  • finish: "length"
  • tokens.input: 0, tokens.output: 0, tokens.reasoning: 0
  • parts contain only step-start and step-finish

The loop exits quietly with no user-visible error:

message=stream providerID=openrouter modelID=openai/gpt-6-luna
message="llm runtime selected" llm.runtime=ai-sdk llm.provider=openrouter llm.model=openai/gpt-6-luna
message=loop session.id=... step=1
message="exiting loop" session.id=...

Raw provider response:

data: {"id":"gen-...","object":"chat.completion.chunk","model":"openai/gpt-6-luna","provider":"OpenAI","choices":[{"index":0,"delta":{"content":"","role":"assistant"},"finish_reason":"length","native_finish_reason":"max_output_tokens"}]}
data: [DONE]

Suggested fixes

  1. 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.

  2. 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.

Environment

  • opencode 1.18.32, Linux
  • Provider openrouter (@openrouter/ai-sdk-provider 2.9.0)
  • Model openai/gpt-6-luna (reproduces with other OpenAI GPT models)

Activity

  1. Yash-912 commented on Sep 29, 2026

    @Yash-912

    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.

  2. bacigal commented on Oct 5, 2026

    @bacigal

    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.

  3. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    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 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions