diff --git a/src/api/transform/__tests__/gemini-format.spec.ts b/src/api/transform/__tests__/gemini-format.spec.ts index 14ab6f8d8f..45cda7157a 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 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([ @@ -140,6 +141,73 @@ describe("convertAnthropicMessageToGemini", () => { ]) }) + 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", + }, + ], + }, + ]) + }) + it("should convert a message with tool result as string", () => { const toolIdToName = new Map() toolIdToName.set("calculator-123", "calculator") diff --git a/src/api/transform/gemini-format.ts b/src/api/transform/gemini-format.ts index bc4dc4aa54..1b9a5f2dfc 100644 --- a/src/api/transform/gemini-format.ts +++ b/src/api/transform/gemini-format.ts @@ -30,22 +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. Fallback to "skip_thought_signature_validator" if missing (e.g. cross-model history). + // 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) { - functionCallSignature = activeThoughtSignature || "skip_thought_signature_validator" + 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") {