mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 06:02:14 +03:00
fix(translator): remove thoughtSignature from functionCall parts in Gemini translation
HTTP 400 "invalid argument" was triggered when OmniRoute translated OpenAI tool_calls to Gemini format, because thoughtSignature was injected onto every functionCall part unconditionally. thoughtSignature is only valid on thinking/reasoning parts (those with thought: true). The Gemini API rejects any request where a functionCall part carries a thoughtSignature field, returning HTTP 400. Fix: remove the thoughtSignature field from functionCall parts. The thinking parts that legitimately require thoughtSignature (emitted when a message has reasoning_content) are unchanged. Adds regression test (T43) with three cases: - single tool call: no thoughtSignature on functionCall part - multiple tool calls: none carry thoughtSignature - thinking part regression guard: thoughtSignature still present on thought parts Fixes #725
This commit is contained in:
committed by
Diego Rodrigues de Sa e Souza
parent
fc24361aa6
commit
48182edbd5
@@ -167,8 +167,10 @@ function openaiToGeminiBase(model, body, stream) {
|
||||
if (tc.type !== "function") continue;
|
||||
|
||||
const args = tryParseJSON(tc.function?.arguments || "{}");
|
||||
// Do NOT include thoughtSignature on functionCall parts — it is only valid
|
||||
// on thinking/reasoning parts and causes HTTP 400 "invalid argument" from the
|
||||
// Gemini API when present on a functionCall part (#725).
|
||||
parts.push({
|
||||
thoughtSignature: DEFAULT_THINKING_GEMINI_SIGNATURE,
|
||||
functionCall: {
|
||||
id: tc.id,
|
||||
name: tc.function.name,
|
||||
|
||||
161
tests/unit/t43-gemini-tool-call-no-thought-signature.test.mjs
Normal file
161
tests/unit/t43-gemini-tool-call-no-thought-signature.test.mjs
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* T43: Gemini tool call parts must NOT include thoughtSignature.
|
||||
*
|
||||
* Regression test for HTTP 400 "invalid argument" when OmniRoute translates
|
||||
* OpenAI tool_calls to Gemini format. The thoughtSignature field is only valid
|
||||
* on thinking/reasoning parts — injecting it on functionCall parts causes the
|
||||
* Gemini API to reject the request with a 400 error.
|
||||
*
|
||||
* Reproduces: https://github.com/diegosouzapw/OmniRoute/issues/725
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { translateRequest } = await import("../../open-sse/translator/index.ts");
|
||||
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
|
||||
|
||||
function translateToGemini(messages, tools) {
|
||||
return translateRequest(FORMATS.OPENAI, FORMATS.GEMINI, "gemini-2.0-flash", {
|
||||
model: "gemini-2.0-flash",
|
||||
messages,
|
||||
tools,
|
||||
stream: false,
|
||||
});
|
||||
}
|
||||
|
||||
test("T43: functionCall parts must not contain thoughtSignature", () => {
|
||||
const messages = [
|
||||
{ role: "user", content: "What is the weather in Tokyo?" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_abc123",
|
||||
type: "function",
|
||||
function: { name: "get_weather", arguments: '{"location":"Tokyo"}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "tool",
|
||||
tool_call_id: "call_abc123",
|
||||
content: '{"temp": "15°C", "condition": "cloudy"}',
|
||||
},
|
||||
];
|
||||
|
||||
const tools = [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_weather",
|
||||
description: "Get weather for a location",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { location: { type: "string" } },
|
||||
required: ["location"],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = translateToGemini(messages, tools);
|
||||
|
||||
// Find the model turn that contains the functionCall
|
||||
const modelTurn = result.contents.find(
|
||||
(c) => c.role === "model" && c.parts?.some((p) => p.functionCall)
|
||||
);
|
||||
|
||||
assert.ok(modelTurn, "Expected a model turn with functionCall parts");
|
||||
|
||||
for (const part of modelTurn.parts) {
|
||||
if (part.functionCall) {
|
||||
assert.ok(
|
||||
!("thoughtSignature" in part),
|
||||
`functionCall part must not contain thoughtSignature — Gemini API returns HTTP 400 "invalid argument" when it does. Got: ${JSON.stringify(part)}`
|
||||
);
|
||||
assert.equal(part.functionCall.name, "get_weather");
|
||||
assert.deepEqual(part.functionCall.args, { location: "Tokyo" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("T43: multiple tool calls — none of the functionCall parts may have thoughtSignature", () => {
|
||||
const messages = [
|
||||
{ role: "user", content: "Get weather for Tokyo and London" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_001",
|
||||
type: "function",
|
||||
function: { name: "get_weather", arguments: '{"location":"Tokyo"}' },
|
||||
},
|
||||
{
|
||||
id: "call_002",
|
||||
type: "function",
|
||||
function: { name: "get_weather", arguments: '{"location":"London"}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "tool",
|
||||
tool_call_id: "call_001",
|
||||
content: '{"temp":"15°C"}',
|
||||
},
|
||||
{
|
||||
role: "tool",
|
||||
tool_call_id: "call_002",
|
||||
content: '{"temp":"10°C"}',
|
||||
},
|
||||
];
|
||||
|
||||
const result = translateToGemini(messages, []);
|
||||
|
||||
const modelTurn = result.contents.find(
|
||||
(c) => c.role === "model" && c.parts?.some((p) => p.functionCall)
|
||||
);
|
||||
assert.ok(modelTurn, "Expected a model turn with functionCall parts");
|
||||
|
||||
const functionCallParts = modelTurn.parts.filter((p) => p.functionCall);
|
||||
assert.equal(functionCallParts.length, 2, "Expected 2 functionCall parts");
|
||||
|
||||
for (const part of functionCallParts) {
|
||||
assert.ok(
|
||||
!("thoughtSignature" in part),
|
||||
`functionCall part must not contain thoughtSignature. Got: ${JSON.stringify(part)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("T43: thinking parts still include thoughtSignature (regression guard)", () => {
|
||||
// Ensure we did not accidentally break the thinking parts that legitimately
|
||||
// need thoughtSignature (present when msg.reasoning_content is set).
|
||||
const messages = [
|
||||
{ role: "user", content: "Think about the weather" },
|
||||
{
|
||||
role: "assistant",
|
||||
reasoning_content: "The user wants weather data.",
|
||||
content: "I'll check the weather.",
|
||||
tool_calls: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const result = translateToGemini(messages, []);
|
||||
|
||||
const modelTurn = result.contents.find((c) => c.role === "model");
|
||||
assert.ok(modelTurn, "Expected a model turn");
|
||||
|
||||
const thinkingPart = modelTurn.parts.find((p) => p.thought === true);
|
||||
assert.ok(thinkingPart, "Expected a thinking part when reasoning_content is set");
|
||||
assert.equal(thinkingPart.text, "The user wants weather data.");
|
||||
|
||||
const signaturePart = modelTurn.parts.find((p) => "thoughtSignature" in p);
|
||||
assert.ok(signaturePart, "Expected a thoughtSignature part after thinking part");
|
||||
assert.ok(
|
||||
!signaturePart.functionCall,
|
||||
"thoughtSignature part must not also be a functionCall part"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user