mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(translator): prevent doubled tool args in OpenAI-to-Claude (#5828)
Integrated into release/v3.8.43
This commit is contained in:
committed by
GitHub
parent
08dbf6f58a
commit
8bba88cd30
@@ -110,6 +110,8 @@
|
||||
|
||||
- **fix(kiro):** stop injecting a placeholder user turn on trailing tool-result turns so agentic loops aren't disrupted. (thanks @jetmiky)
|
||||
|
||||
- **fix(translator):** prevent doubled tool arguments in OpenAI-to-Claude responses (duplicate finish_reason guard + string tool-input passthrough). (thanks @vishalrajv)
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
---
|
||||
|
||||
@@ -410,7 +410,10 @@ function convertClaudeMessage(msg, preserveCacheControl = false) {
|
||||
type: "function",
|
||||
function: {
|
||||
name: block.name,
|
||||
arguments: JSON.stringify(block.input || {}),
|
||||
arguments:
|
||||
typeof block.input === "string"
|
||||
? block.input
|
||||
: JSON.stringify(block.input || {}),
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -225,8 +225,8 @@ export function openaiToClaudeResponse(chunk, state) {
|
||||
}
|
||||
}
|
||||
|
||||
// Finish
|
||||
if (choice.finish_reason) {
|
||||
// Finish — guard against duplicate finish_reason chunks (common with OpenAI-compatible models)
|
||||
if (choice.finish_reason && !state.finishReason) {
|
||||
stopThinkingBlock(state, results);
|
||||
stopTextBlock(state, results);
|
||||
|
||||
|
||||
@@ -259,6 +259,42 @@ test("Claude -> OpenAI turns thinking and tool_use blocks into assistant tool_ca
|
||||
});
|
||||
});
|
||||
|
||||
test("Claude -> OpenAI passes pre-stringified tool_use input through verbatim (no double-encoding)", () => {
|
||||
// #2279: some upstream Claude sources emit tool_use.input already JSON-encoded
|
||||
// as a string. Re-running JSON.stringify on it would double-encode the
|
||||
// payload (wrapping it in an extra pair of quotes with escaped internals).
|
||||
const result = claudeToOpenAIRequest(
|
||||
"gpt-4o",
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "tu_2",
|
||||
name: "read_file",
|
||||
input: '{"path":"/tmp/demo"}',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
assert.deepEqual(result.messages[0].tool_calls, [
|
||||
{
|
||||
id: "tu_2",
|
||||
type: "function",
|
||||
function: {
|
||||
name: "read_file",
|
||||
arguments: '{"path":"/tmp/demo"}',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("Claude -> OpenAI converts tool_result blocks into tool messages and preserves trailing user text", () => {
|
||||
const result = claudeToOpenAIRequest(
|
||||
"gpt-4o",
|
||||
|
||||
@@ -148,6 +148,53 @@ test("OpenAI stream: tool calls strip Claude OAuth prefix and keep cache usage",
|
||||
assert.equal(result[5].usage.cache_creation_input_tokens, 1);
|
||||
});
|
||||
|
||||
test("OpenAI stream: two finish_reason chunks emit finish events exactly once", () => {
|
||||
// #2279: some OpenAI-compatible upstreams resend a terminal chunk that still
|
||||
// carries finish_reason (e.g. an empty-delta echo of the finish chunk). Without
|
||||
// a guard, the whole finish block (content_block_stop / message_delta /
|
||||
// message_stop) re-runs and duplicates those events downstream.
|
||||
const state = createState();
|
||||
const first = openaiToClaudeResponse(
|
||||
{
|
||||
id: "chatcmpl-4",
|
||||
model: "gpt-4.1",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: "call_1",
|
||||
type: "function",
|
||||
function: { name: "read_file", arguments: '{"path":"/tmp/a"}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: "tool_calls",
|
||||
},
|
||||
],
|
||||
usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
|
||||
},
|
||||
state
|
||||
);
|
||||
// Duplicate terminal chunk: empty delta, same finish_reason.
|
||||
const second = openaiToClaudeResponse(
|
||||
{
|
||||
id: "chatcmpl-4",
|
||||
model: "gpt-4.1",
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }],
|
||||
usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
|
||||
},
|
||||
state
|
||||
);
|
||||
const result = flatten([first, second]);
|
||||
|
||||
assert.equal(result.filter((e) => e.type === "content_block_stop").length, 1);
|
||||
assert.equal(result.filter((e) => e.type === "message_delta").length, 1);
|
||||
assert.equal(result.filter((e) => e.type === "message_stop").length, 1);
|
||||
});
|
||||
|
||||
test("OpenAI non-stream: chat completion becomes Claude message with thinking and tool_use", () => {
|
||||
const result = translateNonStreamingResponse(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user