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 () => {