fix(responses): handle response.done terminal events (#2142)
Treat response.done as a terminal OpenAI Responses stream event so passthrough streams ending with response.done are not flagged incomplete and no synthetic response.failed is emitted. Restore the data: [DONE] sentinel for same-format Responses passthrough streams. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
373850ee36
commit
a9785a5f70
3 changed files with 26 additions and 5 deletions
|
|
@ -5,6 +5,7 @@ import { formatSSE } from "./streamHelpers.js";
|
||||||
// Responses API events that signal the stream has reached a terminal state
|
// Responses API events that signal the stream has reached a terminal state
|
||||||
const OPENAI_RESPONSES_TERMINAL_EVENTS = new Set([
|
const OPENAI_RESPONSES_TERMINAL_EVENTS = new Set([
|
||||||
"response.completed",
|
"response.completed",
|
||||||
|
"response.done",
|
||||||
"response.failed",
|
"response.failed",
|
||||||
"error"
|
"error"
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -217,9 +217,11 @@ export function createSSEStream(options = {}) {
|
||||||
sseEmittedCount++;
|
sseEmittedCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [DONE] not emitted in translate mode — some clients' SSE decoders
|
if (keepsOpenAIResponsesFormat && !streamDoneSent) {
|
||||||
// fail to parse the OpenAI sentinel on Claude-format translated streams.
|
const doneOutput = "data: [DONE]\n\n";
|
||||||
// message_stop already signals end-of-response; stream close handles it.
|
reqLogger?.appendConvertedChunk?.(doneOutput);
|
||||||
|
controller.enqueue(sharedEncoder.encode(doneOutput));
|
||||||
|
}
|
||||||
streamDoneSent = true;
|
streamDoneSent = true;
|
||||||
if (keepsOpenAIResponsesFormat) openAIResponsesDoneSent = true;
|
if (keepsOpenAIResponsesFormat) openAIResponsesDoneSent = true;
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -417,8 +419,13 @@ export function createSSEStream(options = {}) {
|
||||||
openAIResponsesTerminalSeen = true;
|
openAIResponsesTerminalSeen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [DONE] not emitted in translate mode — see comment above.
|
if (keepsOpenAIResponsesFormat && !openAIResponsesDoneSent && !streamDoneSent) {
|
||||||
// Passthrough mode still emits it for standard OpenAI clients.
|
const doneOutput = "data: [DONE]\n\n";
|
||||||
|
reqLogger?.appendConvertedChunk?.(doneOutput);
|
||||||
|
controller.enqueue(sharedEncoder.encode(doneOutput));
|
||||||
|
openAIResponsesDoneSent = true;
|
||||||
|
streamDoneSent = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasValidUsage(state?.usage) && totalContentLength > 0) {
|
if (!hasValidUsage(state?.usage) && totalContentLength > 0) {
|
||||||
state.usage = estimateUsage(body, totalContentLength, sourceFormat);
|
state.usage = estimateUsage(body, totalContentLength, sourceFormat);
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,19 @@ describe("OpenAI Responses streaming termination", () => {
|
||||||
expect(output).toContain("data: [DONE]");
|
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 () => {
|
it("emits response.failed before DONE when a Responses stream sends DONE without a terminal event", async () => {
|
||||||
const output = await runTransform([
|
const output = await runTransform([
|
||||||
`event: response.created`,
|
`event: response.created`,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue