GPT-5.6 Luna via OpenCode Zen ends SSE stream without finish_reason or [DONE], causing incomplete_stream
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-zenAPI-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
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
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_usagealone 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-solworks 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:
data: [DONE], or- 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:
- Add an optional completion policy to
StreamRelayOptions, rather than hard-coding a provider name inside the generic protocol observer. - Pass a policy from
handleStreamResponse()whenmeta.provider === 'opencode-zen'. - Let
StreamProtocolObservertrack the provider-specific terminal trailers while parsing the OpenAI events:- a valid OpenAI usage object, and
- the OpenCode Zen cost event.
- For that policy only, treat EOF as complete when both final trailers have been observed and no provider-declared error was received.
- Leave the current strict
[DONE]/ non-nullfinish_reasonrequirement unchanged for every other provider and for OpenCode Zen streams that end before the final usage/cost sequence.
Conceptually:
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():
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:
- A strict OpenAI Chat Completions observer still rejects clean EOF without a terminal event.
- The OpenCode Zen compatibility policy accepts a sequence containing content, a valid usage event, and a cost event followed by EOF.
- The compatibility policy still rejects EOF before the usage/cost trailers.
- Provider-declared errors still throw even when usage was already seen.
- A normal
[DONE]or non-nullfinish_reasoncontinues to work for every policy. - A regression test exercises
handleStreamResponse()withprovider: 'opencode-zen'and verifies that the successful response is recorded as success rather thanincomplete_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?
Source: mnfst/manifest