From b13dd820cc842446f0add1384ca687698c840766 Mon Sep 17 00:00:00 2001 From: Marcello Fitton <106866560+angelplusultra@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:42:02 -0800 Subject: [PATCH] fix: resolve Gemini agent 400 error on tool call responses (#5054) * add gtc__ prefix to tool call names in Gemini agent message formatting * resolve Gemini agent 400 error on tool call responses * add comments explaining geminis thought signatures --------- Co-authored-by: Timothy Carambat --- .../utils/agents/aibitat/providers/gemini.js | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/server/utils/agents/aibitat/providers/gemini.js b/server/utils/agents/aibitat/providers/gemini.js index 53d3113a..9b43d49f 100644 --- a/server/utils/agents/aibitat/providers/gemini.js +++ b/server/utils/agents/aibitat/providers/gemini.js @@ -83,6 +83,10 @@ class GeminiProvider extends Provider { * Format the messages to the Gemini API Responses format. * - Gemini has some loosely documented format for tool calls and it can change at any time. * - We need to map the function call to the correct id and Gemini will throw an error if it does not. + * - Gemini requires a `thought_signature` (via `extra_content.google.thought_signature`) on function call + * parts in multi-turn tool conversations. This is an encrypted token Gemini attaches to every tool call + * it makes, and it must be passed back when sending tool results or Gemini rejects the request with a 400. + * See: https://ai.google.dev/gemini-api/docs/thought-signatures * @param {any[]} messages - The messages to format. * @returns {OpenAI.OpenAI.Responses.ResponseInput[]} The formatted messages. */ @@ -101,17 +105,25 @@ class GeminiProvider extends Provider { return; } + const prefixedName = this.prefixToolCall( + message.originalFunctionCall.name, + "add" + ); formattedMessages.push( { role: "assistant", + content: "", tool_calls: [ { type: "function", + ...(message.originalFunctionCall.extra_content + ? { extra_content: message.originalFunctionCall.extra_content } + : {}), function: { arguments: JSON.stringify( message.originalFunctionCall.arguments ), - name: message.originalFunctionCall.name, + name: prefixedName, }, id: message.originalFunctionCall.id, }, @@ -120,6 +132,7 @@ class GeminiProvider extends Provider { { role: "tool", tool_call_id: message.originalFunctionCall.id, + name: prefixedName, content: message.content, } ); @@ -188,6 +201,8 @@ class GeminiProvider extends Provider { name: this.prefixToolCall(toolCall.function.name, "strip"), call_id: toolCall.id, arguments: toolCall.function.arguments, + // Preserve Gemini's thought_signature so it can be passed back in #formatMessages + extra_content: toolCall.extra_content ?? null, }; eventHandler?.("reportStreamEvent", { type: "toolCallInvocation", @@ -208,6 +223,7 @@ class GeminiProvider extends Provider { id: completion.functionCall.call_id, name: completion.functionCall.name, arguments: completion.functionCall.arguments, + extra_content: completion.functionCall.extra_content, }, cost: this.getCost(), }; @@ -265,6 +281,8 @@ class GeminiProvider extends Provider { name: this.prefixToolCall(toolCall.function.name, "strip"), arguments: functionArgs, id: toolCall.id, + // Preserve Gemini's thought_signature so it can be passed back in #formatMessages + extra_content: toolCall.extra_content ?? null, }, cost, };