fix(ai): use skip_thought_signature_validator for unsigned Gemini 3 tool calls

Replace text fallback with the official sentinel value so unsigned function
calls retain structured context in multi-turn conversations.

closes #1829
This commit is contained in:
Mario Zechner 2026-03-05 22:04:00 +01:00
parent cfbb15876a
commit a0d839ce84
4 changed files with 148 additions and 50 deletions

View file

@ -45,6 +45,11 @@ export function retainThoughtSignature(existing: string | undefined, incoming: s
// Thought signatures must be base64 for Google APIs (TYPE_BYTES).
const base64SignaturePattern = /^[A-Za-z0-9+/]+={0,2}$/;
// Sentinel value that tells the Gemini API to skip thought signature validation.
// Used for unsigned function call parts (e.g. replayed from providers without thought signatures).
// See: https://ai.google.dev/gemini-api/docs/thought-signatures
const SKIP_THOUGHT_SIGNATURE = "skip_thought_signature_validator";
function isValidThoughtSignature(signature: string | undefined): boolean {
if (!signature) return false;
if (signature.length % 4 !== 0) return false;
@ -138,28 +143,19 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
} else if (block.type === "toolCall") {
const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.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.
// We include a note telling the model this is historical context to prevent mimicry.
// Use the skip_thought_signature_validator sentinel for unsigned function calls
// (e.g. replayed from providers without thought signatures like Claude via Antigravity).
const isGemini3 = model.id.toLowerCase().includes("gemini-3");
if (isGemini3 && !thoughtSignature) {
const argsStr = JSON.stringify(block.arguments ?? {}, null, 2);
parts.push({
text: `[Historical context: a different model called tool "${block.name}" with arguments: ${argsStr}. Do not mimic this format - use proper function calling.]`,
});
} 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);
}
const effectiveSignature = thoughtSignature || (isGemini3 ? SKIP_THOUGHT_SIGNATURE : undefined);
const part: Part = {
functionCall: {
name: block.name,
args: block.arguments ?? {},
...(requiresToolCallId(model.id) ? { id: block.id } : {}),
},
...(effectiveSignature && { thoughtSignature: effectiveSignature }),
};
parts.push(part);
}
}