From be8ac86165acec110b2e1fbff1bb393ac52558d1 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 24 Dec 2025 19:57:45 +0000 Subject: [PATCH 1/2] fix: Remove fallback thoughtSignature to prevent 400 errors with Gemini 3 models --- src/api/transform/__tests__/gemini-format.spec.ts | 3 +-- src/api/transform/gemini-format.ts | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/api/transform/__tests__/gemini-format.spec.ts b/src/api/transform/__tests__/gemini-format.spec.ts index 14ab6f8d8f0..cb627293915 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 without thoughtSignature when none exists", () => { const anthropicMessage: Anthropic.Messages.MessageParam = { role: "assistant", content: [ @@ -133,7 +133,6 @@ describe("convertAnthropicMessageToGemini", () => { name: "calculator", args: { operation: "add", numbers: [2, 3] }, }, - thoughtSignature: "skip_thought_signature_validator", }, ], }, diff --git a/src/api/transform/gemini-format.ts b/src/api/transform/gemini-format.ts index bc4dc4aa54a..a123226012f 100644 --- a/src/api/transform/gemini-format.ts +++ b/src/api/transform/gemini-format.ts @@ -42,10 +42,11 @@ 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). + // 2. Don't use a fallback - only include thoughtSignature if we have a real one. + // The fallback "skip_thought_signature_validator" causes 400 errors with Gemini 3 models. let functionCallSignature: string | undefined - if (includeThoughtSignatures) { - functionCallSignature = activeThoughtSignature || "skip_thought_signature_validator" + if (includeThoughtSignatures && activeThoughtSignature) { + functionCallSignature = activeThoughtSignature } if (typeof content === "string") { From a53db60a0d5ef2636a33cd072fc36f698fd63ffe Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 24 Dec 2025 20:13:13 +0000 Subject: [PATCH 2/2] fix: Restore fallback thoughtSignature for cross-model tool_use scenarios - Only use fallback when tool_use blocks exist without thoughtSignature - Fallback is needed when switching from non-thinking models to Gemini 3 - Added tests for both cross-model (fallback) and non-thinking (no signature) scenarios --- .../transform/__tests__/gemini-format.spec.ts | 71 ++++++++++++++++++- src/api/transform/gemini-format.ts | 21 ++++-- 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/src/api/transform/__tests__/gemini-format.spec.ts b/src/api/transform/__tests__/gemini-format.spec.ts index cb627293915..45cda7157a5 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 without thoughtSignature when none exists", () => { + it("should use fallback thoughtSignature for tool_use when includeThoughtSignatures is true but no signature exists (cross-model scenario)", () => { const anthropicMessage: Anthropic.Messages.MessageParam = { role: "assistant", content: [ @@ -121,6 +121,7 @@ describe("convertAnthropicMessageToGemini", () => { ], } + // Default includeThoughtSignatures is true, so fallback should be used const result = convertAnthropicMessageToGemini(anthropicMessage) expect(result).toEqual([ @@ -133,6 +134,74 @@ describe("convertAnthropicMessageToGemini", () => { name: "calculator", args: { operation: "add", numbers: [2, 3] }, }, + thoughtSignature: "skip_thought_signature_validator", + }, + ], + }, + ]) + }) + + it("should NOT include thoughtSignature for tool_use when includeThoughtSignatures is false", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "assistant", + content: [ + { type: "text", text: "Let me calculate that for you." }, + { + type: "tool_use", + id: "calc-123", + name: "calculator", + input: { operation: "add", numbers: [2, 3] }, + }, + ], + } + + // With includeThoughtSignatures false, no signature should be included + const result = convertAnthropicMessageToGemini(anthropicMessage, { includeThoughtSignatures: false }) + + expect(result).toEqual([ + { + role: "model", + parts: [ + { text: "Let me calculate that for you." }, + { + functionCall: { + name: "calculator", + args: { operation: "add", numbers: [2, 3] }, + }, + }, + ], + }, + ]) + }) + + it("should use real thoughtSignature when present in content", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "assistant", + content: [ + { type: "text", text: "Let me calculate that for you." }, + { + type: "tool_use", + id: "calc-123", + name: "calculator", + input: { operation: "add", numbers: [2, 3] }, + }, + { type: "thoughtSignature", thoughtSignature: "real-signature-abc123" } as any, + ], + } + + 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-signature-abc123", }, ], }, diff --git a/src/api/transform/gemini-format.ts b/src/api/transform/gemini-format.ts index a123226012f..1b9a5f2dfc8 100644 --- a/src/api/transform/gemini-format.ts +++ b/src/api/transform/gemini-format.ts @@ -30,23 +30,34 @@ export function convertAnthropicContentToGemini( const includeThoughtSignatures = options?.includeThoughtSignatures ?? true const toolIdToName = options?.toolIdToName - // First pass: find thoughtSignature if it exists in the content blocks + // First pass: find thoughtSignature and check for tool_use blocks let activeThoughtSignature: string | undefined + let hasToolUseBlocks = false if (Array.isArray(content)) { const sigBlock = content.find((block) => isThoughtSignatureContentBlock(block)) as ThoughtSignatureContentBlock if (sigBlock?.thoughtSignature) { activeThoughtSignature = sigBlock.thoughtSignature } + // Check if this message contains tool_use blocks + hasToolUseBlocks = content.some((block) => "type" in block && (block as { type: string }).type === "tool_use") } // 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. Don't use a fallback - only include thoughtSignature if we have a real one. - // The fallback "skip_thought_signature_validator" causes 400 errors with Gemini 3 models. + // 2. If there are tool_use blocks but no signature (cross-model history scenario), + // use the fallback "skip_thought_signature_validator" to satisfy Gemini 3's validation. + // See: https://ai.google.dev/gemini-api/docs/thought-signatures#faqs + // 3. If there are no tool_use blocks, don't include any signature (nothing to attach it to). let functionCallSignature: string | undefined - if (includeThoughtSignatures && activeThoughtSignature) { - functionCallSignature = activeThoughtSignature + if (includeThoughtSignatures) { + if (activeThoughtSignature) { + functionCallSignature = activeThoughtSignature + } else if (hasToolUseBlocks) { + // Cross-model scenario: tool_use blocks exist but no thoughtSignature was captured. + // This happens when switching from a non-thinking model to a thinking model. + functionCallSignature = "skip_thought_signature_validator" + } } if (typeof content === "string") {