Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion src/api/transform/__tests__/gemini-format.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -121,6 +121,7 @@ describe("convertAnthropicMessageToGemini", () => {
],
}

// Default includeThoughtSignatures is true, so fallback should be used
const result = convertAnthropicMessageToGemini(anthropicMessage)

expect(result).toEqual([
Expand All @@ -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<string, string>()
toolIdToName.set("calculator-123", "calculator")
Expand Down
18 changes: 15 additions & 3 deletions src/api/transform/gemini-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
Loading