#2745·manifest

GPT-5.6 Luna via OpenCode Zen ends SSE stream without finish_reason or [DONE], causing incomplete_stream

Author: bexemCreated Aug 20, 2026Updated Aug 21, 2026

Summary

When opencode-zen/gpt-5.6-luna is routed through Manifest's OpenAI-compatible Chat Completions API with streaming enabled, OpenCode Zen returns the answer text and usage/cost data, then closes the SSE connection without sending a terminal completion event.

Manifest subsequently records the request as failed and emits:

Upstream provider stream ended before its completion event.

The client may already have received and displayed the complete answer before the error arrives.

This has currently only been reproduced with gpt-5.6-luna. gpt-5.6-sol works through the same OpenCode Zen/Manifest route in the current test set. Other OpenCode Zen models have not been tested enough to generalise the failure beyond Luna.

Environment

  • Self-hosted Manifest using the opencode-zen API-key provider
  • Affected model: opencode-zen/gpt-5.6-luna
  • Manifest API: OpenAI-compatible POST /v1/chat/completions
  • OpenCode Zen API: https://opencode.ai/zen/v1/chat/completions
  • Wire format: OpenAI Chat Completions SSE

Reproduction

Directly against OpenCode Zen

bash
export OPENCODE_ZEN_BASE="https://opencode.ai/zen/v1"

curl -N "$OPENCODE_ZEN_BASE/chat/completions" \
  -H "Authorization: Bearer $OPENCODE_ZEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.6-luna",
    "messages": [
      {"role": "user", "content": "hi"}
    ],
    "stream": true
  }'

The response produces the answer text, followed by empty choice chunks, a usage object, and a cost event. The connection then closes.

The observed Luna stream contains no:

finish_reason: "stop"

and no:

data: [DONE]

All observed finish_reason values are null.

Through Manifest

bash
export MANIFEST_BASE="https://your-manifest-host/v1"

curl -N "$MANIFEST_BASE/chat/completions" \
  -H "Authorization: Bearer $MANIFEST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "opencode-zen/gpt-5.6-luna",
    "messages": [
      {"role": "user", "content": "hi"}
    ],
    "stream": true
  }'

The client receives the answer text, but the final part of the SSE response contains an error similar to:

data: {"error":{"message":"Upstream provider stream ended before its completion event.","type":"server_error","code":"incomplete_stream","status":503,"source":"provider","provider":"opencode-zen","model":"opencode-zen/gpt-5.6-luna"}}

data: [DONE]

The [DONE] shown in the Manifest response is emitted after Manifest detects the incomplete upstream stream. It is not present in the raw OpenCode Zen response.

Additional observations

  • The same incomplete termination occurs for Luna with and without stream_options.include_usage.
  • It also occurs for Luna when tools are present, and when tools are absent.
  • Disabling stream_options.include_usage alone does not resolve the problem.
  • Disabling Manifest's "auto-fix failing requests" setting does not change the outcome; the failure reproduces with the setting both on and off.
  • A true buffered/non-streaming request returns a normal JSON response through Manifest.
  • Manifest's Playground path works, but it uses a different routing path from the normal default chat-completions stream.
  • OpenRouter's Luna route works through Manifest.
  • gpt-5.6-sol works through the same OpenCode Zen/Manifest route in the current test set.
  • Direct clients can display the Luna answer because they tolerate the EOF after receiving content, even though the stream does not provide the normal OpenAI terminal event.

Why Manifest reports the request as failed

The current Manifest stream observer in packages/backend/src/routing/proxy/stream-protocol.ts considers an OpenAI Chat Completions stream complete only when it sees either:

  1. data: [DONE], or
  2. a non-null choices[*].finish_reason.

Otherwise assertComplete() raises incomplete_stream.

The observer is created by stream-writer.ts, while proxy-response-handler.ts supplies the relay protocol for the upstream response.

This behaviour is appropriate for a genuinely interrupted stream, but it does not accommodate the Luna response observed above, where the upstream sends content followed by usage and cost trailers before a clean EOF.

Proposed Manifest fix

The safest fix appears to be a narrowly scoped completion policy rather than weakening the global OpenAI stream validator.

The current code path could be extended as follows:

  1. Add an optional completion policy to StreamRelayOptions, rather than hard-coding a provider name inside the generic protocol observer.
  2. Pass a policy from handleStreamResponse() when meta.provider === 'opencode-zen'.
  3. Let StreamProtocolObserver track the provider-specific terminal trailers while parsing the OpenAI events:
    • a valid OpenAI usage object, and
    • the OpenCode Zen cost event.
  4. For that policy only, treat EOF as complete when both final trailers have been observed and no provider-declared error was received.
  5. Leave the current strict [DONE] / non-null finish_reason requirement unchanged for every other provider and for OpenCode Zen streams that end before the final usage/cost sequence.

Conceptually:

typescript
const completionPolicy =
  meta.provider === 'opencode-zen'
    ? 'allow-eof-after-usage-and-cost'
    : 'strict';

Then the observer would retain the existing terminal checks and add a provider-policy fallback at assertComplete():

typescript
if (
  !this.completed &&
  this.policy === 'allow-eof-after-usage-and-cost' &&
  this.sawUsage &&
  this.sawCost
) {
  return;
}

if (!this.completed) {
  throw new StreamFailure(
    'incomplete_stream',
    503,
    INCOMPLETE_STREAM_MESSAGE,
  );
}

This would preserve detection of:

  • a connection ending before any meaningful response,
  • a response ending before usage/cost trailers,
  • a provider-declared error,
  • a genuine mid-response interruption on all other providers.

The alternative, and arguably preferable upstream fix, is for OpenCode Zen to emit a normal OpenAI-compatible terminal chunk with a non-null finish_reason, followed by data: [DONE].

Tests that should accompany the fix

The existing stream-protocol.spec.ts tests strict clean-EOF rejection. I suggest adding tests for both behaviours:

  1. A strict OpenAI Chat Completions observer still rejects clean EOF without a terminal event.
  2. The OpenCode Zen compatibility policy accepts a sequence containing content, a valid usage event, and a cost event followed by EOF.
  3. The compatibility policy still rejects EOF before the usage/cost trailers.
  4. Provider-declared errors still throw even when usage was already seen.
  5. A normal [DONE] or non-null finish_reason continues to work for every policy.
  6. A regression test exercises handleStreamResponse() with provider: 'opencode-zen' and verifies that the successful response is recorded as success rather than incomplete_stream.

Related Manifest work

These existing changes are related but do not appear to fix this specific Luna issue:

  • #2141 added OpenCode Zen to the streaming provider allowlist.
  • #2539 added terminal errors for interrupted provider streams.
  • #2578 formalised incomplete-stream detection and request outcome recording.

Question

Is the missing terminal event a Luna-specific OpenCode Zen API behaviour? If so, should Manifest add the narrowly scoped compatibility policy described above, or should the OpenCode Zen endpoint be changed to emit the standard Chat Completions termination sequence?