#13693·OmniRoute

fix(api): `/v1/responses` streaming emits `response.output_text.done` after the first delta, permanently truncating visible output

Author: meteor614Created Sep 15, 2026Updated Sep 17, 2026
Labelsbugbacklogapi

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.text equals the concatenation of all preceding response.output_text.delta values for that item, and
  • no response.output_text.delta is emitted after response.output_item.done for the same output_index, and
  • response.completed.response.output[].content[].text equals 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.