From b6ea87d6acff84430a8e39cb90c7227dd4141bfc Mon Sep 17 00:00:00 2001 From: Ayush Ojha Date: Fri, 30 Jan 2026 00:45:16 -0800 Subject: [PATCH] fix(gateway): send terminal chunk before [DONE] in OpenAI streaming OpenAI-compatible clients expect a terminal chunk with an empty delta and finish_reason:"stop" before the [DONE] sentinel. Without it, some clients crash while finalizing the stream. Centralizes stream shutdown in an endStream() helper that ensures the terminal chunk is sent exactly once before [DONE]. Closes #4298 --- src/gateway/openai-http.ts | 39 ++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/gateway/openai-http.ts b/src/gateway/openai-http.ts index 2cbcb7c1f..c2b983081 100644 --- a/src/gateway/openai-http.ts +++ b/src/gateway/openai-http.ts @@ -252,6 +252,26 @@ export async function handleOpenAiHttpRequest( let wroteRole = false; let sawAssistantDelta = false; let closed = false; + let sentTerminalChunk = false; + + /** Send a terminal chunk with finish_reason before [DONE]. */ + const endStream = () => { + if (closed) return; + closed = true; + if (!sentTerminalChunk) { + sentTerminalChunk = true; + writeSse(res, { + id: runId, + object: "chat.completion.chunk", + created: Math.floor(Date.now() / 1000), + model, + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + }); + } + unsubscribe(); + writeDone(res); + res.end(); + }; const unsubscribe = onAgentEvent((evt) => { if (evt.runId !== runId) return; @@ -294,17 +314,16 @@ export async function handleOpenAiHttpRequest( if (evt.stream === "lifecycle") { const phase = evt.data?.phase; if (phase === "end" || phase === "error") { - closed = true; - unsubscribe(); - writeDone(res); - res.end(); + endStream(); } } }); req.on("close", () => { - closed = true; - unsubscribe(); + if (!closed) { + closed = true; + unsubscribe(); + } }); void (async () => { @@ -363,6 +382,7 @@ export async function handleOpenAiHttpRequest( } } catch (err) { if (closed) return; + sentTerminalChunk = true; writeSse(res, { id: runId, object: "chat.completion.chunk", @@ -382,12 +402,7 @@ export async function handleOpenAiHttpRequest( data: { phase: "error" }, }); } finally { - if (!closed) { - closed = true; - unsubscribe(); - writeDone(res); - res.end(); - } + endStream(); } })();