Skip to content

run: openai-compatible provider (Z.AI) stream always ends without finish_reason; interactive sessions unaffected #55124

Open

Description

@janmoreforsk

opencode run fails against openai-compatible provider: stream always ends without finish_reason

Summary

opencode run fails 100% of the time against a custom openai-compatible provider (Z.AI coding endpoint via @ai-sdk/openai-compatible): every streaming response terminates without a finish_reason and the session aborts after retries — even for a trivial prompt. Interactive TUI sessions against the same provider, background service, and config work flawlessly, so the provider is reachable and the credential is valid.

Environment

  • opencode version: 2.0.27
  • OS: Windows 11, build 10.0.26300.9457 (win32 x64)
  • Terminal: Unavailable: reproduced via the shared background service (non-interactive opencode run); interactive sessions on the same machine work
  • Shell: cmd.exe (COMSPEC); reproduction invoked through OpenCode's shell tool (git-bash)
  • Install/channel: latest
  • Active plugins: none

Provider config (key redacted):

"provider": {
  "provider-auth-zai": {
    "name": "Z.AI",
    "npm": "@ai-sdk/openai-compatible",
    "options": {
      "apiKey": "{redacted}",
      "baseURL": "https://api.z.ai/api/coding/v1"
    }
  }
}

Models tried: glm-5 and glm-4.7 — both affected.

Reproduction

  1. Configure an openai-compatible provider pointing at the Z.AI coding endpoint (https://api.z.ai/api/coding/v1) with @ai-sdk/openai-compatible.
  2. With the background service running (during these reproductions another interactive session was active on the same service — possibly relevant, see concurrency note below).
  3. Run:
opencode run "Reply with exactly: OK"

Also reproduced with --model provider-auth-zai/glm-4.7 and with a longer prompt requiring MCP tool use. 4/4 failing sessions.

Expected Behavior

The run session completes and prints OK — the same provider streams normally in interactive sessions on this machine and config.

Actual Behavior

The CLI retries the step (4–10 times, shown as > build · glm-5 lines), then exits with:

Error: OpenAI Chat stream ended without finish_reason

Service log entry (identical across all four sessions):

level=ERROR message="Failed to drain Session"
cause="AI.Error: OpenAI Chat stream ended without finish_reason
    ... SessionStep.attempt ... SessionRunner.runStep ... SessionRunner.drain ..."
{ [cause]: AI.Error.InvalidProviderOutput: OpenAI Chat stream ended without finish_reason }

Failing session IDs: ses_ecaedfc7fffehVBgQO1KLszcZT, ses_ecaec6b48ffe8ISDa8d4M3807d, ses_ecaeadf2bffeJw8m78Tf3wzY5i, ses_ecae9683dffek5esLzAgzYJQq5i

Additional Context

  • 100% reproducible in run mode (4 sessions, 2 models, trivial and long prompts); 0% in interactive sessions on the same provider/service.
  • Ruled out: prompt content, model choice, MCP servers (failure occurs before any tool access; reproduced with a trivial prompt), and provider availability (interactive sessions stream fine concurrently).
  • Two candidate causes worth discriminating:
    1. Concurrency cap on the provider plan — every failure occurred while another OpenCode session was active on the same API key. The Z.AI SSE stream may be dropped for concurrent non-interactive requests.
    2. Run-mode request composition — something in how run builds the request (agent/system prompt assembly, tool schemas, streaming options) may cause the provider to truncate the stream.
  • Workaround: use interactive sessions; opencode run is unusable with this provider.

Activity

  1. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I tried this on v2.0.27 with your provider config, pointed at a local mock openai-compatible server instead of Z.AI. With a normal stream that ends in finish_reason: "stop", opencode run "Reply with exactly: OK" printed OK and exited cleanly. So run handles a well-formed stream, and nothing in the request it sent looked specific to run mode.

    That suggests the problem depends on what Z.AI's coding endpoint actually returns to these requests. I can't check that here because it needs a real Z.AI account, so this needs a maintainer who has one.

    If you can, these would help narrow it down:

    • Does opencode run still fail when no other OpenCode session is active on the same API key? That would tell a concurrency limit apart from something about the request itself.
    • Does it fail when run directly from cmd.exe/PowerShell instead of through OpenCode's shell tool, without the shared background service?

    Related, though it looks like a different trigger: #50146 (the same stream ended without finish_reason error, intermittent on GLM streams via OpenCode Go).

  2. Israel-Kli commented on Oct 14, 2026

    @Israel-Kli

    I can reproduce the same missing finish_reason failure with Snowflake Cortex's OpenAI-compatible endpoint, specifically for Claude models.

    Environment:

    • opencode v2.0.29
    • provider: built-in snowflake-cortex, @ai-sdk/openai-compatible
    • endpoint: https://<account>.snowflakecomputing.com/api/v2/cortex/v1/chat/completions
    • models tested: claude-sonnet-4-6, claude-opus-5, openai-gpt-5.2

    Session DB evidence from opencode after a successful visible answer from claude-sonnet-4-6:

    "finish":"error",
    "error":{
      "type":"provider.invalid-output",
      "message":"OpenAI Chat stream ended without finish_reason",
      "status":200
    }

    That makes opencode inject the synthetic continuation prompt:

    The previous response was interrupted. Continue from where you left off without repeating completed content.
    

    The direct SSE response explains it. Cortex's Claude stream emits content, then an empty role chunk, then usage, then [DONE]. No chunk contains choices[0].finish_reason:

    data: {"choices":[{"delta":{"content":"...","role":"assistant"},"index":0}],...}
    data: {"choices":[{"delta":{"role":"assistant"},"index":0}],...}
    data: {"choices":[],"usage":{"completion_tokens":25,...},...}
    data: [DONE]
    

    By comparison, the same Cortex OpenAI-compatible endpoint with openai-gpt-5.2 does include the terminal finish chunk:

    data: {"choices":[{"delta":{},"finish_reason":"stop","index":0,...}],...}
    data: {"choices":[],"usage":{"completion_tokens":5,...},...}
    data: [DONE]
    

    Cortex's Anthropic-style endpoint for the same Claude model does provide an explicit stop signal:

    event: message_delta
    data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null,...},"usage":{...}}
    
    event: message_stop
    

    So this is another OpenAI-compatible provider/model combination where a completed stream can end cleanly with [DONE] but without finish_reason. opencode currently treats it as a failed turn and retries, even though the answer was already delivered.

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