From a1edde420ef4b6202ffb0a1d5778c4116e824ec5 Mon Sep 17 00:00:00 2001 From: Aman <1402357+Zartharas@users.noreply.github.com> Date: Wed, 5 Aug 2026 20:52:21 -0600 Subject: [PATCH] fix(stream): preserve standalone whitespace deltas (#9189) Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc) --- open-sse/utils/reasoningPlaceholder.ts | 2 + .../unit/reasoning-placeholder-strip.test.ts | 12 +- ...esponses-to-claude-whitespace-9170.test.ts | 165 ++++++++++++++++++ 3 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 tests/unit/responses-to-claude-whitespace-9170.test.ts diff --git a/open-sse/utils/reasoningPlaceholder.ts b/open-sse/utils/reasoningPlaceholder.ts index 915af48c92..4e0ab3646c 100644 --- a/open-sse/utils/reasoningPlaceholder.ts +++ b/open-sse/utils/reasoningPlaceholder.ts @@ -21,6 +21,8 @@ export function isInternalReasoningPlaceholder(value: unknown): boolean { * real content, or streamed deltas glue together with their spaces eaten. */ export function stripInternalReasoningPlaceholder(value: string): string { + if (!value.includes(NON_ANTHROPIC_THINKING_PLACEHOLDER)) return value; + const stripped = value.replaceAll(NON_ANTHROPIC_THINKING_PLACEHOLDER, ""); return stripped.trim() === "" ? "" : stripped; } diff --git a/tests/unit/reasoning-placeholder-strip.test.ts b/tests/unit/reasoning-placeholder-strip.test.ts index b9fab87593..fc3f3a3e8b 100644 --- a/tests/unit/reasoning-placeholder-strip.test.ts +++ b/tests/unit/reasoning-placeholder-strip.test.ts @@ -46,10 +46,20 @@ test("a chunk with the placeholder mixed into real text strips it (trim only aff // only strips the string's own leading/trailing whitespace, not internal gaps. assert.equal( stripInternalReasoningPlaceholder(`foo ${NON_ANTHROPIC_THINKING_PLACEHOLDER} bar`), - "foo bar", + "foo bar" ); }); +test("standalone whitespace-only chunks pass through byte-for-byte when no placeholder is present", () => { + for (const chunk of [" ", "\t", "\n", "\n\n", "\r\n"]) { + assert.equal( + stripInternalReasoningPlaceholder(chunk), + chunk, + `expected ${JSON.stringify(chunk)} to remain unchanged` + ); + } +}); + test("an empty string stays empty", () => { assert.equal(stripInternalReasoningPlaceholder(""), ""); }); diff --git a/tests/unit/responses-to-claude-whitespace-9170.test.ts b/tests/unit/responses-to-claude-whitespace-9170.test.ts new file mode 100644 index 0000000000..b6e5925476 --- /dev/null +++ b/tests/unit/responses-to-claude-whitespace-9170.test.ts @@ -0,0 +1,165 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { createSSETransformStreamWithLogger } from "@omniroute/open-sse/utils/stream.ts"; +import { FORMATS } from "@omniroute/open-sse/translator/formats.ts"; + +function sse(type: string, payload: Record): string { + return `event: ${type}\ndata: ${JSON.stringify({ type, ...payload })}\n\n`; +} + +async function runClaudeFromCodex(rawSse: string): Promise { + const transform = createSSETransformStreamWithLogger( + FORMATS.OPENAI_RESPONSES, + FORMATS.CLAUDE, + "codex", + null, + null, + "gpt-5.5-high", + "conn-9170", + { model: "gpt-5.5-high" }, + null, + null, + null + ); + + const writer = transform.writable.getWriter(); + const reader = transform.readable.getReader(); + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + + const readAll = (async () => { + const output: string[] = []; + + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + output.push(decoder.decode(value)); + } + + return output.join(""); + })(); + + // Deliberately split the wire stream into single-byte writes to exercise + // the same buffering and SSE reconstruction used by the live path. + for (let index = 0; index < rawSse.length; index += 1) { + await writer.write(encoder.encode(rawSse.slice(index, index + 1))); + } + + await writer.close(); + + const rawClaudeSse = await readAll; + let content = ""; + + for (const line of rawClaudeSse.split("\n")) { + if (!line.startsWith("data:")) continue; + + const payload = line.slice(5).trim(); + if (!payload || payload === "[DONE]") continue; + + try { + const event = JSON.parse(payload) as { + type?: string; + delta?: { + type?: string; + text?: string; + }; + }; + + if (event.type === "content_block_delta" && event.delta?.type === "text_delta") { + content += event.delta.text ?? ""; + } + } catch { + // Ignore metadata comments and non-JSON SSE lines. + } + } + + return content; +} + +test("#9170 Responses-to-Claude streaming preserves standalone whitespace deltas", async () => { + const deltas = [ + "cleanup", + "\n\n", + "### Context", + "\n", + "ADR-R08", + "\n\n", + "```text", + "\n", + "ironbox://worker/", + "\n", + "```", + "\n\n", + "contain", + " ", + "1–253 bytes", + ]; + + const expected = deltas.join(""); + let sequenceNumber = 0; + + const rawSse = [ + sse("response.created", { + sequence_number: sequenceNumber++, + response: { + id: "resp_9170", + object: "response", + model: "gpt-5.5-high", + status: "in_progress", + output: [], + }, + }), + sse("response.output_item.added", { + sequence_number: sequenceNumber++, + output_index: 0, + item: { + id: "msg_9170", + type: "message", + role: "assistant", + content: [], + }, + }), + ...deltas.map((delta) => + sse("response.output_text.delta", { + sequence_number: sequenceNumber++, + item_id: "msg_9170", + output_index: 0, + content_index: 0, + delta, + }) + ), + sse("response.output_item.done", { + sequence_number: sequenceNumber++, + output_index: 0, + item: { + id: "msg_9170", + type: "message", + role: "assistant", + content: [{ type: "output_text", text: expected }], + }, + }), + sse("response.completed", { + sequence_number: sequenceNumber++, + response: { + id: "resp_9170", + object: "response", + model: "gpt-5.5-high", + status: "completed", + output: [], + usage: { + input_tokens: 10, + output_tokens: 20, + total_tokens: 30, + }, + }, + }), + ].join(""); + + const actual = await runClaudeFromCodex(rawSse); + + assert.equal(actual, expected); + assert.match(actual, /cleanup\n\n### Context\nADR-R08/); + assert.match(actual, /```text\nironbox:\/\/worker\/\n```/); + assert.match(actual, /contain 1–253 bytes$/); +});