mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 23:32:12 +03:00
fix(stream): preserve standalone whitespace deltas (#9189)
Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(""), "");
|
||||
});
|
||||
|
||||
165
tests/unit/responses-to-claude-whitespace-9170.test.ts
Normal file
165
tests/unit/responses-to-claude-whitespace-9170.test.ts
Normal file
@@ -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, unknown>): string {
|
||||
return `event: ${type}\ndata: ${JSON.stringify({ type, ...payload })}\n\n`;
|
||||
}
|
||||
|
||||
async function runClaudeFromCodex(rawSse: string): Promise<string> {
|
||||
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/<worker-name>",
|
||||
"\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\/<worker-name>\n```/);
|
||||
assert.match(actual, /contain 1–253 bytes$/);
|
||||
});
|
||||
Reference in New Issue
Block a user