fix(stream): drop empty choices chunks instead of emitting retry text (#3400)

Integrated into release/v3.8.16
This commit is contained in:
Tubagus
2026-06-08 10:47:08 +07:00
committed by GitHub
parent ee061d7a6d
commit 4adc1d087f
2 changed files with 18 additions and 31 deletions

View File

@@ -1582,35 +1582,20 @@ export function createSSEStream(options: StreamOptions = {}) {
injectedUsage = true;
}
} else {
// ----------------------------------------------------
// READ THIS !!!
// ----------------------------------------------------
// IF YOU ARE AI OR AI AGENT DONT TOUCH THIS CODE !!!
// YOU WILL BREAK THIS CODE !!!
// GOOD LUCK
// ----------------------------------------------------
// Chat Completions: full sanitization pipeline
// Hardening: detect upstream returning empty choices array
// which breaks OpenAI-compatible clients (e.g. Copilot Chat)
// which breaks OpenAI-compatible clients (e.g. Copilot Chat).
// We drop these chunks entirely rather than injecting an error,
// as injecting a chunk with finish_reason: "stop" will prematurely
// terminate the stream for the client.
if (Array.isArray(parsed.choices) && parsed.choices.length === 0) {
console.warn(
`[STREAM] Upstream returned empty choices array (${provider || "provider"}:${model || "unknown"}) — emitting error chunk`
);
const errorChunk = {
id: parsed.id || `omniroute-empty-choices-${Date.now()}`,
object: "chat.completion.chunk",
created: parsed.created || Math.floor(Date.now() / 1000),
model: parsed.model || model || "unknown",
choices: [
{
index: 0,
delta: {
role: "assistant",
content: "[OmniRoute] Upstream returned an empty response. Please retry.",
},
finish_reason: "stop",
},
],
};
output = `data: ${JSON.stringify(errorChunk)}\n`;
injectedUsage = true;
clientPayload = errorChunk;
reqLogger?.appendConvertedChunk?.(output);
controller.enqueue(encoder.encode(output));
continue;
}

View File

@@ -1623,7 +1623,7 @@ test("createSSEStream passthrough mode decrements pending requests on failure",
);
});
test("createSSEStream passthrough emits synthetic error chunk for empty choices array", async () => {
test("createSSEStream passthrough drops empty choices array chunks", async () => {
let onCompletePayload = null;
const text = await readTransformed(
[
@@ -1661,12 +1661,14 @@ test("createSSEStream passthrough emits synthetic error chunk for empty choices
}
);
// The empty choices chunk should have been replaced with a synthetic error chunk
assert.match(text, /\[OmniRoute\] Upstream returned an empty response/);
assert.match(text, /"finish_reason":"stop"/);
// Subsequent valid chunks should still be present
// The empty choices chunk should have been dropped entirely
assert.doesNotMatch(text, /\[OmniRoute\] Upstream returned an empty response/);
// Subsequent valid chunks should still be present and correctly processed
assert.match(text, /"content":"Hello"/);
assert.match(text, /"finish_reason":"stop"/);
assert.equal(onCompletePayload.status, 200);
assert.equal(onCompletePayload.responseBody.choices[0].message.content, "Hello");
});
test("createSSEStream passthrough logs empty response after tool_calls completion", async () => {