Files
cotizador/scripts/verificar-propuesta-cliente.ts
T
urieljarethandClaude Opus 5 3f46d8a734 Propuesta IA: quitar los rechazos autoinfligidos y tolerar rarezas de MiniMax
Seis vueltas completas del pipeline contra los datos reales de UJ2606UR001
mostraron que la mayoria de los reintentos los provocaba el propio schema, no
el modelo:

- alcance.max(40) contra una cotizacion de 58 partidas garantizaba un rechazo de
  Zod en CADA corrida. Sube a 120. El tope solo acota una respuesta desbocada;
  la completitud del documento ya no depende de este array desde d20007d2.
- titulo.max(80) se quedaba corto y costo un reintento en 2 de 6 corridas.
  Sube a 140.

Y dos rarezas del proveedor, ambas observadas contra la API real:

- MiniMax a veces envuelve los elementos de un array en {item: {...}}. Se
  normaliza antes de validar, solo cuando "item" es la unica clave, para no
  tocar un campo legitimo con ese nombre.
- A veces emite DOS bloques tool_use en una respuesta. Antes se tomaba el
  primero a secas; ahora se prueban todos y gana el que valide.

Sobre el error de tipo de documento que se vio en produccion: NO se reprodujo en
seis corridas completas contra los mismos datos, y la generacion que lo siguio
completo sin problema (la fila quedo en la tabla). La evidencia apunta a un
fallo transitorio del proveedor, no a un defecto determinista nuestro. En vez de
inventar un arreglo para algo que no se puede reproducir, se reintentan los
fallos transitorios (5xx, 429, timeouts, red y los 400 con mensaje de parseo
interno) con espera creciente.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 20:54:48 -06:00

71 lines
3.2 KiB
TypeScript

import { crearCliente, modelo, normalizarRespuesta } from "@/lib/propuesta/cliente-ia";
// No llama a la API. Comprueba el contrato local y que el fallo sin clave sea claro.
let fallas = 0;
function check(n: string, ok: boolean, d = "") {
console.log(`${ok ? " OK " : " FALLA"} | ${n}${d ? " -> " + d : ""}`);
if (!ok) fallas++;
}
const guardada = process.env.MINIMAX_API_KEY;
const guardadaBase = process.env.MINIMAX_BASE_URL;
delete process.env.MINIMAX_API_KEY;
let msg = "";
try {
crearCliente();
} catch (e) {
msg = e instanceof Error ? e.message : String(e);
}
check("sin clave falla en vez de llamar a ciegas", msg.length > 0);
check("el error nombra la variable correcta", msg.includes("MINIMAX_API_KEY"));
check("el error advierte del prefijo ANTHROPIC_", msg.includes("ANTHROPIC_API_KEY"));
process.env.MINIMAX_API_KEY = "clave-de-prueba";
delete process.env.MINIMAX_BASE_URL;
const c = crearCliente();
check("baseURL apunta a MiniMax por defecto", String(c.baseURL).includes("api.minimax.io"), String(c.baseURL));
check("modelo por defecto es MiniMax-M3", modelo() === "MiniMax-M3", modelo());
// Aunque el shell tenga ANTHROPIC_BASE_URL, el cliente no debe hacerle caso.
process.env.ANTHROPIC_BASE_URL = "https://api.anthropic.com";
const c2 = crearCliente();
check("ANTHROPIC_BASE_URL del shell no secuestra el baseURL",
!String(c2.baseURL).includes("api.anthropic.com"), String(c2.baseURL));
delete process.env.ANTHROPIC_BASE_URL;
process.env.MINIMAX_BASE_URL = "https://ejemplo.local/anthropic";
check("MINIMAX_BASE_URL si se respeta", String(crearCliente().baseURL).includes("ejemplo.local"));
process.env.MINIMAX_MODEL = "MiniMax-M2";
check("MINIMAX_MODEL se respeta", modelo() === "MiniMax-M2");
delete process.env.MINIMAX_MODEL;
if (guardada) process.env.MINIMAX_API_KEY = guardada;
else delete process.env.MINIMAX_API_KEY;
if (guardadaBase) process.env.MINIMAX_BASE_URL = guardadaBase;
else delete process.env.MINIMAX_BASE_URL;
// ── Normalizacion de rarezas de MiniMax, observadas contra la API real ──
const igual = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b);
check("desenvuelve {item: X} cuando item es la unica clave",
igual(normalizarRespuesta({ item: { a: 1 } }), { a: 1 }));
check("desenvuelve dentro de arrays",
igual(normalizarRespuesta([{ item: { a: 1 } }, { item: { a: 2 } }]), [{ a: 1 }, { a: 2 }]));
check("desenvuelve en profundidad",
igual(normalizarRespuesta({ lista: [{ item: { x: [{ item: 5 }] } }] }), { lista: [{ x: [5] }] }));
check("NO toca un objeto con item junto a otras claves",
igual(normalizarRespuesta({ item: 1, otro: 2 }), { item: 1, otro: 2 }));
check("deja intactos los valores simples",
normalizarRespuesta("texto") === "texto" && normalizarRespuesta(5) === 5 && normalizarRespuesta(null) === null);
check("no altera un objeto normal",
igual(normalizarRespuesta({ a: 1, b: [1, 2] }), { a: 1, b: [1, 2] }));
check("el caso real observado: alcance envuelto",
igual(normalizarRespuesta({ alcance: [{ item: { refPartida: "P01", descripcionResultado: "x" } }] }),
{ alcance: [{ refPartida: "P01", descripcionResultado: "x" }] }));
console.log(fallas === 0 ? "\ntodo paso\n" : `\n${fallas} fallas\n`);
process.exit(fallas === 0 ? 0 : 1);