From 8fd93a11bccd52ef5d753a3360e77dc070ae7e32 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Sat, 8 Aug 2026 11:13:58 +0800 Subject: [PATCH] fix(openai-chat): ignore empty data: frames; tighten unspaced [DONE] test Follow-up to #1194 carrying forward the two pieces of #1188 that the superseding PR did not take, per maintainer review: - A bare `data:` line (heartbeat-style keep-alive on some gateways) now yields no payload and is skipped, instead of falling into JSON.parse("") and terminating the stream as a malformed frame. - The unspaced [DONE] regression test is driven by the sentinel frame alone: with a preceding answer frame, the finish-less EOF fallback could emit done even if unspaced [DONE] parsing were broken. --- src/adapters/openai-chat.ts | 3 +++ tests/sse-unspaced-data-fields.test.ts | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/adapters/openai-chat.ts b/src/adapters/openai-chat.ts index 42a923657..09c94b069 100644 --- a/src/adapters/openai-chat.ts +++ b/src/adapters/openai-chat.ts @@ -951,6 +951,9 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd const rawPayload = sseFieldValue(line, "data"); if (rawPayload === null) return "continue"; const payload = rawPayload.trim(); + // A bare `data:` line carries nothing (heartbeat-style keep-alive on some gateways); + // it is not a malformed frame, just nothing to parse. + if (payload.length === 0) return "continue"; if (payload === "[DONE]") { yield* flushToolCalls(); const stopReason = stopReasonFor(finishReason); diff --git a/tests/sse-unspaced-data-fields.test.ts b/tests/sse-unspaced-data-fields.test.ts index 3df1db33f..a1b6322c9 100644 --- a/tests/sse-unspaced-data-fields.test.ts +++ b/tests/sse-unspaced-data-fields.test.ts @@ -112,12 +112,23 @@ describe("openai-chat adapter (#1170)", () => { }); test("accepts an unspaced [DONE] sentinel", async () => { + // Sentinel only: a preceding answer frame would let the finish-less EOF fallback emit + // `done` even if unspaced [DONE] handling were broken, so this test must not carry one. + const response = new Response("data:[DONE]\n\n"); + const events = await collect(createOpenAIChatAdapter(provider).parseStream(response)); + expect(events.at(-1)?.type).toBe("done"); + expect(events.some(e => e.type === "error")).toBe(false); + }); + + test("a bare data: line is ignored, not reported as a malformed frame", async () => { const response = new Response([ - 'data:{"choices":[{"delta":{"content":"hi"}}]}\n\n', - "data:[DONE]\n\n", + "data:\n\n", + 'data:{"choices":[{"delta":{"content":"hi"},"finish_reason":"stop"}]}\n\n', ].join("")); const events = await collect(createOpenAIChatAdapter(provider).parseStream(response)); + expect(events.find(e => e.type === "text_delta")).toMatchObject({ type: "text_delta", text: "hi" }); expect(events.at(-1)?.type).toBe("done"); + expect(events.some(e => e.type === "error")).toBe(false); }); test("still handles the spaced form identically", async () => {