diff --git a/open-sse/translator/request/gemini-to-openai.ts b/open-sse/translator/request/gemini-to-openai.ts index b7f1d4b16d..2206b106f4 100644 --- a/open-sse/translator/request/gemini-to-openai.ts +++ b/open-sse/translator/request/gemini-to-openai.ts @@ -137,7 +137,7 @@ function convertGeminiContent(content) { if (part.functionCall) { toolCalls.push({ - id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + id: part.functionCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, type: "function", function: { name: part.functionCall.name, diff --git a/tests/unit/translator-gemini-to-openai.test.ts b/tests/unit/translator-gemini-to-openai.test.ts index a0902bb3ef..bac2aeda33 100644 --- a/tests/unit/translator-gemini-to-openai.test.ts +++ b/tests/unit/translator-gemini-to-openai.test.ts @@ -100,10 +100,7 @@ test("Gemini -> OpenAI maps a thought:true part to reasoning_content instead of contents: [ { role: "model", - parts: [ - { thought: true, text: "internal reasoning" }, - { text: "final answer" }, - ], + parts: [{ thought: true, text: "internal reasoning" }, { text: "final answer" }], }, ], }, @@ -116,9 +113,7 @@ test("Gemini -> OpenAI maps a thought:true part to reasoning_content instead of assert.equal(assistant.reasoning_content, "internal reasoning"); // The visible content must not contain the thought text. const visibleText = - typeof assistant.content === "string" - ? assistant.content - : JSON.stringify(assistant.content); + typeof assistant.content === "string" ? assistant.content : JSON.stringify(assistant.content); assert.doesNotMatch(visibleText, /internal reasoning/); assert.match(visibleText, /final answer/); }); @@ -172,3 +167,73 @@ test("Gemini -> OpenAI converts function responses into tool messages", () => { }, ]); }); + +test("Gemini -> OpenAI preserves functionCall id when present", () => { + const result = geminiToOpenAIRequest( + "gpt-4o", + { + contents: [ + { + role: "model", + parts: [ + { + functionCall: { + id: "call_custom_id_999", + name: "get_weather", + args: { city: "Tokyo" }, + }, + }, + ], + }, + ], + }, + false + ); + + assert.equal(result.messages.length, 1); + assert.equal(result.messages[0].role, "assistant"); + assert.equal(result.messages[0].tool_calls[0].id, "call_custom_id_999"); + assert.equal(result.messages[0].tool_calls[0].function.name, "get_weather"); +}); + +test("Gemini -> OpenAI maintains matching IDs across multi-turn tool call and response", () => { + const result = geminiToOpenAIRequest( + "gpt-4o", + { + contents: [ + { + role: "model", + parts: [ + { + functionCall: { + id: "call_calc_456", + name: "calculator", + args: { expr: "2 + 2" }, + }, + }, + ], + }, + { + role: "user", + parts: [ + { + functionResponse: { + id: "call_calc_456", + name: "calculator", + response: { result: 4 }, + }, + }, + ], + }, + ], + }, + false + ); + + assert.equal(result.messages.length, 2); + const assistantCallId = result.messages[0].tool_calls[0].id; + const toolResponseCallId = result.messages[1].tool_call_id; + assert.equal(assistantCallId, "call_calc_456"); + assert.equal(toolResponseCallId, "call_calc_456"); + assert.equal(assistantCallId, toolResponseCallId); +});