diff --git a/open-sse/utils/responsesStreamHelpers.js b/open-sse/utils/responsesStreamHelpers.js index 6f90a0c1..eaacdcd0 100644 --- a/open-sse/utils/responsesStreamHelpers.js +++ b/open-sse/utils/responsesStreamHelpers.js @@ -5,6 +5,7 @@ import { formatSSE } from "./streamHelpers.js"; // Responses API events that signal the stream has reached a terminal state const OPENAI_RESPONSES_TERMINAL_EVENTS = new Set([ "response.completed", + "response.done", "response.failed", "error" ]); diff --git a/open-sse/utils/stream.js b/open-sse/utils/stream.js index 082b8033..aea96147 100644 --- a/open-sse/utils/stream.js +++ b/open-sse/utils/stream.js @@ -217,9 +217,11 @@ export function createSSEStream(options = {}) { sseEmittedCount++; } - // [DONE] not emitted in translate mode — some clients' SSE decoders - // fail to parse the OpenAI sentinel on Claude-format translated streams. - // message_stop already signals end-of-response; stream close handles it. + if (keepsOpenAIResponsesFormat && !streamDoneSent) { + const doneOutput = "data: [DONE]\n\n"; + reqLogger?.appendConvertedChunk?.(doneOutput); + controller.enqueue(sharedEncoder.encode(doneOutput)); + } streamDoneSent = true; if (keepsOpenAIResponsesFormat) openAIResponsesDoneSent = true; continue; @@ -417,8 +419,13 @@ export function createSSEStream(options = {}) { openAIResponsesTerminalSeen = true; } - // [DONE] not emitted in translate mode — see comment above. - // Passthrough mode still emits it for standard OpenAI clients. + if (keepsOpenAIResponsesFormat && !openAIResponsesDoneSent && !streamDoneSent) { + const doneOutput = "data: [DONE]\n\n"; + reqLogger?.appendConvertedChunk?.(doneOutput); + controller.enqueue(sharedEncoder.encode(doneOutput)); + openAIResponsesDoneSent = true; + streamDoneSent = true; + } if (!hasValidUsage(state?.usage) && totalContentLength > 0) { state.usage = estimateUsage(body, totalContentLength, sourceFormat); diff --git a/tests/unit/openai-responses-terminal-event.test.js b/tests/unit/openai-responses-terminal-event.test.js index db2e3ad8..32ee66c3 100644 --- a/tests/unit/openai-responses-terminal-event.test.js +++ b/tests/unit/openai-responses-terminal-event.test.js @@ -67,6 +67,19 @@ describe("OpenAI Responses streaming termination", () => { expect(output).toContain("data: [DONE]"); }); + it("does not add response.failed when a Responses stream sends response.done", async () => { + const output = await runTransform([ + `event: response.done`, + `data: ${JSON.stringify({ type: "response.done", response: { id: "resp_test" } })}`, + "", + ].join("\n")); + + expect(output).toContain("event: response.done"); + expect(output).not.toContain("event: response.failed"); + expect(output).not.toContain("data: null"); + expect(output).toContain("data: [DONE]"); + }); + it("emits response.failed before DONE when a Responses stream sends DONE without a terminal event", async () => { const output = await runTransform([ `event: response.created`,