import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' import type { GeminiFunctionCallingConfig, GeminiTool, } from './types.js' const GEMINI_JSON_SCHEMA_TYPES = new Set([ 'string', 'number', 'integer', 'boolean', 'object', 'array', 'null', ]) function normalizeGeminiJsonSchemaType( value: unknown, ): string | string[] | undefined { if (typeof value === 'string') { return GEMINI_JSON_SCHEMA_TYPES.has(value) ? value : undefined } if (Array.isArray(value)) { const normalized = value.filter( (item): item is string => typeof item === 'string' && GEMINI_JSON_SCHEMA_TYPES.has(item), ) const unique = Array.from(new Set(normalized)) if (unique.length === 0) return undefined return unique.length === 1 ? unique[0] : unique } return undefined } function inferGeminiJsonSchemaTypeFromValue(value: unknown): string | undefined { if (value === null) return 'null' if (Array.isArray(value)) return 'array' if (typeof value === 'string') return 'string' if (typeof value === 'boolean') return 'boolean' if (typeof value === 'number') { return Number.isInteger(value) ? 'integer' : 'number' } if (typeof value === 'object') return 'object' return undefined } function inferGeminiJsonSchemaTypeFromEnum( values: unknown[], ): string | string[] | undefined { const inferred = values .map(inferGeminiJsonSchemaTypeFromValue) .filter((value): value is string => value !== undefined) const unique = Array.from(new Set(inferred)) if (unique.length === 0) return undefined return unique.length === 1 ? unique[0] : unique } function addNullToGeminiJsonSchemaType( value: string | string[] | undefined, ): string | string[] | undefined { if (value === undefined) return ['null'] if (Array.isArray(value)) { return value.includes('null') ? value : [...value, 'null'] } return value === 'null' ? value : [value, 'null'] } function sanitizeGeminiJsonSchemaProperties( value: unknown, ): Record> | undefined { if (!value || typeof value !== 'object' || Array.isArray(value)) { return undefined } const sanitizedEntries = Object.entries(value as Record) .map(([key, schema]) => [key, sanitizeGeminiJsonSchema(schema)] as const) .filter(([, schema]) => Object.keys(schema).length > 0) if (sanitizedEntries.length === 0) { return undefined } return Object.fromEntries(sanitizedEntries) } function sanitizeGeminiJsonSchemaArray( value: unknown, ): Record[] | undefined { if (!Array.isArray(value)) return undefined const sanitized = value .map(item => sanitizeGeminiJsonSchema(item)) .filter(item => Object.keys(item).length > 0) return sanitized.length > 0 ? sanitized : undefined } function sanitizeGeminiJsonSchema( schema: unknown, ): Record { if (!schema || typeof schema !== 'object' || Array.isArray(schema)) { return {} } const source = schema as Record const result: Record = {} let type = normalizeGeminiJsonSchemaType(source.type) if (source.const !== undefined) { result.enum = [source.const] type = type ?? inferGeminiJsonSchemaTypeFromValue(source.const) } else if (Array.isArray(source.enum) && source.enum.length > 0) { result.enum = source.enum type = type ?? inferGeminiJsonSchemaTypeFromEnum(source.enum) } if (!type) { if (source.properties && typeof source.properties === 'object') { type = 'object' } else if (source.items !== undefined || source.prefixItems !== undefined) { type = 'array' } } if (source.nullable === true) { type = addNullToGeminiJsonSchemaType(type) } if (type) { result.type = type } if (typeof source.title === 'string') { result.title = source.title } if (typeof source.description === 'string') { result.description = source.description } if (typeof source.format === 'string') { result.format = source.format } if (typeof source.pattern === 'string') { result.pattern = source.pattern } if (typeof source.minimum === 'number') { result.minimum = source.minimum } else if (typeof source.exclusiveMinimum === 'number') { result.minimum = source.exclusiveMinimum } if (typeof source.maximum === 'number') { result.maximum = source.maximum } else if (typeof source.exclusiveMaximum === 'number') { result.maximum = source.exclusiveMaximum } if (typeof source.minItems === 'number') { result.minItems = source.minItems } if (typeof source.maxItems === 'number') { result.maxItems = source.maxItems } if (typeof source.minLength === 'number') { result.minLength = source.minLength } if (typeof source.maxLength === 'number') { result.maxLength = source.maxLength } if (typeof source.minProperties === 'number') { result.minProperties = source.minProperties } if (typeof source.maxProperties === 'number') { result.maxProperties = source.maxProperties } const properties = sanitizeGeminiJsonSchemaProperties(source.properties) if (properties) { result.properties = properties result.propertyOrdering = Object.keys(properties) } if (Array.isArray(source.required)) { const required = source.required.filter( (item): item is string => typeof item === 'string', ) if (required.length > 0) { result.required = required } } if (typeof source.additionalProperties === 'boolean') { result.additionalProperties = source.additionalProperties } else { const additionalProperties = sanitizeGeminiJsonSchema( source.additionalProperties, ) if (Object.keys(additionalProperties).length > 0) { result.additionalProperties = additionalProperties } } const items = sanitizeGeminiJsonSchema(source.items) if (Object.keys(items).length > 0) { result.items = items } const prefixItems = sanitizeGeminiJsonSchemaArray(source.prefixItems) if (prefixItems) { result.prefixItems = prefixItems } const anyOf = sanitizeGeminiJsonSchemaArray(source.anyOf ?? source.oneOf) if (anyOf) { result.anyOf = anyOf } return result } function sanitizeGeminiFunctionParameters( schema: unknown, ): Record { const sanitized = sanitizeGeminiJsonSchema(schema) if (Object.keys(sanitized).length > 0) { return sanitized } return { type: 'object', properties: {}, } } export function anthropicToolsToGemini(tools: BetaToolUnion[]): GeminiTool[] { const functionDeclarations = tools .filter(tool => { const toolType = (tool as unknown as { type?: string }).type return tool.type === 'custom' || !('type' in tool) || toolType !== 'server' }) .map(tool => { const anyTool = tool as unknown as Record const name = (anyTool.name as string) || '' const description = (anyTool.description as string) || '' const inputSchema = (anyTool.input_schema as Record | undefined) ?? { type: 'object', properties: {}, } return { name, description, parametersJsonSchema: sanitizeGeminiFunctionParameters(inputSchema), } }) return functionDeclarations.length > 0 ? [{ functionDeclarations }] : [] } export function anthropicToolChoiceToGemini( toolChoice: unknown, ): GeminiFunctionCallingConfig | undefined { if (!toolChoice || typeof toolChoice !== 'object') return undefined const tc = toolChoice as Record const type = tc.type as string switch (type) { case 'auto': return { mode: 'AUTO' } case 'any': return { mode: 'ANY' } case 'tool': return { mode: 'ANY', allowedFunctionNames: typeof tc.name === 'string' ? [tc.name] : undefined, } default: return undefined } }