Files
diegosouzapw--omniroute/tests/unit/responses-transformer.test.ts
2026-07-13 13:39:12 +08:00

381 lines
15 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const { createResponsesApiTransformStream, createResponsesLogger } =
await import("../../open-sse/transformer/responsesTransformer.ts");
const encoder = new TextEncoder();
const decoder = new TextDecoder();
async function runTransformStream(chunks, logger = null) {
const stream = createResponsesApiTransformStream(logger);
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();
const output = [];
const readerTask = (async () => {
while (true) {
const { value, done } = await reader.read();
if (done) break;
output.push(decoder.decode(value));
}
})();
for (const chunk of chunks) {
await writer.write(encoder.encode(chunk));
}
await writer.close();
await readerTask;
return output.join("");
}
function parseSseOutput(output) {
return output
.trim()
.split("\n\n")
.map((entry) => {
const lines = entry.split("\n");
const eventLine = lines.find((line) => line.startsWith("event: "));
const dataLine = lines.find((line) => line.startsWith("data: "));
return {
event: eventLine ? eventLine.slice("event: ".length) : null,
data: dataLine ? dataLine.slice("data: ".length) : null,
};
});
}
test("createResponsesApiTransformStream converts plain chat deltas into Responses API events", async () => {
const output = await runTransformStream([
'data: {"id":"chatcmpl_1","choices":[{"index":0,"delta":{"content":"Hel"}}]}\n\n',
'data: {"choices":[{"index":0,"delta":{"content":"lo"}}]}\n\n',
'data: {"usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3}}\n\n',
'data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n',
]);
const events = parseSseOutput(output);
const types = events.map((event) => event.event || event.data);
const deltas = events
.filter((event) => event.event === "response.output_text.delta")
.map((event) => JSON.parse(event.data));
const completed = JSON.parse(
events.find((event) => event.event === "response.completed").data
).response;
const doneMarker = events.at(-1);
assert.deepEqual(
deltas.map((delta) => delta.delta),
["Hel", "lo"]
);
assert.ok(types.includes("response.created"));
assert.ok(types.includes("response.in_progress"));
assert.ok(types.includes("response.output_item.added"));
assert.ok(types.includes("response.output_text.done"));
assert.equal(completed.output[0].content[0].text, "Hello");
assert.deepEqual(completed.usage, {
prompt_tokens: 1,
completion_tokens: 2,
total_tokens: 3,
});
assert.equal(doneMarker.data, "[DONE]");
});
test("createResponsesApiTransformStream preserves prompt-format think tags by default", async () => {
const output = await runTransformStream([
'data: {"id":"chatcmpl_1","model":"gpt-4.1","choices":[{"index":0,"delta":{"content":"<think>plan"}}]}\n\n',
'data: {"choices":[{"index":0,"delta":{"content":"ning</think>answer"}}]}\n\n',
'data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n',
]);
const events = parseSseOutput(output);
const reasoningDeltas = events
.filter((event) => event.event === "response.reasoning_summary_text.delta")
.map((event) => JSON.parse(event.data).delta);
const completed = JSON.parse(
events.find((event) => event.event === "response.completed").data
).response;
assert.deepEqual(reasoningDeltas, []);
assert.deepEqual(completed.output[0].content, [
{ type: "output_text", annotations: [], logprobs: [], text: "<think>planning</think>answer" },
]);
});
test("createResponsesApiTransformStream extracts think tags for tag-native models", async () => {
const output = await runTransformStream([
'data: {"id":"chatcmpl_1","model":"deepseek-ai/DeepSeek-R1","choices":[{"index":0,"delta":{"content":"<think>plan"}}]}\n\n',
'data: {"choices":[{"index":0,"delta":{"content":"ning</think>answer"}}]}\n\n',
'data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n',
]);
const events = parseSseOutput(output);
const reasoningDeltas = events
.filter((event) => event.event === "response.reasoning_summary_text.delta")
.map((event) => JSON.parse(event.data).delta);
const completed = JSON.parse(
events.find((event) => event.event === "response.completed").data
).response;
assert.deepEqual(reasoningDeltas, ["plan", "ning"]);
assert.equal(completed.output[0].type, "reasoning");
assert.deepEqual(completed.output[1].content, [
{ type: "output_text", annotations: [], logprobs: [], text: "answer" },
]);
});
test("createResponsesApiTransformStream handles native reasoning content and tool call index replacement", async () => {
const output = await runTransformStream([
'data: {"choices":[{"index":0,"delta":{"reasoning_content":"draft "}}]}\n\n',
'data: {"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"name":"search","arguments":"{\\"q\\":\\"hel"}}]}}]}\n\n',
'data: {"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"arguments":"lo\\"}"}}]}}]}\n\n',
'data: {"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_2","function":{"name":"lookup","arguments":"{}"}}]}}]}\n\n',
]);
const events = parseSseOutput(output);
const addedCalls = events
.filter((event) => event.event === "response.output_item.added")
.map((event) => JSON.parse(event.data).item)
.filter((item) => item.type === "function_call");
const doneCalls = events
.filter((event) => event.event === "response.output_item.done")
.map((event) => JSON.parse(event.data).item)
.filter((item) => item.type === "function_call");
const completed = JSON.parse(
events.find((event) => event.event === "response.completed").data
).response;
assert.deepEqual(
addedCalls.map((item) => ({ id: item.id, call_id: item.call_id, name: item.name })),
[
{ id: "fc_call_1", call_id: "call_1", name: "search" },
{ id: "fc_call_2", call_id: "call_2", name: "lookup" },
]
);
assert.deepEqual(
doneCalls.map((item) => ({ id: item.id, call_id: item.call_id, name: item.name })),
[
{ id: "fc_call_1", call_id: "call_1", name: "search" },
{ id: "fc_call_2", call_id: "call_2", name: "lookup" },
]
);
assert.equal(completed.output[0].type, "reasoning");
assert.deepEqual(
completed.output
.filter((item) => item.type === "function_call")
.map((item) => ({
id: item.id,
call_id: item.call_id,
name: item.name,
arguments: item.arguments,
})),
[{ id: "fc_call_2", call_id: "call_2", name: "lookup", arguments: "{}" }]
);
});
test("createResponsesLogger persists input and output event logs on flush", async () => {
const logsDir = mkdtempSync(join(tmpdir(), "responses-transformer-"));
const logger = createResponsesLogger("gpt-4o", logsDir);
assert.ok(logger);
const output = await runTransformStream(
['data: {"choices":[{"index":0,"delta":{"content":"hi"},"finish_reason":"stop"}]}\n\n'],
logger
);
const logRoot = join(logsDir, "logs");
const [sessionDir] = readdirSync(logRoot);
const inputLog = readFileSync(join(logRoot, sessionDir, "1_input_stream.txt"), "utf8");
const outputLog = readFileSync(join(logRoot, sessionDir, "2_output_stream.txt"), "utf8");
assert.match(sessionDir, /^responses_gpt-4o_/);
assert.match(inputLog, /"content":"hi"/);
assert.match(outputLog, /response\.completed/);
assert.match(output, /data: \[DONE]/);
});
test("createResponsesApiTransformStream ignores malformed events and preserves usage-only chunks", async () => {
const output = await runTransformStream([
"event: ping\n\n",
"data: [DONE]\n\n",
'data: {"usage":{"prompt_tokens":2,"completion_tokens":1,"total_tokens":3}}\n\n',
"data: {not-json}\n\n",
'data: {"id":"chatcmpl_edge","choices":[{"index":0,"delta":{"content":"ok"},"finish_reason":"stop"}]}\n\n',
]);
const events = parseSseOutput(output);
const completed = JSON.parse(
events.find((event) => event.event === "response.completed").data
).response;
assert.equal(completed.id, "resp_chatcmpl_edge");
assert.equal(completed.output[0].content[0].text, "ok");
assert.deepEqual(completed.usage, {
prompt_tokens: 2,
completion_tokens: 1,
total_tokens: 3,
});
});
test("createResponsesLogger returns null for invalid base paths and swallows flush write failures", () => {
const blockedPath = join(tmpdir(), `responses-transformer-blocked-${Date.now()}`);
writeFileSync(blockedPath, "blocked");
try {
const blockedLogger = createResponsesLogger("gpt-4o", blockedPath);
assert.equal(blockedLogger, null);
} finally {
unlinkSync(blockedPath);
}
const logsDir = mkdtempSync(join(tmpdir(), "responses-transformer-broken-"));
const logger = createResponsesLogger("gpt-4o", logsDir);
const capturedLogs = [];
const originalConsoleLog = console.log;
logger.logInput("input");
logger.logOutput("output");
const sessionDir = readdirSync(join(logsDir, "logs"))[0];
rmSync(join(logsDir, "logs", sessionDir), { recursive: true, force: true });
console.log = (...args) => capturedLogs.push(args.join(" "));
try {
logger.flush();
} finally {
console.log = originalConsoleLog;
}
assert.ok(capturedLogs.some((entry) => entry.includes("[RESPONSES] Failed to write logs:")));
});
test("createResponsesApiTransformStream deduplicates repeated tool argument snapshots", async () => {
const args = JSON.stringify({ command: "find /tmp -name test.txt" });
const output = await runTransformStream([
`data: {"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"name":"shell","arguments":${JSON.stringify(args)}}}]}}]}\n\n`,
`data: {"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":${JSON.stringify(args)}}]},"finish_reason":"tool_calls"}]}\n\n`,
]);
const events = parseSseOutput(output);
const completed = JSON.parse(
events.find((event) => event.event === "response.completed").data
).response;
const toolCall = completed.output.find((item) => item.type === "function_call");
assert.equal(toolCall.arguments, args);
assert.equal(JSON.parse(toolCall.arguments).command, "find /tmp -name test.txt");
// The streamed deltas must also reconstruct the arguments exactly once — a
// duplicated snapshot must not be re-emitted to the client.
const streamedArgs = events
.filter((event) => event.event === "response.function_call_arguments.delta")
.map((event) => JSON.parse(event.data).delta)
.join("");
assert.equal(streamedArgs, args);
});
test("createResponsesApiTransformStream concatenates incremental tool argument fragments without dropping repeated chars", async () => {
// Real providers stream `function.arguments` as small incremental fragments.
// A doubled char straddling a fragment boundary ("l" + "l -l") must survive
// — the previous fuzzy-dedup heuristic silently turned `ll -l` into `l -l`.
const output = await runTransformStream([
`data: {"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"name":"shell","arguments":"{\\"cmd\\":\\"l"}}]}}]}\n\n`,
`data: {"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"l -l\\"}"}}]},"finish_reason":"tool_calls"}]}\n\n`,
]);
const events = parseSseOutput(output);
const completed = JSON.parse(
events.find((event) => event.event === "response.completed").data
).response;
const toolCall = completed.output.find((item) => item.type === "function_call");
assert.equal(toolCall.arguments, '{"cmd":"ll -l"}');
assert.equal(JSON.parse(toolCall.arguments).cmd, "ll -l");
const streamedArgs = events
.filter((event) => event.event === "response.function_call_arguments.delta")
.map((event) => JSON.parse(event.data).delta)
.join("");
assert.equal(streamedArgs, '{"cmd":"ll -l"}');
});
test("createResponsesApiTransformStream clears the keepalive timer when the stream is cancelled (no timer leak)", async () => {
// Regression: the 3s keepalive interval used to be cleared ONLY in flush(), which
// does not run when the client disconnects mid-stream. The orphaned interval then
// fired (and threw on the closed controller) forever, leaking one live timer per
// aborted /v1/responses stream and burning CPU as they accumulated. Verify the timer
// is cleared when the readable side is cancelled.
const realSetInterval = globalThis.setInterval;
const realClearInterval = globalThis.clearInterval;
const live = new Set();
globalThis.setInterval = function (handler, timeout, ...args) {
const id = realSetInterval(handler, timeout, ...args);
live.add(id);
return id;
};
globalThis.clearInterval = function (id) {
live.delete(id);
return realClearInterval(id);
};
try {
const stream = createResponsesApiTransformStream(null, 10);
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();
// start() runs on construction and creates exactly one keepalive interval.
assert.equal(live.size, 1, "keepalive interval should be active while streaming");
await writer.write(
encoder.encode('data: {"choices":[{"index":0,"delta":{"content":"hi"}}]}\n\n')
);
await reader.read();
// Simulate a client disconnect mid-stream.
await reader.cancel();
assert.equal(live.size, 0, "keepalive interval must be cleared when the stream is cancelled");
} finally {
globalThis.setInterval = realSetInterval;
globalThis.clearInterval = realClearInterval;
}
});
test("createResponsesApiTransformStream keepalive self-clears when enqueue fails on a torn-down controller", async () => {
// Backstop for transports where neither flush() nor cancel() runs: the keepalive
// callback must clear its own interval the first time enqueue() throws, instead of
// re-throwing on every tick forever.
const realSetInterval = globalThis.setInterval;
const realClearInterval = globalThis.clearInterval;
let capturedCallback = null;
let capturedId = null;
let cleared = false;
globalThis.setInterval = function (handler, timeout, ...args) {
capturedCallback = handler;
capturedId = realSetInterval(() => {}, 1 << 30, ...args); // inert real timer as the id
return capturedId;
};
globalThis.clearInterval = function (id) {
if (id === capturedId) cleared = true;
return realClearInterval(id);
};
try {
const stream = createResponsesApiTransformStream(null, 10);
// Error the readable side so the controller can no longer accept enqueues.
await stream.readable.cancel();
assert.equal(typeof capturedCallback, "function", "keepalive callback should be captured");
// Manually invoke the keepalive tick: enqueue() will throw on the torn-down
// controller, and the callback must clear its own interval rather than rethrow.
assert.doesNotThrow(() => capturedCallback());
assert.equal(cleared, true, "keepalive interval should self-clear after a failed enqueue");
} finally {
globalThis.setInterval = realSetInterval;
globalThis.clearInterval = realClearInterval;
}
});