anomalyco / anomalyco/opencode

Streaming responses for muse-* models on the Zen gateway never send finish_reason — strict OpenAI-compatible clients enter a retry loop

Open
#43,379 7 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Summary

When streaming chat completions for muse-family models through the OpenCode Zen gateway (https://opencode.ai/zen/go/v1), the stream closes after delivering the full text content without ever sending a chunk that carries choices[0].finish_reason (and, in some runs, without the standard data: [DONE] terminator). OpenAI-compatible clients that strictly validate the end-of-stream marker therefore treat every single turn as a failed/aborted generation, abort the agent loop, and retry — producing repeated identical output and eventually timing out.

Environment

  • Endpoint: https://opencode.ai/zen/go/v1/chat/completions (OpenAI-compatible SSE streaming)
  • Auth: Authorization: Bearer <zen key> / standard OpenAI auth
  • Model: muse-spark-1.2-contributor (legacy alias muse-spark-1.2 also accepted and exhibits the same behavior)
  • OS: Windows 11
  • Client: Pi Agent SDK (Claude Agent SDK runtime) with streaming enabled

Steps to reproduce

  1. Send an OpenAI-compatible streaming request ("stream": true) for any muse model, e.g. muse-spark-1.2-contributor, to https://opencode.ai/zen/go/v1/chat/completions.
  2. Observe the SSE stream until completion.

Expected behavior (OpenAI spec)

Per the OpenAI streaming spec, the stream must end with:

  1. a final content chunk whose choices[0].finish_reason is non-null ("stop", "tool_calls", ...), and then
  2. a terminating data: [DONE] event.
data: {"id":"...","choices":[{"index":0,"delta":{},"finish_reason":"stop"}], ...}
data: [DONE]

Strict clients rely on this final chunk to mark the turn as complete.

Actual behavior

  • Content deltas arrive fully and correctly (full assistant reply is delivered).
  • A usage chunk may also be delivered.
  • The stream then closes without any chunk containing a non-null finish_reason.
  • Resulting client-side error (from Pi Agent SDK's OpenAI streaming parser):
Stream ended without finish_reason

Log excerpt showing content + usage delivered but stopReason: "error":

{
  "message": { "role": "assistant", "content": [{"type": "text", "text": "<full reply> ..."}] },
  "api": "openai-completions",
  "model": "muse-spark-1.2",
  "usage": { "input": 19394, "output": 318 },
  "stopReason": "error",
  "errorMessage": "Stream ended without finish_reason",
  "responseId": "resp_6a855d2fa21a731c0f8d4b55"
}

Because the client treats the turn as failed, it retries the request; each retry regenerates the same reply and fails the same way. After a few retries the connection times out (Request timed out.) and the agent reports a network error.

Impact

  • Agentic clients (Claude Agent SDK based, Copilot-like harnesses that validate finish_reason) cannot use muse models through the Zen OpenAI-compatible endpoint at all: every turn is flaky-to-broken, with duplicated output and reconnect errors.
  • The same client works fine with other Zen models (e.g. deepseek-v4-flash, kimi-k3, gpt-5.6-luna), which strongly points to a muse-specific streaming-transport issue on the gateway.

Additional observations

  • GET /zen/go/v1/models currently lists the model as muse-spark-1.2-contributor (no plain muse-spark-1.2), yet the legacy id is still routable — the alias path yields the same missing finish_reason behavior.
  • With some egress regions the gateway also returns 403 RegionError: "This model is not available in your country." for muse models, which suggests both geo-restriction and per-model transport differences.
  • The Anthropic-compatible endpoint (POST /zen/go/v1/messages, auth via x-api-key) exists and appears to be a stable path for the same models; confirming whether it emits proper message_stop/stop_reason would be useful.

Suggested fix

  1. Ensure OpenAI-compatible streaming for muse models appends the standard final chunk {"choices":[{"finish_reason": ...}]} followed by data: [DONE] — even when a usage chunk is included.
  2. If the underlying upstream muse provider cannot report a finish reason, synthesize one from the transport (e.g. treat a complete content stream as "stop").

Happy to provide more logs or run additional probes if useful.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the issue with a streaming request to https://opencode.ai/zen/go/v1/chat/completions using a muse model, and compare its SSE events with a working Zen model. Trace the gateway's OpenAI-compatible streaming path and verify that a non-null finish_reason chunk and data: [DONE] are emitted after content, including when a usage chunk is present.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.