mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 00:02:20 +03:00
fix(stream): drop empty choices chunks instead of emitting retry text (#3400)
Integrated into release/v3.8.16
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user