Component: 9Router central gateway (decolua/9router), OpenAI-compatible POST /v1/chat/completions (the ds/ DeepSeek lane).
data: [DONE] to a non-streaming chat.completionComponent: 9Router central gateway (decolua/9router), OpenAI-compatible POST /v1/chat/completions (the ds/ DeepSeek lane).
Symptom: with stream omitted, a 200 response is a valid chat.completion JSON body immediately followed by the SSE sentinel:
...\"system_fingerprint\":\"aeb56401ca74e127821c4f9126dcb669\"}data: [DONE]\n\n
9Router treats an omitted stream as streaming. In open-sse/handlers/chatCore.js (v0.5.95, a99cf57):
const providerRequiresStreaming = PROVIDERS[provider]?.forceStream === true;
let stream = providerRequiresStreaming ? true : (body.stream !== false); // <-- BUG
body.stream !== false is true when the key is absent — backwards from the OpenAI spec (omitted ⇒ non-streaming). deepseek/ds (open-sse/providers/registry/deepseek.js) does not set forceStream, so a stream-less request is routed down the SSE path.
The upstream then returns a plain application/json body (no SSE framing). The passthrough transform in open-sse/utils/stream.js flushes the leftover buffer verbatim, then unconditionally terminates the “SSE” body with the OpenAI sentinel:
if (mode === STREAM_MODE.PASSTHROUGH) {
if (buffer) { // whole JSON body, no trailing \n
controller.enqueue(sharedEncoder.encode(buffer)); // emitted with no `data:` prefix
}
if (!streamDoneSent && !isGeminiFamily) {
controller.enqueue(sharedEncoder.encode("data: [DONE]\n\n")); // <-- sentinel
}
}
Response is sent as Content-Type: text/event-stream, so the client sees <json>data: [DONE]\n\n. The correct fix is the routing decision: a client that did not ask for SSE must not enter the SSE path.
open-sse/utils/bypassHandler.js has the same wrong default for the Claude-CLI naming/warmup path:
const stream = body.stream !== false; // <-- BUG (same default)
Canonical upstream fix (PR #4427), two one-line changes:
--- a/open-sse/handlers/chatCore.js
+++ b/open-sse/handlers/chatCore.js
@@ -133,7 +133,7 @@
const clientRequestedStreaming = body.stream === true || sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI || sourceFormat === FORMATS.GEMINI_CLI;
const providerRequiresStreaming = PROVIDERS[provider]?.forceStream === true;
- let stream = providerRequiresStreaming ? true : (body.stream !== false);
+ let stream = providerRequiresStreaming ? true : (body.stream === true);
--- a/open-sse/utils/bypassHandler.js
+++ b/open-sse/utils/bypassHandler.js
@@ -73,7 +73,7 @@
if (!shouldBypass) return null;
const sourceFormat = detectFormat(body);
- const stream = body.stream !== false;
+ const stream = body.stream === true;
Apply from the repo root:
git apply /workspace/nonstream-default.patch # patch file also written to the sandbox
Semantics after the change (all OpenAI-spec compliant):
stream in request |
provider forceStream |
behaviour |
|---|---|---|
| omitted | false | application/json (fixed) |
| omitted | true | text/event-stream (unchanged) |
false |
false | application/json (unchanged) |
true |
any | text/event-stream + data: [DONE] (unchanged) |
Not required once the default is fixed, but if a non-stream path ever receives SSE it can strip the sentinel:
const json = JSON.stringify(restoreToolNames(translatedResponse, toolNameMap));
const clean = json.endsWith("data: [DONE]") ? json.slice(0, json.lastIndexOf("data: [DONE]")).trimEnd() : json;
return { success: true, response: new Response(clean, { headers: { "Content-Type": "application/json", ... } }) };
Repository https://github.com/decolua/9router @ a99cf57 (v0.5.95), Vitest. Install under tests/ with pnpm add vitest@^4 undici@^7 uuid@^13, plus a root node_modules symlink so open-sse/… resolves undici/uuid.
tests/unit/nonstream-default-regression.test.js drives the real handleChatCore with the DeepSeek (ds) provider, Accept: */*, and asserts the stream flag passed to the executor:
it("does not stream when stream is omitted", async () => {
const { handleChatCore } = await import("../../open-sse/handlers/chatCore.js");
await handleChatCore(deepseekOptions(undefined)); // body has no `stream`
expect(executeMock).toHaveBeenCalledTimes(1);
expect(executeMock.mock.calls[0][0].stream).toBe(false);
});
Before (unmodified):
❯ tests/unit/nonstream-default-regression.test.js (6 tests | 1 failed)
× does not stream when stream is omitted
AssertionError: expected true to be false // Object.is equality
After applying the patch (git apply --check clean, then apply):
✓ tests/unit/nonstream-default-regression.test.js (6 tests)
Test Files 1 passed (1)
Tests 6 passed (6)
Confirms forceStream providers (openai/codex/commandcode), explicit stream: true, and explicit stream: false are unaffected.
# BEFORE: body ends with }data: [DONE]\n\n, Content-Type: text/event-stream
curl -s -D - -X POST http://master001:20128/v1/chat/completions \
-H "Authorization: Bearer $NINEROUTER_KEY" -H 'Content-Type: application/json' \
-d '{"model":"ds/deepseek-v4-flash","messages":[{"role":"user","content":"hi"}]}' \
-o /tmp/body.bin
tail -c 80 /tmp/body.bin | od -c # ... } d a t a : [ D O N E ] \n \n
# AFTER (fixed)
curl -s -D /tmp/hdrs.txt -X POST http://master001:20128/v1/chat/completions \
-H "Authorization: Bearer $NINEROUTER_KEY" -H 'Content-Type: application/json' \
-d '{"model":"ds/deepseek-v4-flash","messages":[{"role":"user","content":"hi"}]}' \
-o /tmp/body.bin
grep -i '^content-type' /tmp/hdrs.txt # content-type: application/json
python3 -c "import json;json.load(open('/tmp/body.bin'));print('valid JSON')"
Expected: omitted/false ⇒ application/json valid JSON with no [DONE]; true ⇒ text/event-stream terminated by data: [DONE]\n\n.
open-sse/handlers/chatCore.js — line 135: body.stream !== false → body.stream === trueopen-sse/utils/bypassHandler.js — line 76: body.stream !== false → body.stream === trueArtifacts written to /workspace: solution.md, nonstream-default.patch, nonstream-default-regression.test.js.
Reference: decolua/9router PR #4427 — “fix: default to non-streaming response when stream unspecified”.
# Evidence - Problem class: openai-compat-gateway-appends-data-done-to-nonstream-body - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-10-05T18:03:59.867Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "REPRODUCED 2026-10-05 (prior capture 2026-10-04, DF-CHIMERA-V2-63): POST /v1/chat/completions to http://master001:20128/v1 with model ds/deepseek-v4-flash, stream UNSET, returns HTTP 200 with a valid OpenAI chat.completion JSON body followed by a literal 'data: [DONE]' line after the closing brace. Byte-verified tail: ...\"system_fingerprint\":\"aeb56401ca74e127821c4f9126dcb669\"}data: [DONE]\\n\\n. A strict JSON client hits parse errors; chimera's lenient client tolerates it, but other OpenAI-compat consumers of the shared fleet gateway may not. Fix belongs to 9router's non-stream response path (strip the SSE sentinel when Content-Type is application/json).", "environment": "9router central gateway :20128, ds/ upstream lane", "language": "go/http", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "openai-compat-gateway-appends-data-done-to-nonstream-body", "provider": "openrouter", "solved_at": "2026-10-05T18:03:59.867Z", "version": ""}