import { getCachedSignature } from "../cache"; import { createLogger } from "../logger"; import { applyAntigravitySystemInstruction, normalizeThinkingConfig } from "../request-helpers"; import { cacheToolSchemas } from "../tool-schema-cache"; import type { RequestPayload, TransformContext, TransformResult } from "./types"; const log = createLogger("transform.gemini"); const THOUGHT_SIGNATURE_BYPASS = "skip_thought_signature_validator"; const GEMINI_TOOL_SCHEMA_SYSTEM_INSTRUCTION = ` You are operating in a CUSTOM ENVIRONMENT where tool definitions COMPLETELY DIFFER from your training data. VIOLATION OF THESE RULES WILL CAUSE IMMEDIATE SYSTEM FAILURE. ## ABSOLUTE RULES - NO EXCEPTIONS 1. **SCHEMA IS LAW**: The JSON schema in each tool definition is the ONLY source of truth. - Your pre-trained knowledge about tools like 'read_file', 'apply_diff', 'write_to_file', 'bash', etc. is INVALID here. - Every tool has been REDEFINED with different parameters than what you learned during training. 2. **PARAMETER NAMES ARE EXACT**: Use ONLY the parameter names from the schema. - WRONG: 'suggested_answers', 'file_path', 'files_to_read', 'command_to_run' - RIGHT: Check the 'properties' field in the schema for the exact names - The schema's 'required' array tells you which parameters are mandatory 3. **ARRAY PARAMETERS**: When a parameter has "type": "array", check the 'items' field: - If items.type is "object", you MUST provide an array of objects with the EXACT properties listed - If items.type is "string", you MUST provide an array of strings - NEVER provide a single object when an array is expected - NEVER provide an array when a single value is expected 4. **NESTED OBJECTS**: When items.type is "object": - Check items.properties for the EXACT field names required - Check items.required for which nested fields are mandatory - Include ALL required nested fields in EVERY array element 5. **STRICT PARAMETERS HINT**: Tool descriptions contain "STRICT PARAMETERS: ..." which lists: - Parameter name, type, and whether REQUIRED - For arrays of objects: the nested structure in brackets like [field: type REQUIRED, ...] - USE THIS as your quick reference, but the JSON schema is authoritative 6. **BEFORE EVERY TOOL CALL**: a. Read the tool's 'parametersJsonSchema' or 'parameters' field completely b. Identify ALL required parameters c. Verify your parameter names match EXACTLY (case-sensitive) d. For arrays, verify you're providing the correct item structure e. Do NOT add parameters that don't exist in the schema ## COMMON FAILURE PATTERNS TO AVOID - Using 'path' when schema says 'filePath' (or vice versa) - Using 'content' when schema says 'text' (or vice versa) - Providing {"file": "..."} when schema wants [{"path": "...", "line_ranges": [...]}] - Omitting required nested fields in array items - Adding 'additionalProperties' that the schema doesn't define - Guessing parameter names from similar tools you know from training ## REMEMBER Your training data about function calling is OUTDATED for this environment. The tool names may look familiar, but the schemas are DIFFERENT. When in doubt, RE-READ THE SCHEMA before making the call. ## GEMINI 3 RESPONSE RULES - Default to a direct, concise answer; add detail only when asked or required for correctness. - For multi-part tasks, use a short numbered list or labeled sections. - For long provided context, answer only from that context and avoid assumptions. - For multimodal inputs, explicitly reference each modality used and synthesize across them; do not invent details from absent modalities. - For complex tasks, outline a short plan and verify constraints before acting. `; function hasFunctionTools(payload: RequestPayload): boolean { const tools = payload.tools as Array> | undefined; if (!Array.isArray(tools)) return false; return tools.some((tool) => Array.isArray(tool.functionDeclarations)); } function extractSystemInstructionText(systemInstruction: unknown): string { if (typeof systemInstruction === "string") { return systemInstruction; } if (!systemInstruction || typeof systemInstruction !== "object") { return ""; } const parts = (systemInstruction as Record).parts as Array> | undefined; if (!Array.isArray(parts)) { return ""; } return parts .map((part) => (typeof part.text === "string" ? part.text : "")) .filter(Boolean) .join("\n"); } function getToolCallingMode(payload: RequestPayload): string | undefined { const toolConfig = payload.toolConfig; if (!toolConfig || typeof toolConfig !== "object") return undefined; const functionCallingConfig = (toolConfig as Record).functionCallingConfig; if (!functionCallingConfig || typeof functionCallingConfig !== "object") return undefined; const mode = (functionCallingConfig as Record).mode; return typeof mode === "string" ? mode : undefined; } function getFunctionToolNames(payload: RequestPayload): string[] { const tools = payload.tools as Array> | undefined; if (!Array.isArray(tools)) return []; const names: string[] = []; for (const tool of tools) { const funcDecls = tool.functionDeclarations as Array> | undefined; if (!Array.isArray(funcDecls)) continue; for (const funcDecl of funcDecls) { const name = funcDecl.name; if (typeof name === "string") names.push(name); } } return names; } function hasInjectedToolSchemaInstruction(payload: RequestPayload): boolean { const existingText = extractSystemInstructionText(payload.systemInstruction); return existingText.includes(""); } type ScrubResult = { cleaned: string; removedLines: number; removedBlocks: number }; function scrubToolTranscriptArtifacts(text: string): ScrubResult { const lines = text.split("\n"); const output: string[] = []; let removedLines = 0; let removedBlocks = 0; let inFence = false; let fenceStart = ""; let fenceLines: string[] = []; const isMarkerLine = (line: string): boolean => { return /^\s*Tool:\s*\w+/i.test(line) || /^\s*(?:thought|think)\s*:/i.test(line); }; for (const line of lines) { const isFence = line.trim().startsWith("```"); if (isFence) { if (!inFence) { inFence = true; fenceStart = line; fenceLines = []; continue; } const hadMarker = fenceLines.some(isMarkerLine); const cleanedFenceLines: string[] = []; for (const fenceLine of fenceLines) { if (isMarkerLine(fenceLine)) { removedLines += 1; } else { cleanedFenceLines.push(fenceLine); } } const hasNonWhitespace = cleanedFenceLines.some((l) => l.trim().length > 0); if (hadMarker && !hasNonWhitespace) { removedBlocks += 1; } else { output.push(fenceStart); output.push(...cleanedFenceLines); output.push(line); } inFence = false; fenceStart = ""; fenceLines = []; continue; } if (inFence) { fenceLines.push(line); continue; } if (isMarkerLine(line)) { removedLines += 1; continue; } output.push(line); } if (inFence) { output.push(fenceStart); output.push(...fenceLines); } const cleaned = output.join("\n").replace(/\n{4,}/g, "\n\n\n"); return { cleaned, removedLines, removedBlocks }; } function scrubConversationArtifactsFromModelHistory(payload: RequestPayload): void { const contents = payload.contents as Array> | undefined; if (!Array.isArray(contents)) return; let scrubbedParts = 0; let removedLines = 0; let removedBlocks = 0; for (const content of contents) { if (content.role !== "model") continue; const parts = content.parts as Array> | undefined; if (!Array.isArray(parts)) continue; for (const part of parts) { if (typeof part.text !== "string") continue; const scrubbed = scrubToolTranscriptArtifacts(part.text); if (scrubbed.removedLines > 0 || scrubbed.removedBlocks > 0) { part.text = scrubbed.cleaned; scrubbedParts += 1; removedLines += scrubbed.removedLines; removedBlocks += scrubbed.removedBlocks; } } } if (scrubbedParts > 0) { log.debug("Scrubbed tool transcript artifacts from model history", { scrubbedParts, removedLines, removedBlocks, }); } } function injectSystemInstructionIfNeeded(payload: RequestPayload): void { if (!hasFunctionTools(payload)) return; const existingText = extractSystemInstructionText(payload.systemInstruction); if (existingText.includes("")) { return; } const existing = payload.systemInstruction; if (!existing || typeof existing === "string") { const suffix = typeof existing === "string" && existing.trim().length > 0 ? `\n\n${existing}` : ""; payload.systemInstruction = { parts: [{ text: `${GEMINI_TOOL_SCHEMA_SYSTEM_INSTRUCTION}${suffix}` }] }; log.debug("Injected tool-schema systemInstruction", { existingType: typeof existing }); return; } const asRecord = existing as Record; const parts = asRecord.parts; if (Array.isArray(parts)) { asRecord.parts = [{ text: GEMINI_TOOL_SCHEMA_SYSTEM_INSTRUCTION }, ...parts]; payload.systemInstruction = asRecord; log.debug("Injected tool-schema systemInstruction", { existingType: "object(parts[])" }); return; } payload.systemInstruction = { ...asRecord, parts: [{ text: GEMINI_TOOL_SCHEMA_SYSTEM_INSTRUCTION }], }; log.debug("Injected tool-schema systemInstruction", { existingType: "object" }); } function normalizeSchemaType(typeValue: unknown): string | undefined { if (typeof typeValue === "string") { return typeValue; } if (Array.isArray(typeValue)) { const nonNull = typeValue.filter((t) => t !== "null"); const first = nonNull[0] ?? typeValue[0]; return typeof first === "string" ? first : undefined; } return undefined; } function summarizeSchema(schema: unknown, depth: number): string { if (!schema || typeof schema !== "object") { return "unknown"; } const record = schema as Record; const normalizedType = normalizeSchemaType(record.type); const enumValues = Array.isArray(record.enum) ? record.enum : undefined; if (normalizedType === "array") { const items = record.items; const itemSummary = depth > 0 ? summarizeSchema(items, depth - 1) : "unknown"; return `array[${itemSummary}]`; } if (normalizedType === "object") { const props = record.properties as Record | undefined; const required = Array.isArray(record.required) ? (record.required as unknown[]).filter((v): v is string => typeof v === "string") : []; if (!props || depth <= 0) { return "object"; } const keys = Object.keys(props); const requiredKeys = keys.filter((k) => required.includes(k)); const optionalKeys = keys.filter((k) => !required.includes(k)); const orderedKeys = [...requiredKeys.sort(), ...optionalKeys.sort()]; const maxPropsToShow = 8; const shownKeys = orderedKeys.slice(0, maxPropsToShow); const inner = shownKeys .map((key) => { const propSchema = props[key]; const propType = summarizeSchema(propSchema, depth - 1); const requiredSuffix = required.includes(key) ? " REQUIRED" : ""; return `${key}: ${propType}${requiredSuffix}`; }) .join(", "); const extraCount = orderedKeys.length - shownKeys.length; const extra = extraCount > 0 ? `, …+${extraCount}` : ""; return `{${inner}${extra}}`; } if (enumValues && enumValues.length > 0) { const preview = enumValues.slice(0, 6).map(String).join("|"); const suffix = enumValues.length > 6 ? "|…" : ""; return `${normalizedType ?? "unknown"} enum(${preview}${suffix})`; } return normalizedType ?? "unknown"; } function buildStrictParamsSummary(parametersSchema: Record): string { const schemaType = normalizeSchemaType(parametersSchema.type); const properties = parametersSchema.properties as Record | undefined; const required = Array.isArray(parametersSchema.required) ? (parametersSchema.required as unknown[]).filter((v): v is string => typeof v === "string") : []; if (schemaType !== "object" || !properties) { return "(schema missing top-level object properties)"; } const keys = Object.keys(properties); const requiredKeys = keys.filter((k) => required.includes(k)); const optionalKeys = keys.filter((k) => !required.includes(k)); const orderedKeys = [...requiredKeys.sort(), ...optionalKeys.sort()]; const parts = orderedKeys.map((key) => { const propSchema = properties[key]; const typeSummary = summarizeSchema(propSchema, 2); const requiredSuffix = required.includes(key) ? " REQUIRED" : ""; return `${key}: ${typeSummary}${requiredSuffix}`; }); const summary = parts.join(", "); const maxLen = 900; return summary.length > maxLen ? `${summary.slice(0, maxLen)}…` : summary; } function augmentToolDescriptionsWithStrictParams(payload: RequestPayload): void { const tools = payload.tools as Array> | undefined; if (!Array.isArray(tools)) return; let augmented = 0; const toolNames: string[] = []; for (const tool of tools) { const funcDecls = tool.functionDeclarations as Array> | undefined; if (!Array.isArray(funcDecls)) continue; for (const funcDecl of funcDecls) { const schema = (funcDecl.parametersJsonSchema ?? funcDecl.parameters) as Record | undefined; if (!schema || typeof schema !== "object") continue; const currentDescription = typeof funcDecl.description === "string" ? funcDecl.description : ""; if (currentDescription.includes("STRICT PARAMETERS:")) continue; const summary = buildStrictParamsSummary(schema); const nextDescription = currentDescription.trim().length > 0 ? `${currentDescription.trim()}\n\nSTRICT PARAMETERS: ${summary}` : `STRICT PARAMETERS: ${summary}`; funcDecl.description = nextDescription; augmented += 1; if (typeof funcDecl.name === "string") { toolNames.push(funcDecl.name); } } } if (augmented > 0) { log.debug("Augmented tool descriptions with STRICT PARAMETERS", { count: augmented, toolNamesPreview: toolNames.slice(0, 8), }); } } /** * Sanitizes tool names for Gemini API compatibility. * Gemini requires tool names to match: ^[a-zA-Z_][a-zA-Z0-9_-]*$ * This means names cannot start with numbers. * * @param name - The original tool name * @returns A sanitized tool name that starts with a letter or underscore */ function sanitizeToolNameForGemini(name: string): string { // If the name starts with a number, prepend 't_' (for 'tool_') if (/^[0-9]/.test(name)) { return `t_${name}`; } return name; } /** * Recursively sanitizes all tool names in the request payload. * * @param payload - The request payload containing tools */ function sanitizeToolNames(payload: RequestPayload): void { const tools = payload.tools as Array> | undefined; if (!Array.isArray(tools)) return; for (const tool of tools) { const funcDecls = tool.functionDeclarations as Array> | undefined; if (!Array.isArray(funcDecls)) continue; for (const func of funcDecls) { if (typeof func.name === "string") { const originalName = func.name; func.name = sanitizeToolNameForGemini(originalName); if (originalName !== func.name) { log.debug(`Sanitized tool name: ${originalName} → ${func.name}`); } } } } } /** * Transforms a request payload for native Gemini models. * * Handles common transformations: * - Removes `safetySettings` (Antigravity manages these) * - Sets `toolConfig.functionCallingConfig.mode` to "VALIDATED" * - Normalizes `thinkingConfig` for Gemini 2.5/3 models * - Extracts and normalizes `cachedContent` from various locations * - Wraps payload with Antigravity metadata (project, userAgent, requestId, sessionId) */ export function transformGeminiRequest( context: TransformContext, parsedBody: RequestPayload, ): TransformResult { const requestPayload: RequestPayload = { ...parsedBody }; delete requestPayload.safetySettings; if (!requestPayload.toolConfig) { requestPayload.toolConfig = {}; } if (typeof requestPayload.toolConfig === "object") { const toolConfig = requestPayload.toolConfig as Record; if (!toolConfig.functionCallingConfig) { toolConfig.functionCallingConfig = {}; } if (typeof toolConfig.functionCallingConfig === "object") { (toolConfig.functionCallingConfig as Record).mode = "VALIDATED"; } } const rawGenerationConfig = requestPayload.generationConfig as Record | undefined; const normalizedThinking = normalizeThinkingConfig(rawGenerationConfig?.thinkingConfig); if (normalizedThinking) { if (rawGenerationConfig) { rawGenerationConfig.thinkingConfig = normalizedThinking; requestPayload.generationConfig = rawGenerationConfig; } else { requestPayload.generationConfig = { thinkingConfig: normalizedThinking }; } } else if (rawGenerationConfig?.thinkingConfig) { delete rawGenerationConfig.thinkingConfig; requestPayload.generationConfig = rawGenerationConfig; } if ("system_instruction" in requestPayload) { requestPayload.systemInstruction = requestPayload.system_instruction; delete requestPayload.system_instruction; } const cachedContentFromExtra = typeof requestPayload.extra_body === "object" && requestPayload.extra_body ? (requestPayload.extra_body as Record).cached_content ?? (requestPayload.extra_body as Record).cachedContent : undefined; const cachedContent = (requestPayload.cached_content as string | undefined) ?? (requestPayload.cachedContent as string | undefined) ?? (cachedContentFromExtra as string | undefined); if (cachedContent) { requestPayload.cachedContent = cachedContent; } delete requestPayload.cached_content; delete requestPayload.cachedContent; if (requestPayload.extra_body && typeof requestPayload.extra_body === "object") { delete (requestPayload.extra_body as Record).cached_content; delete (requestPayload.extra_body as Record).cachedContent; if (Object.keys(requestPayload.extra_body as Record).length === 0) { delete requestPayload.extra_body; } } if ("model" in requestPayload) { delete requestPayload.model; } // Sanitize tool names to ensure Gemini API compatibility sanitizeToolNames(requestPayload); // Cache tool schemas for response normalization cacheToolSchemas(requestPayload.tools as any[]); augmentToolDescriptionsWithStrictParams(requestPayload); injectSystemInstructionIfNeeded(requestPayload); scrubConversationArtifactsFromModelHistory(requestPayload); applyAntigravitySystemInstruction(requestPayload, context.model); const contents = requestPayload.contents as Array> | undefined; if (Array.isArray(contents)) { for (let contentIndex = 0; contentIndex < contents.length; contentIndex++) { const content = contents[contentIndex]; if (!content) continue; if (content.role !== "model") continue; const parts = content.parts as Array> | undefined; if (!Array.isArray(parts)) continue; const filteredParts: Array> = []; let thinkingBlocksRemoved = 0; let removedThoughtSignatures = 0; let addedFunctionCallThoughtSignatures = 0; let currentThoughtSignature: string | undefined; for (let partIndex = 0; partIndex < parts.length; partIndex++) { const part = parts[partIndex]; if (!part) continue; if (part.thought === true) { const thoughtText = part.text as string | undefined; if (thoughtText && context.sessionId) { const cachedSig = getCachedSignature(context.family, context.sessionId, thoughtText); if (cachedSig) { part.thoughtSignature = cachedSig; currentThoughtSignature = cachedSig; filteredParts.push(part); log.debug("Restored thought from own cache", { family: context.family, sessionId: context.sessionId, contentIndex, partIndex, textLen: thoughtText.length, }); continue; } } thinkingBlocksRemoved += 1; log.debug("Removed thinking block (not in own cache)", { family: context.family, sessionId: context.sessionId, contentIndex, partIndex, textLen: typeof part.text === "string" ? part.text.length : undefined, }); continue; } if (part.functionCall) { const functionCall = (part.functionCall ?? {}) as Record; const callName = typeof functionCall.name === "string" ? functionCall.name : ""; const existingSig = part.thoughtSignature; if (typeof existingSig !== "string" || existingSig.length === 0) { const source = currentThoughtSignature ? "current_thought" : "bypass"; const nextSig = currentThoughtSignature ?? THOUGHT_SIGNATURE_BYPASS; part.thoughtSignature = nextSig; addedFunctionCallThoughtSignatures += 1; log.debug("Added thoughtSignature to functionCall part", { family: context.family, sessionId: context.sessionId, contentIndex, partIndex, callName, signatureSource: source, signatureLen: nextSig.length, }); } filteredParts.push(part); continue; } if (part.thoughtSignature !== undefined) { delete part.thoughtSignature; removedThoughtSignatures += 1; } filteredParts.push(part); } content.parts = filteredParts; if (thinkingBlocksRemoved > 0) { log.debug("Removed foreign thinking blocks", { count: thinkingBlocksRemoved, contentIndex }); } if (removedThoughtSignatures > 0) { log.debug("Removed thoughtSignature from non-thought non-tool model parts", { count: removedThoughtSignatures, contentIndex }); } if (addedFunctionCallThoughtSignatures > 0) { log.debug("Added thoughtSignature to functionCall parts", { count: addedFunctionCallThoughtSignatures, contentIndex }); } } } requestPayload.sessionId = context.sessionId; const wrappedBody = { project: context.projectId, model: context.model, userAgent: "antigravity", requestType: "agent", requestId: context.requestId, request: requestPayload, }; const toolCount = countTools(requestPayload); const toolCallingMode = getToolCallingMode(requestPayload); const systemInstructionHasMarker = hasInjectedToolSchemaInstruction(requestPayload); const functionToolNames = getFunctionToolNames(requestPayload); const contentsCount = Array.isArray(requestPayload.contents) ? requestPayload.contents.length : 0; log.debug("Gemini request transformed", { model: context.model, streaming: context.streaming, sessionId: context.sessionId, toolCount, toolCallingMode, systemInstructionHasMarker, functionToolNamesPreview: functionToolNames.slice(0, 8), contentsCount, }); const body = JSON.stringify(wrappedBody); return { body, debugInfo: { transformer: "gemini", toolCount, }, }; } function countTools(payload: RequestPayload): number { const tools = payload.tools as Array> | undefined; if (!Array.isArray(tools)) return 0; let count = 0; for (const tool of tools) { const funcDecls = tool.functionDeclarations as Array | undefined; if (Array.isArray(funcDecls)) { count += funcDecls.length; } if (tool.googleSearch) { count += 1; } if (tool.urlContext) { count += 1; } } return count; }