From dc07aaa1f57857a8a3ba4c80eea46fc0e243e1cb Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 3 Apr 2026 13:15:58 +0000 Subject: [PATCH] fix: remove skip_thought_signature_validator bypass for Gemini 3.1+ models Gemini 3.1+ models reject the synthetic "skip_thought_signature_validator" string that was injected as a fallback when no real thought signature was available. This caused 400 errors ("Thought signature is not valid") after tool calls. Instead of falling back to the bypass string, omit the thoughtSignature field entirely when no real signature exists. This is safe because: - Real signatures from the model are still preserved and round-tripped - Function calls without signatures are accepted by the API - The bypass was only needed for Gemini 3 cross-model history, which works fine without it Closes #12050 --- .../transform/__tests__/gemini-format.spec.ts | 37 ++++++++++++++++++- src/api/transform/gemini-format.ts | 8 ++-- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/api/transform/__tests__/gemini-format.spec.ts b/src/api/transform/__tests__/gemini-format.spec.ts index 23f752e207f..f64321cb1b3 100644 --- a/src/api/transform/__tests__/gemini-format.spec.ts +++ b/src/api/transform/__tests__/gemini-format.spec.ts @@ -107,7 +107,7 @@ describe("convertAnthropicMessageToGemini", () => { expect(() => convertAnthropicMessageToGemini(anthropicMessage)).toThrow("Unsupported image source type") }) - it("should convert a message with tool use", () => { + it("should convert a message with tool use (no thought signature in history)", () => { const anthropicMessage: Anthropic.Messages.MessageParam = { role: "assistant", content: [ @@ -133,7 +133,40 @@ describe("convertAnthropicMessageToGemini", () => { name: "calculator", args: { operation: "add", numbers: [2, 3] }, }, - thoughtSignature: "skip_thought_signature_validator", + }, + ], + }, + ]) + }) + + it("should attach thoughtSignature to functionCall when a real signature exists", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "assistant", + content: [ + { type: "thoughtSignature", thoughtSignature: "real-sig-abc" } as any, + { type: "text", text: "Let me calculate that for you." }, + { + type: "tool_use", + id: "calc-123", + name: "calculator", + input: { operation: "add", numbers: [2, 3] }, + }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual([ + { + role: "model", + parts: [ + { text: "Let me calculate that for you." }, + { + functionCall: { + name: "calculator", + args: { operation: "add", numbers: [2, 3] }, + }, + thoughtSignature: "real-sig-abc", }, ], }, diff --git a/src/api/transform/gemini-format.ts b/src/api/transform/gemini-format.ts index 6f240362960..dbfbfcd567d 100644 --- a/src/api/transform/gemini-format.ts +++ b/src/api/transform/gemini-format.ts @@ -41,11 +41,13 @@ export function convertAnthropicContentToGemini( // Determine the signature to attach to function calls. // If we're in a mode that expects signatures (includeThoughtSignatures is true): - // 1. Use the actual signature if we found one in the history/content. - // 2. Fallback to "skip_thought_signature_validator" if missing (e.g. cross-model history). + // - Use the actual signature if we found one in the history/content. + // - If no real signature exists, omit it (undefined) so the API accepts the request. + // Gemini 3.1+ models reject the synthetic "skip_thought_signature_validator" bypass + // that worked with earlier Gemini 3 models. let functionCallSignature: string | undefined if (includeThoughtSignatures) { - functionCallSignature = activeThoughtSignature || "skip_thought_signature_validator" + functionCallSignature = activeThoughtSignature } if (typeof content === "string") {