mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-25 08:32:11 +03:00
fix(translator): preserve Claude tool call state
This commit is contained in:
committed by
Markus Hartung
parent
2544ee9498
commit
13e29f2f39
@@ -1155,7 +1155,12 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
|
||||
// Keyed by index, not insertion order — readers that need call order for
|
||||
// parallel calls closed out of order should sort by this key rather than
|
||||
// relying on Map iteration order.
|
||||
// Responses→Claude uses this same shared map for Claude block lifecycle
|
||||
// state. Preserve those fields when adding the completed-call summary;
|
||||
// replacing the entry makes the arguments chunk look like a new unnamed
|
||||
// tool and emits a duplicate empty content_block_start.
|
||||
state.toolCalls.set(currentIndex, {
|
||||
...state.toolCalls.get(currentIndex),
|
||||
id: callId,
|
||||
index: currentIndex,
|
||||
type: "function",
|
||||
|
||||
133
tests/unit/codex-claude-empty-tool-use.test.ts
Normal file
133
tests/unit/codex-claude-empty-tool-use.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { FORMATS } from "../../open-sse/translator/formats.ts";
|
||||
import { createSSETransformStreamWithLogger } from "../../open-sse/utils/stream.ts";
|
||||
|
||||
function sse(type: string, data: Record<string, unknown>): string {
|
||||
return `event: ${type}\ndata: ${JSON.stringify({ type, ...data })}\n\n`;
|
||||
}
|
||||
|
||||
async function translateCodexToolCall(rawSse: string): Promise<Record<string, unknown>[]> {
|
||||
const transform = createSSETransformStreamWithLogger(
|
||||
FORMATS.OPENAI_RESPONSES,
|
||||
FORMATS.CLAUDE,
|
||||
"codex",
|
||||
null,
|
||||
null,
|
||||
"gpt-5.6-sol",
|
||||
"connection-codex-tool",
|
||||
{ model: "gpt-5.6-sol", stream: true },
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
const writer = transform.writable.getWriter();
|
||||
const reader = transform.readable.getReader();
|
||||
const readAll = (async () => {
|
||||
const decoder = new TextDecoder();
|
||||
let output = "";
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
output += decoder.decode(value, { stream: true });
|
||||
}
|
||||
output += decoder.decode();
|
||||
return output;
|
||||
})();
|
||||
|
||||
await writer.write(new TextEncoder().encode(rawSse));
|
||||
await writer.close();
|
||||
|
||||
return (await readAll)
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.startsWith("data:"))
|
||||
.map((line) => line.slice(5).trim())
|
||||
.filter((payload) => payload && payload !== "[DONE]")
|
||||
.map((payload) => JSON.parse(payload) as Record<string, unknown>);
|
||||
}
|
||||
|
||||
test("Codex Responses tool call emits exactly one named Claude tool_use block", async () => {
|
||||
const callId = "call_codex_claude_1";
|
||||
const itemId = "fc_codex_claude_1";
|
||||
const raw = [
|
||||
sse("response.created", {
|
||||
sequence_number: 0,
|
||||
response: { id: "resp_codex_claude_1", status: "in_progress", model: "gpt-5.6-sol" },
|
||||
}),
|
||||
sse("response.output_item.added", {
|
||||
sequence_number: 1,
|
||||
output_index: 0,
|
||||
item: {
|
||||
id: itemId,
|
||||
type: "function_call",
|
||||
call_id: callId,
|
||||
name: "check_status",
|
||||
arguments: "",
|
||||
status: "in_progress",
|
||||
},
|
||||
}),
|
||||
sse("response.function_call_arguments.delta", {
|
||||
sequence_number: 2,
|
||||
item_id: itemId,
|
||||
output_index: 0,
|
||||
delta: '{"value":"ok"}',
|
||||
}),
|
||||
sse("response.function_call_arguments.done", {
|
||||
sequence_number: 3,
|
||||
item_id: itemId,
|
||||
output_index: 0,
|
||||
arguments: '{"value":"ok"}',
|
||||
}),
|
||||
sse("response.output_item.done", {
|
||||
sequence_number: 4,
|
||||
output_index: 0,
|
||||
item: {
|
||||
id: itemId,
|
||||
type: "function_call",
|
||||
call_id: callId,
|
||||
name: "check_status",
|
||||
arguments: '{"value":"ok"}',
|
||||
status: "completed",
|
||||
},
|
||||
}),
|
||||
sse("response.completed", {
|
||||
sequence_number: 5,
|
||||
response: {
|
||||
id: "resp_codex_claude_1",
|
||||
status: "completed",
|
||||
model: "gpt-5.6-sol",
|
||||
output: [
|
||||
{
|
||||
id: itemId,
|
||||
type: "function_call",
|
||||
call_id: callId,
|
||||
name: "check_status",
|
||||
arguments: '{"value":"ok"}',
|
||||
status: "completed",
|
||||
},
|
||||
],
|
||||
usage: { input_tokens: 10, output_tokens: 5, total_tokens: 15 },
|
||||
},
|
||||
}),
|
||||
].join("");
|
||||
|
||||
const events = await translateCodexToolCall(raw);
|
||||
const starts = events.filter((event) => event.type === "content_block_start") as Array<{
|
||||
index?: number;
|
||||
content_block?: { type?: string; id?: string; name?: string };
|
||||
}>;
|
||||
const toolStarts = starts.filter((event) => event.content_block?.type === "tool_use");
|
||||
|
||||
assert.equal(toolStarts.length, 1, "must not append a duplicate empty tool_use block");
|
||||
assert.deepEqual(toolStarts[0], {
|
||||
type: "content_block_start",
|
||||
index: 0,
|
||||
content_block: {
|
||||
type: "tool_use",
|
||||
id: callId,
|
||||
name: "check_status",
|
||||
input: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user