anomalyco / anomalyco/opencode
Streaming responses for muse-* models on the Zen gateway never send finish_reason — strict OpenAI-compatible clients enter a retry loop
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 aliasmuse-spark-1.2also accepted and exhibits the same behavior) - OS: Windows 11
- Client: Pi Agent SDK (Claude Agent SDK runtime) with streaming enabled
Steps to reproduce
- Send an OpenAI-compatible streaming request (
"stream": true) for any muse model, e.g.muse-spark-1.2-contributor, tohttps://opencode.ai/zen/go/v1/chat/completions. - Observe the SSE stream until completion.
Expected behavior (OpenAI spec)
Per the OpenAI streaming spec, the stream must end with:
- a final content chunk whose
choices[0].finish_reasonis non-null ("stop","tool_calls", ...), and then - 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/modelscurrently lists the model asmuse-spark-1.2-contributor(no plainmuse-spark-1.2), yet the legacy id is still routable — the alias path yields the same missingfinish_reasonbehavior.- 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 viax-api-key) exists and appears to be a stable path for the same models; confirming whether it emits propermessage_stop/stop_reasonwould be useful.
Suggested fix
- Ensure OpenAI-compatible streaming for muse models appends the standard final chunk
{"choices":[{"finish_reason": ...}]}followed bydata: [DONE]— even when a usage chunk is included. - 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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