mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 02:02:13 +03:00
Closed #9183 in favor of this branch at the operator's request. Folds in the exact PR diff (verified byte-identical via `gh pr diff 9183`, applied cleanly with `git apply`): - Fixed output_index collisions between reasoning/message/tool-call items in Responses API streaming, which caused clients to ignore tool calls. - Fixed a race in chunk processing where tool calls and finish signals were lost when they arrived in the same chunk as reasoning/text content. - Enabled full processing of multi-choice chunks (previously truncated to a single choice). - Added reasoning_content capture/replay for DeepSeek-based models (big-pickle), keyed off the real position in the next turn's replayed messages array instead of a hardcoded index 0 — prevents history corruption that caused models to lose context and stop prematurely. - Added big-pickle to models recognized for native textual reasoning tags so <think> blocks convert to reasoning items correctly. - Improved Responses-to-Chat translation to group reasoning, content, and tool calls into a single assistant turn, as strict OpenAI-compatible upstreams require. - Added a descriptive placeholder for encrypted Responses API reasoning blocks so downgraded Chat API requests keep context. Test plan: - All 82 tests across reasoning-cache.test.ts, translator-helper-branches.test.ts, translator-request-openai-responses.test.ts, and the 3 new test files (responses-api-truncation.test.ts, responses-replay-fixes.test.ts, responses-request-translation.test.ts) pass - npm run typecheck:core — clean - npm run check:file-size — chatCore.ts (5024->5032) and translator/response/openai-responses.ts (1174->1180) rebaselined, both cohesive additions at the two reasoning-cache capture call sites and the turn-grouping fix; tests/unit/reasoning-cache.test.ts rebaselined at 1035 (crosses the 1000-line new-file test cap, entirely this fold's diff)
100 lines
3.6 KiB
TypeScript
100 lines
3.6 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { createResponsesApiTransformStream } =
|
|
await import("../../open-sse/transformer/responsesTransformer.ts");
|
|
const { openaiToOpenAIResponsesResponse } =
|
|
await import("../../open-sse/translator/response/openai-responses.ts");
|
|
const { initState } = await import("../../open-sse/translator/index.ts");
|
|
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
|
|
|
|
const encoder = new TextEncoder();
|
|
const decoder = new TextDecoder();
|
|
|
|
async function runTransformStream(chunks) {
|
|
const stream = createResponsesApiTransformStream(null, 3000, { parseTextualReasoningTags: true });
|
|
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("ResponsesTransformer: should not skip tool_calls when reasoning is present in same chunk", async () => {
|
|
const output = await runTransformStream([
|
|
'data: {"choices":[{"index":0,"delta":{"content":"<think>Thinking...</think>","tool_calls":[{"index":0,"id":"call_1","function":{"name":"test","arguments":"{}"}}]}}]}\n\n',
|
|
'data: {"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"arguments":"{}"}}]},"finish_reason":"tool_calls"}]}\n\n',
|
|
]);
|
|
|
|
const events = parseSseOutput(output);
|
|
const toolCallAdded = events.find(
|
|
(e) =>
|
|
e.event === "response.output_item.added" && JSON.parse(e.data).item.type === "function_call"
|
|
);
|
|
assert.ok(toolCallAdded, "Tool call should be added even if reasoning is in the same chunk");
|
|
});
|
|
|
|
test("ResponsesTransformer: should process all choices in a chunk", async () => {
|
|
const output = await runTransformStream([
|
|
'data: {"choices":[{"index":0,"delta":{"content":" "}},{"index":1,"delta":{"content":"Done"},"finish_reason":"stop"}]}\n\n',
|
|
]);
|
|
|
|
const events = parseSseOutput(output);
|
|
const item1Done = events.find(
|
|
(e) => e.event === "response.output_item.done" && JSON.parse(e.data).output_index === 1
|
|
);
|
|
assert.ok(item1Done, "Choice 1 should be processed even if Choice 0 triggered a skip/trim");
|
|
});
|
|
|
|
test("openaiToOpenAIResponsesResponse: should not skip tool_calls when reasoning is present in same chunk", () => {
|
|
const state = initState(FORMATS.OPENAI_RESPONSES);
|
|
const chunk = {
|
|
id: "1",
|
|
model: "deepseek-r1",
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
delta: {
|
|
content: "<think>Thinking...",
|
|
tool_calls: [{ index: 0, id: "call_1", function: { name: "test", arguments: "{}" } }],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const events = openaiToOpenAIResponsesResponse(chunk, state);
|
|
const toolCallAdded = events.find(
|
|
(e) => e.event === "response.output_item.added" && e.data.item.type === "function_call"
|
|
);
|
|
assert.ok(toolCallAdded, "Tool call should be added even if reasoning is in the same chunk");
|
|
});
|