From b18f401d9e27c691b6b93f30380c90d45793b81f Mon Sep 17 00:00:00 2001 From: Roshan Singh Date: Thu, 15 Jan 2026 17:42:39 +0530 Subject: [PATCH] fix(ai): avoid unsigned Gemini 3 tool calls (#741) --- packages/ai/src/providers/google-shared.ts | 31 ++++++--- ...-shared-gemini3-unsigned-tool-call.test.ts | 69 +++++++++++++++++++ 2 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts diff --git a/packages/ai/src/providers/google-shared.ts b/packages/ai/src/providers/google-shared.ts index 93fe596c..deb920d4 100644 --- a/packages/ai/src/providers/google-shared.ts +++ b/packages/ai/src/providers/google-shared.ts @@ -131,18 +131,29 @@ export function convertMessages(model: Model, contex }); } } else if (block.type === "toolCall") { - const part: Part = { - functionCall: { - name: block.name, - args: block.arguments, - ...(requiresToolCallId(model.id) ? { id: block.id } : {}), - }, - }; const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thoughtSignature); - if (thoughtSignature) { - part.thoughtSignature = thoughtSignature; + // Gemini 3 requires thoughtSignature on all function calls when thinking mode is enabled. + // When replaying history from providers without thought signatures (e.g. Claude via Antigravity), + // convert unsigned function calls to text to avoid API validation errors. + const isGemini3 = model.id.toLowerCase().includes("gemini-3"); + if (isGemini3 && !thoughtSignature) { + const argsStr = JSON.stringify(block.arguments, null, 2); + parts.push({ + text: `[Tool Call: ${block.name}]\nArguments: ${argsStr}`, + }); + } else { + const part: Part = { + functionCall: { + name: block.name, + args: block.arguments, + ...(requiresToolCallId(model.id) ? { id: block.id } : {}), + }, + }; + if (thoughtSignature) { + part.thoughtSignature = thoughtSignature; + } + parts.push(part); } - parts.push(part); } } diff --git a/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts b/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts new file mode 100644 index 00000000..d0f4b43b --- /dev/null +++ b/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "vitest"; +import { convertMessages } from "../src/providers/google-shared.js"; +import type { Context, Model } from "../src/types.js"; + +describe("google-shared convertMessages", () => { + it("converts unsigned tool calls to text for Gemini 3", () => { + const model: Model<"google-generative-ai"> = { + id: "gemini-3-pro-preview", + name: "Gemini 3 Pro Preview", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 8192, + }; + + const now = Date.now(); + const context: Context = { + messages: [ + { role: "user", content: "Hi", timestamp: now }, + { + role: "assistant", + content: [ + { + type: "toolCall", + id: "call_1", + name: "bash", + arguments: { command: "ls -la" }, + // No thoughtSignature: simulates Claude via Antigravity. + }, + ], + api: "google-gemini-cli", + provider: "google-antigravity", + model: "claude-sonnet-4-20250514", + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: now, + }, + ], + }; + + const contents = convertMessages(model, context); + + let toolTurn: (typeof contents)[number] | undefined; + for (let i = contents.length - 1; i >= 0; i -= 1) { + if (contents[i]?.role === "model") { + toolTurn = contents[i]; + break; + } + } + + expect(toolTurn).toBeTruthy(); + expect(toolTurn?.parts?.some((p) => p.functionCall !== undefined)).toBe(false); + + const text = toolTurn?.parts?.map((p) => p.text ?? "").join("\n"); + expect(text).toContain("[Tool Call: bash]"); + expect(text).toContain("ls -la"); + }); +});