import Anthropic from "@anthropic-ai/sdk"; import { z } from "zod"; import { aJsonSchema } from "./schemas"; /** * Cliente del proveedor de IA (MiniMax-M3 via su endpoint compatible con Anthropic) * y runner que fuerza el contrato con tool-calling. * * MiniMax NO soporta structured outputs (`output_config` / `json_schema`), asi que * el `input_schema` de la herramienta ES el contrato, y Zod valida siempre del lado * del codigo. `tool_choice` no se envia: no esta documentado en MiniMax, y fijarlo * solo en el reintento le daria al reintento un prefijo distinto — se pagaria el * contexto completo justo cuando es mas grande. */ export interface UsoTokens { entrada: number; salida: number; cacheLectura: number; cacheEscritura: number; } export function crearCliente(): Anthropic { const apiKey = process.env.MINIMAX_API_KEY; if (!apiKey) { throw new Error( "Falta MINIMAX_API_KEY. Definela en .env. No uses ANTHROPIC_API_KEY: el SDK la " + "leeria por su cuenta y mandaria la clave de MiniMax a api.anthropic.com." ); } // apiKey y baseURL explicitos. Si se dejan al SDK, toma las ANTHROPIC_* del shell // y la clave termina en el proveedor equivocado. return new Anthropic({ apiKey, baseURL: process.env.MINIMAX_BASE_URL || "https://api.minimax.io/anthropic", }); } export function modelo(): string { return process.env.MINIMAX_MODEL || "MiniMax-M3"; } export interface BloqueSystem { texto: string; cachear: boolean; } interface OpcionesLlamada { system: BloqueSystem[]; mensajeUsuario: string; herramienta: { nombre: string; descripcion: string; schema: z.ZodType }; maxIntentos: number; maxTokens: number; } export async function llamarConHerramienta( opts: OpcionesLlamada ): Promise<{ datos: T; uso: UsoTokens; intentos: number }> { const cliente = crearCliente(); const tools = [ { name: opts.herramienta.nombre, description: opts.herramienta.descripcion, input_schema: aJsonSchema(opts.herramienta.schema) as Anthropic.Tool["input_schema"], }, ]; const system = opts.system.map((b) => ({ type: "text" as const, text: b.texto, ...(b.cachear ? { cache_control: { type: "ephemeral" as const } } : {}), })); const mensajes: Anthropic.MessageParam[] = [{ role: "user", content: opts.mensajeUsuario }]; const uso: UsoTokens = { entrada: 0, salida: 0, cacheLectura: 0, cacheEscritura: 0 }; let ultimoError = ""; for (let intento = 1; intento <= opts.maxIntentos; intento++) { const res = await cliente.messages.create({ model: modelo(), max_tokens: opts.maxTokens, system, tools, messages: mensajes, }); const u = res.usage as { input_tokens?: number; output_tokens?: number; cache_read_input_tokens?: number; cache_creation_input_tokens?: number; }; uso.entrada += u.input_tokens ?? 0; uso.salida += u.output_tokens ?? 0; uso.cacheLectura += u.cache_read_input_tokens ?? 0; uso.cacheEscritura += u.cache_creation_input_tokens ?? 0; const bloquesTool = res.content.filter( (b): b is Anthropic.ToolUseBlock => b.type === "tool_use" ); const correcto = bloquesTool.find((b) => b.name === opts.herramienta.nombre); if (correcto) { const parsed = opts.herramienta.schema.safeParse(correcto.input); if (parsed.success) { return { datos: parsed.data as T, uso, intentos: intento }; } ultimoError = z.prettifyError(parsed.error); // Hubo tool_use: la API exige un tool_result por CADA uno antes de continuar. // Un turno de usuario plano despues de un tool_use devuelve 400. mensajes.push( { role: "assistant", content: res.content }, { role: "user", content: [ ...bloquesTool.map((b) => ({ type: "tool_result" as const, tool_use_id: b.id, is_error: true, content: b.id === correcto.id ? ultimoError : `Herramienta inesperada. Usa ${opts.herramienta.nombre}.`, })), { type: "text" as const, text: `La llamada no cumple el schema. Corrige EXACTAMENTE estos errores y vuelve a llamar a ${opts.herramienta.nombre}:\n\n${ultimoError}`, }, ], } ); continue; } // No llamo a ninguna herramienta: aqui si va un turno de usuario plano. ultimoError = "El modelo respondio con prosa en vez de llamar a la herramienta."; mensajes.push( { role: "assistant", content: res.content }, { role: "user", content: `Debes responder llamando a la herramienta ${opts.herramienta.nombre}. No escribas prosa suelta.`, } ); } throw new Error( `El paso "${opts.herramienta.nombre}" agoto ${opts.maxIntentos} intentos.\nUltimo error:\n${ultimoError}` ); }