From 9ced2e99df983a1b29b957920b40aeef8e321d4f Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Sat, 25 Jul 2026 04:57:22 -0300 Subject: [PATCH] fix(sse): stop stream readiness from treating a choices-less error frame as success (#7503) (#8504) Co-authored-by: ikelvingo --- .../7503-empty-choices-stream-readiness.md | 1 + open-sse/utils/streamReadiness.ts | 31 +++++++++- tests/unit/repro-7503-no-choices.test.ts | 57 +++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/7503-empty-choices-stream-readiness.md create mode 100644 tests/unit/repro-7503-no-choices.test.ts diff --git a/changelog.d/fixes/7503-empty-choices-stream-readiness.md b/changelog.d/fixes/7503-empty-choices-stream-readiness.md new file mode 100644 index 0000000000..95861c9e07 --- /dev/null +++ b/changelog.d/fixes/7503-empty-choices-stream-readiness.md @@ -0,0 +1 @@ +- fix(sse): stop stream readiness from treating a choices-less mid-stream error frame as a successful stream, so combo can fail over instead of returning zero `choices` (#7503) diff --git a/open-sse/utils/streamReadiness.ts b/open-sse/utils/streamReadiness.ts index 0ee108eb6b..7b74f54be3 100644 --- a/open-sse/utils/streamReadiness.ts +++ b/open-sse/utils/streamReadiness.ts @@ -81,11 +81,40 @@ function getPayloadType(payload: unknown, eventType = ""): string { return typeof type === "string" ? type : eventType; } +// Keys that indicate a frame carries (or is starting to carry) actual model +// output — as opposed to a bare `{error:{...}}` frame with no output signal +// at all. A stream that only ever emits error-only frames (e.g. a CLI +// passthrough executor's mid-stream spawn failure, #7503) must NOT be +// classified as "ready" — treating it as ready lets the malformed frame +// reach the client as a fake 200 success and blocks combo fallback to the +// next candidate. +const CONTENT_BEARING_KEYS = [ + "choices", + "candidates", + "content_block", + "delta", + "output", + "response", + "parts", + "tool_calls", + "tool_use", + "function_call", + "function_call_output", +]; + +function isErrorOnlyStructuredPayload(payload: Record): boolean { + if (!("error" in payload)) return false; + return !CONTENT_BEARING_KEYS.some((key) => key in payload); +} + function hasNonPingStructuredPayload(payload: unknown, eventType = ""): boolean { const type = getPayloadType(payload, eventType); if (isPingEventType(eventType) || isPingEventType(type)) return false; if (Array.isArray(payload)) return payload.length > 0; - if (isRecord(payload)) return Object.keys(payload).length > 0; + if (isRecord(payload)) { + if (Object.keys(payload).length === 0) return false; + return !isErrorOnlyStructuredPayload(payload); + } return payload !== null && payload !== undefined; } diff --git a/tests/unit/repro-7503-no-choices.test.ts b/tests/unit/repro-7503-no-choices.test.ts new file mode 100644 index 0000000000..1f7b82673c --- /dev/null +++ b/tests/unit/repro-7503-no-choices.test.ts @@ -0,0 +1,57 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { hasStreamReadinessSignal } = await import("@omniroute/open-sse/utils/streamReadiness"); +const { buildErrorBody } = await import("@omniroute/open-sse/utils/error"); +const { AuggieExecutor } = await import("@omniroute/open-sse/executors/auggie"); + +test("hasStreamReadinessSignal wrongly reports readiness for a choices-less mid-stream error frame", () => { + const errorBody = buildErrorBody(502, "Auggie CLI not found: auggie.cmd"); + assert.equal("choices" in errorBody, false, "sanity check: buildErrorBody() output really has no choices key"); + + const sse = `data: ${JSON.stringify(errorBody)}\n\ndata: [DONE]\n\n`; + const ready = hasStreamReadinessSignal(sse); + + assert.equal( + ready, + false, + "a stream carrying only a choices-less error frame should NOT be reported as 'ready' " + + "(successful) — it gives the combo router no signal to fail over to the next candidate, " + + "and the client accumulates zero `choices` for the whole request, which is exactly what " + + "makes strict OpenAI-SDK clients (VS Code Copilot, Cline) throw 'Response contained no choices.'" + ); +}); + +test("AuggieExecutor's real streaming error path emits an SSE body with zero populated `choices` (end-to-end)", async () => { + const executor = new AuggieExecutor(); + const previousBin = process.env.AUGGIE_BIN; + process.env.AUGGIE_BIN = "/definitely/does/not/exist/auggie-xyz-7503"; + try { + const { response } = await executor.execute({ + model: "claude-sonnet-4.6", + body: { messages: [{ role: "user", content: "hi" }] }, + stream: true, + credentials: {} as never, + signal: null, + log: { info: () => {}, debug: () => {}, warn: () => {} }, + }); + + const text = await response.text(); + const dataLines = text.split("\n").filter((l) => l.startsWith("data: ")) + .map((l) => l.slice("data: ".length).trim()).filter((d) => d !== "[DONE]"); + assert.ok(dataLines.length > 0, "expected at least one SSE data event"); + + const anyPopulatedChoices = dataLines.some((d) => { + const parsed = JSON.parse(d); + return Array.isArray(parsed.choices) && parsed.choices.length > 0 && + (parsed.choices[0]?.delta?.content || parsed.choices[0]?.message?.content); + }); + + assert.equal(anyPopulatedChoices, false, + "AuggieExecutor's CLI-failure SSE stream never carries a populated `choices` entry — " + + "it is only a choices-less {error:...} frame + [DONE]"); + } finally { + if (previousBin === undefined) delete process.env.AUGGIE_BIN; + else process.env.AUGGIE_BIN = previousBin; + } +});