Propuesta IA: cliente MiniMax con tool-calling y reintentos
El contrato se fuerza con el input_schema de la herramienta porque MiniMax no soporta structured outputs. tool_choice no se envia: fijarlo solo en el reintento le daria un prefijo distinto y se pagaria el contexto completo. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5bad273977
commit
877a181a03
@@ -28,3 +28,11 @@ SEED_ADMIN_NAME=Administrador
|
|||||||
SEED_ASESOR_EMAIL=
|
SEED_ASESOR_EMAIL=
|
||||||
SEED_ASESOR_PASSWORD=
|
SEED_ASESOR_PASSWORD=
|
||||||
SEED_ASESOR_NAME=
|
SEED_ASESOR_NAME=
|
||||||
|
|
||||||
|
# --- Propuesta consultiva con IA (MiniMax) ---
|
||||||
|
# Clave del panel de MiniMax. NUNCA se commitea: .env esta en .gitignore.
|
||||||
|
# Ojo con el prefijo: NO uses ANTHROPIC_API_KEY. El SDK la lee por su cuenta y
|
||||||
|
# mandaria la clave de MiniMax a api.anthropic.com.
|
||||||
|
MINIMAX_API_KEY=
|
||||||
|
MINIMAX_BASE_URL=https://api.minimax.io/anthropic
|
||||||
|
MINIMAX_MODEL=MiniMax-M3
|
||||||
|
|||||||
Generated
+73
-1
@@ -8,6 +8,7 @@
|
|||||||
"name": "cotizador-e3",
|
"name": "cotizador-e3",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.115.0",
|
||||||
"@prisma/adapter-pg": "^7.8.0",
|
"@prisma/adapter-pg": "^7.8.0",
|
||||||
"@prisma/client": "^7.8.0",
|
"@prisma/client": "^7.8.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
"prisma": "^7.8.0",
|
"prisma": "^7.8.0",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
|
"zod": "^4.3.6",
|
||||||
"zustand": "^5.0.12"
|
"zustand": "^5.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -51,6 +53,27 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@anthropic-ai/sdk": {
|
||||||
|
"version": "0.115.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.115.0.tgz",
|
||||||
|
"integrity": "sha512-BJrFIVyjNuU8lfDyIJTvlRYzgQg+zEl78BxE7fq8esULsGz9IRQvGtW5spq3tydmtjQb/GFdooKGdGsetpx+lQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"json-schema-to-ts": "^3.1.1",
|
||||||
|
"standardwebhooks": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"anthropic-ai-sdk": "bin/cli"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"zod": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||||
@@ -243,6 +266,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||||
@@ -2233,6 +2265,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@stablelib/base64": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@standard-schema/spec": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
@@ -5179,6 +5217,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-sha256": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||||
@@ -6364,6 +6408,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/json-schema-to-ts": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.18.3",
|
||||||
|
"ts-algebra": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/json-schema-traverse": {
|
"node_modules/json-schema-traverse": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
@@ -8554,6 +8611,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/standardwebhooks": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@stablelib/base64": "^1.0.0",
|
||||||
|
"fast-sha256": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/std-env": {
|
"node_modules/std-env": {
|
||||||
"version": "3.10.0",
|
"version": "3.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||||
@@ -8890,6 +8957,12 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-algebra": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
||||||
@@ -9498,7 +9571,6 @@
|
|||||||
"version": "4.3.6",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"seed": "npx tsx prisma/seed.ts"
|
"seed": "npx tsx prisma/seed.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.115.0",
|
||||||
"@prisma/adapter-pg": "^7.8.0",
|
"@prisma/adapter-pg": "^7.8.0",
|
||||||
"@prisma/client": "^7.8.0",
|
"@prisma/client": "^7.8.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { crearCliente, modelo } 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;
|
||||||
|
|
||||||
|
console.log(fallas === 0 ? "\ntodo paso\n" : `\n${fallas} fallas\n`);
|
||||||
|
process.exit(fallas === 0 ? 0 : 1);
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
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<T>(
|
||||||
|
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}`
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user