fix(api): `/v1/responses` streaming emits `response.output_text.done` after the first delta, permanently truncating visible output
bug(api): /v1/responses streaming emits response.output_text.done after the first delta, permanently truncating visible output
OmniRoute Version
3.8.50 (npm latest at time of writing; also reproduced on 3.8.49 behaviour)
Installation Method
npm global install (omniroute package, launched as node .../bin/omniroute)
Operating System
macOS
OS Version
macOS 26
Node.js Version
v26.8.2
Provider(s) Involved
Bare model deepseek-flash (no combo, no custom provider, no translation). The reporter's instance also proxies to deepseek-v4.1-flash upstream.
Model(s) Involved
deepseek-flash
Client Tool
Codex (Responses wire API, POST /v1/responses)
Description
On a plain text-only streaming Responses request, OmniRoute 3.8.50 closes the message output item after the very first response.output_text.delta, then keeps emitting the remaining text deltas on the wire.
Concretely, the event order is:
response.output_text.delta delta="1"
response.output_text.done text="1" <-- emitted here, only 1 character in buffer
response.content_part.done
response.output_item.done
response.output_text.delta delta="\n" <-- remaining deltas arrive after the item closed
response.output_text.delta delta="2"
...
response.completed
A Responses-native client treats response.output_item.done as terminal for that item, so it finalises the message at the text captured so far. Everything after is attributed to an already-closed item and discarded. In Codex the assistant reply is visibly truncated to the first character(s) — exactly as many characters as the first upstream packet happened to contain.
This is not a display-only artifact. The terminal response.completed.response.output[0].content[0].text also contains only "1", so the truncation is committed to the final payload as well. The text deltas still on the wire are stranded.
Steps to Reproduce
No tools, no combo, no provider rotation. Plain text, stream: true:
curl -sN http://localhost:20128/v1/responses \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <OMNIROUTE_API_KEY>' \
-d '{
"model": "deepseek-flash",
"input": "Count from 1 to 5, one number per line. Nothing else.",
"stream": true,
"max_output_tokens": 100
}'
Reproduced on two consecutive runs, byte-identical output (4307 bytes each), 17 events each.
Expected Behavior
All response.output_text.delta chunks for the message are delivered first, then the completion group:
response.content_part.added
response.output_text.delta (n times)
response.output_text.done <-- text == concatenation of all deltas
response.content_part.done
response.output_item.done
...
response.completed
response.output_text.done.text and response.completed.response.output[*].content[*].text both contain the full message.
Actual Behavior
Raw event sequence (sequence_number shown):
idx event detail
0 response.created
1 response.in_progress
2 response.output_item.added
3 response.content_part.added
4 response.output_text.delta delta='1' seq=5
5 response.output_text.done text='1' seq=6
6 response.content_part.done seq=7
7 response.output_item.done <-- message item closed seq=8
8 response.output_text.delta delta='\n' seq=9
9 response.output_text.delta delta='2' seq=10
10 response.output_text.delta delta='\n' seq=11
11 response.output_text.delta delta='3' seq=12
12 response.output_text.delta delta='\n' seq=13
13 response.output_text.delta delta='4' seq=14
14 response.output_text.delta delta='\n' seq=15
15 response.output_text.delta delta='5' seq=16
16 response.completed seq=17
Concatenation of all 9 deltas: '1\n2\n3\n4\n5' (correct text is fully transmitted).
But the terminal payload disagrees:
{
"type": "response.completed",
"response": {
"id": "resp_cmb-4e17e3fdb0b511f185d06ee0323bb52d",
"object": "response",
"created_at": 1789442818,
"status": "completed",
"background": false,
"error": null,
"output": [
{
"id": "msg_resp_cmb-4e17e3fdb0b511f185d06ee0323bb52d_0",
"type": "message",
"content": [
{ "type": "output_text", "annotations": [], "logprobs": [], "text": "1" }
],
"role": "assistant"
}
],
"model": "deepseek-v4.1-flash",
"usage": { "input_tokens": 20, "output_tokens": 9, "total_tokens": 29 }
},
"sequence_number": 17
}
Note output_text is "1" while usage.output_tokens is 9 — the accounting knows 9 tokens were produced, but only 1 reached the payload. The same request with stream: false returns the full "1\n2\n3\n4\n5", confirming the defect is specific to the streaming path.
Test Impact
Needs a new integration test
Error Logs / Output
No error is surfaced; HTTP 200 throughout. The request "succeeds" from OmniRoute's perspective while silently corrupting the output.
Additional Context
Likely root cause. In the compiled Responses translator (dist/.build/next/server/chunks/_18ct13i._.js), the helper that finalises a message output item snapshots the accumulated text buffer at call time and then marks the item done:
function _(e, t, n) {
if (e.msgItemAdded[n] && !e.msgItemDone[n]) {
e.msgItemDone[n] = !0;
let r = e.msgTextBuf[n] || "", // snapshot of text accumulated SO FAR
o = (0, c.normalizeOutputIndex)(n),
i = `msg_${e.responseId}_${o}`;
t("response.output_text.done", { ..., text: r, logprobs: [] }),
t("response.content_part.done", { ..., part: { ..., text: r } });
let s = { id: i, type: "message",
content: [{ type: "output_text", ..., text: r }], role: "assistant" };
t("response.output_item.done", { ..., item: s }),
h(e, o, s) // enqueue as completed item
}
}
Because r is a snapshot rather than the final buffer, calling _() before the stream ends freezes a partial string as the item's final text. The observed event order implies _() is being invoked on an event that arrives mid-stream — plausibly triggered by an upstream chunk whose shape is misread as "message finished" (e.g. a finish_reason-bearing or role-only chunk), rather than only at true stream end.
Why this differs from #10156. That issue (#10156, closed by #10923) reports commentary-phase items whose entire SSE frame group is dropped, with the text still present in response.completed. Here the frames are all emitted — but output_text.done fires early holding a partial snapshot, and the terminal payload is truncated identically. Different failure mechanism, different signature: the client loses text that OmniRoute believes it delivered.
Related issues checked (none match). #6906/#6965 (early response.completed loses trailing usage), #3612 (index-only first tool_call chunk ⇒ stream judged empty, fixed 3.8.22), #909 (missing response.completed on Claude-format providers), #3948/#4315 (non-streaming empty content), #1542 (chat-shaped error frames), #12370 (Gemini translation loss).
Settings that do not help. Per #10156, stream_default_mode=legacy and streamRecovery have no effect when the request explicitly supplies stream: true; the reporter confirms.
Validation Plan
Add a recorded fixture of a plain text-only streaming Responses response and assert the invariant that currently fails:
response.output_text.done.textequals the concatenation of all precedingresponse.output_text.deltavalues for that item, and- no
response.output_text.deltais emitted afterresponse.output_item.donefor the sameoutput_index, and response.completed.response.output[].content[].textequals the same concatenation.
Then run POST /v1/responses with stream: true against any upstream and verify the Codex/GPT desktop client renders the full assistant message rather than a truncated prefix.
Source: diegosouzapw/OmniRoute