"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.convertMessages = convertMessages;exports.convertTools = convertTools;exports.isThinkingPart = isThinkingPart;exports.mapStopReason = mapStopReason;exports.mapStopReasonString = mapStopReasonString;exports.mapToolChoice = mapToolChoice;exports.requiresToolCallId = requiresToolCallId;exports.retainThoughtSignature = retainThoughtSignature; var _genai = require("@google/genai"); var _sanitizeUnicode = require("../utils/sanitize-unicode.js"); var _transformMessages = require("./transform-messages.js"); /** * Shared utilities for Google Generative AI and Google Cloud Code Assist providers. */ /** * Determines whether a streamed Gemini `Part` should be treated as "thinking". * * Protocol note (Gemini / Vertex AI thought signatures): * - `thought: true` is the definitive marker for thinking content (thought summaries). * - `thoughtSignature` is an encrypted representation of the model's internal thought process * used to preserve reasoning context across multi-turn interactions. * - `thoughtSignature` can appear on ANY part type (text, functionCall, etc.) - it does NOT * indicate the part itself is thinking content. * - For non-functionCall responses, the signature appears on the last part for context replay. * - When persisting/replaying model outputs, signature-bearing parts must be preserved as-is; * do not merge/move signatures across parts. * * See: https://ai.google.dev/gemini-api/docs/thought-signatures */function isThinkingPart(part) { return part.thought === true; } /** * Retain thought signatures during streaming. * * Some backends only send `thoughtSignature` on the first delta for a given part/block; later deltas may omit it. * This helper preserves the last non-empty signature for the current block. * * Note: this does NOT merge or move signatures across distinct response parts. It only prevents * a signature from being overwritten with `undefined` within the same streamed block. */ function retainThoughtSignature(existing, incoming) { if (typeof incoming === "string" && incoming.length > 0) return incoming; return existing; } // 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) { if (!signature) return false; if (signature.length % 4 !== 0) return false; return base64SignaturePattern.test(signature); } /** * Only keep signatures from the same provider/model and with valid base64. */ function resolveThoughtSignature(isSameProviderAndModel, signature) { return isSameProviderAndModel && isValidThoughtSignature(signature) ? signature : undefined; } /** * Models via Google APIs that require explicit tool call IDs in function calls/responses. */ function requiresToolCallId(modelId) { return modelId.startsWith("claude-") || modelId.startsWith("gpt-oss-"); } /** * Convert internal messages to Gemini Content[] format. */ function convertMessages(model, context) { const contents = []; const normalizeToolCallId = (id) => { if (!requiresToolCallId(model.id)) return id; return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64); }; const transformedMessages = (0, _transformMessages.transformMessages)(context.messages, model, normalizeToolCallId); for (const msg of transformedMessages) { if (msg.role === "user") { if (typeof msg.content === "string") { contents.push({ role: "user", parts: [{ text: (0, _sanitizeUnicode.sanitizeSurrogates)(msg.content) }] }); } else { const parts = msg.content.map((item) => { if (item.type === "text") { return { text: (0, _sanitizeUnicode.sanitizeSurrogates)(item.text) }; } else { return { inlineData: { mimeType: item.mimeType, data: item.data } }; } }); const filteredParts = !model.input.includes("image") ? parts.filter((p) => p.text !== undefined) : parts; if (filteredParts.length === 0) continue; contents.push({ role: "user", parts: filteredParts }); } } else if (msg.role === "assistant") { const parts = []; // Check if message is from same provider and model - only then keep thinking blocks const isSameProviderAndModel = msg.provider === model.provider && msg.model === model.id; for (const block of msg.content) { if (block.type === "text") { // Skip empty text blocks - they can cause issues with some models (e.g. Claude via Antigravity) if (!block.text || block.text.trim() === "") continue; const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.textSignature); parts.push({ text: (0, _sanitizeUnicode.sanitizeSurrogates)(block.text), ...(thoughtSignature && { thoughtSignature }) }); } else if (block.type === "thinking") { // Skip empty thinking blocks if (!block.thinking || block.thinking.trim() === "") continue; // Only keep as thinking block if same provider AND same model // Otherwise convert to plain text (no tags to avoid model mimicking them) if (isSameProviderAndModel) { const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thinkingSignature); parts.push({ thought: true, text: (0, _sanitizeUnicode.sanitizeSurrogates)(block.thinking), ...(thoughtSignature && { thoughtSignature }) }); } else { parts.push({ text: (0, _sanitizeUnicode.sanitizeSurrogates)(block.thinking) }); } } else if (block.type === "toolCall") { const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thoughtSignature); // Gemini 3 requires thoughtSignature on all function calls when thinking mode is enabled. // 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"); const effectiveSignature = thoughtSignature || (isGemini3 ? SKIP_THOUGHT_SIGNATURE : undefined); const part = { functionCall: { name: block.name, args: block.arguments ?? {}, ...(requiresToolCallId(model.id) ? { id: block.id } : {}) }, ...(effectiveSignature && { thoughtSignature: effectiveSignature }) }; parts.push(part); } } if (parts.length === 0) continue; contents.push({ role: "model", parts }); } else if (msg.role === "toolResult") { // Extract text and image content const textContent = msg.content.filter((c) => c.type === "text"); const textResult = textContent.map((c) => c.text).join("\n"); const imageContent = model.input.includes("image") ? msg.content.filter((c) => c.type === "image") : []; const hasText = textResult.length > 0; const hasImages = imageContent.length > 0; // Gemini 3 supports multimodal function responses with images nested inside functionResponse.parts // See: https://ai.google.dev/gemini-api/docs/function-calling#multimodal // Older models don't support this, so we put images in a separate user message. const supportsMultimodalFunctionResponse = model.id.includes("gemini-3"); // Use "output" key for success, "error" key for errors as per SDK documentation const responseValue = hasText ? (0, _sanitizeUnicode.sanitizeSurrogates)(textResult) : hasImages ? "(see attached image)" : ""; const imageParts = imageContent.map((imageBlock) => ({ inlineData: { mimeType: imageBlock.mimeType, data: imageBlock.data } })); const includeId = requiresToolCallId(model.id); const functionResponsePart = { functionResponse: { name: msg.toolName, response: msg.isError ? { error: responseValue } : { output: responseValue }, // Nest images inside functionResponse.parts for Gemini 3 ...(hasImages && supportsMultimodalFunctionResponse && { parts: imageParts }), ...(includeId ? { id: msg.toolCallId } : {}) } }; // Cloud Code Assist API requires all function responses to be in a single user turn. // Check if the last content is already a user turn with function responses and merge. const lastContent = contents[contents.length - 1]; if (lastContent?.role === "user" && lastContent.parts?.some((p) => p.functionResponse)) { lastContent.parts.push(functionResponsePart); } else { contents.push({ role: "user", parts: [functionResponsePart] }); } // For older models, add images in a separate user message if (hasImages && !supportsMultimodalFunctionResponse) { contents.push({ role: "user", parts: [{ text: "Tool result image:" }, ...imageParts] }); } } } return contents; } /** * Convert tools to Gemini function declarations format. * * By default uses `parametersJsonSchema` which supports full JSON Schema (including * anyOf, oneOf, const, etc.). Set `useParameters` to true to use the legacy `parameters` * field instead (OpenAPI 3.03 Schema). This is needed for Cloud Code Assist with Claude * models, where the API translates `parameters` into Anthropic's `input_schema`. */ function convertTools(tools, useParameters = false) { if (tools.length === 0) return undefined; return [ { functionDeclarations: tools.map((tool) => ({ name: tool.name, description: tool.description, ...(useParameters ? { parameters: tool.parameters } : { parametersJsonSchema: tool.parameters }) })) }]; } /** * Map tool choice string to Gemini FunctionCallingConfigMode. */ function mapToolChoice(choice) { switch (choice) { case "auto": return _genai.FunctionCallingConfigMode.AUTO; case "none": return _genai.FunctionCallingConfigMode.NONE; case "any": return _genai.FunctionCallingConfigMode.ANY; default: return _genai.FunctionCallingConfigMode.AUTO; } } /** * Map Gemini FinishReason to our StopReason. */ function mapStopReason(reason) { switch (reason) { case _genai.FinishReason.STOP: return "stop"; case _genai.FinishReason.MAX_TOKENS: return "length"; case _genai.FinishReason.BLOCKLIST: case _genai.FinishReason.PROHIBITED_CONTENT: case _genai.FinishReason.SPII: case _genai.FinishReason.SAFETY: case _genai.FinishReason.IMAGE_SAFETY: case _genai.FinishReason.IMAGE_PROHIBITED_CONTENT: case _genai.FinishReason.IMAGE_RECITATION: case _genai.FinishReason.IMAGE_OTHER: case _genai.FinishReason.RECITATION: case _genai.FinishReason.FINISH_REASON_UNSPECIFIED: case _genai.FinishReason.OTHER: case _genai.FinishReason.LANGUAGE: case _genai.FinishReason.MALFORMED_FUNCTION_CALL: case _genai.FinishReason.UNEXPECTED_TOOL_CALL: case _genai.FinishReason.NO_IMAGE: return "error"; default:{ const _exhaustive = reason; throw new Error(`Unhandled stop reason: ${_exhaustive}`); } } } /** * Map string finish reason to our StopReason (for raw API responses). */ function mapStopReasonString(reason) { switch (reason) { case "STOP": return "stop"; case "MAX_TOKENS": return "length"; default: return "error"; } } /* v9-b30483e931e84511 */