Tag: Claude API

  • Tutorial de harness engineering: la regla fuera del prompt

    Tutorial de harness engineering: la regla fuera del prompt

    El ticket dice: "Un cliente pagó 4,95 € de envío en un pedido de 45 €. Debería haber sido gratis".

    Se lo pasas al agente. Lee shipping.ts, encuentra FREE_SHIPPING_THRESHOLD = 50 y concluye que el código está bien. El pedido era de 45. Cerrado.

    Solo que negocio bajó el umbral a 39 € hace dos meses. Esa decisión está en un fichero de catálogo. En el código, no. En el prompt, tampoco. Este tutorial de harness engineering va de eso: de que el agente no tenga que adivinar ni tú que acordarte.

    En corto: la regla de negocio no se escribe en el prompt ni se deja solo en el código: vive en un catálogo del servicio y un harness mínimo la inyecta en cada ejecución. Ese mismo harness decide qué ficheros puede tocar el agente, rechaza cualquier cambio fuera de la lista y usa los tests como feedback para reintentar un número limitado de veces. El agente propone; el harness controla, y nunca despliega.


    ¿Qué significa sacar la regla de negocio fuera del prompt?

    Sacar la regla de negocio del prompt significa que el valor correcto (un umbral, un límite, un plazo) vive en una fuente de verdad versionada que el harness lee e inyecta, en lugar de depender de que la persona que escribe el prompt se acuerde de mencionarlo.

    Harness engineering es la disciplina de diseñar el sistema que rodea al modelo (qué contexto recibe, qué puede tocar y cómo se verifica su trabajo) para que un agente de IA produzca resultados predecibles.

    No voy a repetir la teoría: la anatomía completa está en qué es un agent harness y el origen del término en harness engineering con Codex de OpenAI. Y si quieres la visión de por qué todo tu ciclo de desarrollo es, en realidad, una fábrica de contexto, léete SDLC context engineering.

    Aquí vamos a lo concreto: un servicio, un bug, unas 150 líneas de TypeScript.

    Tres sitios donde puede vivir la regla

    Antes del código, la decisión. El umbral de envío gratis puede vivir en tres sitios, y cada uno falla de una forma distinta.

    En el prompt Hardcodeada en el código En el catálogo, inyectada por el harness
    Quién la mantiene Quien escribe el prompt ese día Quien tocó el fichero la última vez El owner del servicio
    Qué pasa cuando cambia Depende de que alguien se acuerde Nadie se entera hasta que llega un ticket Cambias una línea del YAML y el siguiente run la usa
    La ve el agente Solo si la escribes Sí, pero la toma como verdad aunque esté mal Siempre, marcada como fuente que prevalece
    La ven los tests No Solo si el test repite el número Sí, si el test lee el mismo catálogo
    Riesgo principal Olvido. Cada prompt es un punto de fallo Divergencia silenciosa con negocio Catálogo desactualizado tratado como verdad

    La tercera columna no es perfecta. Pero es la única en la que el error tiene un solo sitio donde corregirse.

    El servicio de ejemplo del tutorial de harness engineering

    Estructura mínima:

    catalog/shipping-service.yaml
    src/shipping.ts
    src/shipping.test.ts
    harness/context.ts
    harness/run.ts
    

    El catálogo. En empresas grandes esto no es un YAML suelto: vive en un developer portal. Backstage lo modela con un catalog-info.yaml por servicio y Port lo expone como entidades con API. Si no tienes nada de eso, un fichero versionado en el repo sirve igual para empezar:

    # catalog/shipping-service.yaml
    name: shipping-service
    owner: team-checkout
    rules:
      freeShippingThreshold:
        value: 39
        unit: EUR
        source: "Decisión de negocio Q3-2026 (OPS-412)"
      standardShippingCost:
        value: 4.95
        unit: EUR
    agent:
      editableFiles:
        - src/shipping.ts
      testCommand: "npx vitest run src/shipping.test.ts"
      maxAttempts: 3
    

    Fíjate en el bloque agent. Qué ficheros puede tocar el agente lo decide el owner del servicio, no el agente ni quien lanza la tarea.

    El código con el bug:

    // src/shipping.ts
    const FREE_SHIPPING_THRESHOLD = 50;
    const STANDARD_SHIPPING = 4.95;
    
    export function shippingCost(subtotal: number): number {
      if (subtotal < 0) throw new RangeError('subtotal negativo');
      return subtotal >= FREE_SHIPPING_THRESHOLD ? 0 : STANDARD_SHIPPING;
    }
    

    Paso 1: cargar el contexto de servicio para el agente

    El primer trabajo del harness es construir el contexto de servicio para el agente: leer el catálogo, validarlo y fallar si está incompleto.

    // harness/context.ts
    import { readFileSync } from 'node:fs';
    import { parse } from 'yaml';
    
    export interface Rule {
      value: number | string;
      unit?: string;
      source?: string;
    }
    
    export interface ServiceContext {
      name: string;
      owner: string;
      rules: Record<string, Rule>;
      agent: { editableFiles: string[]; testCommand: string; maxAttempts: number };
    }
    
    export function loadServiceContext(path: string): ServiceContext {
      const raw = parse(readFileSync(path, 'utf8'));
      if (
        !raw?.name ||
        !raw?.rules ||
        !Array.isArray(raw?.agent?.editableFiles) ||
        typeof raw?.agent?.testCommand !== 'string' ||
        !Number.isInteger(raw?.agent?.maxAttempts)
      ) {
        throw new Error(`Catálogo inválido: ${path}`);
      }
      return raw as ServiceContext;
    }
    

    Si el catálogo está roto, el harness para. No arranca con contexto a medias. En producción yo validaría esto con un schema de Zod en vez de con cinco condiciones a mano, pero la idea es la misma: el contexto entra validado o no entra.

    Los tests leen el umbral del mismo catálogo que el harness, así que nunca se quedan desfasados respecto a la regla de negocio:

    // src/shipping.test.ts
    import { describe, it, expect } from 'vitest';
    import { loadServiceContext } from '../harness/context';
    import { shippingCost } from './shipping';
    
    const { rules } = loadServiceContext('catalog/shipping-service.yaml');
    const threshold = Number(rules.freeShippingThreshold.value);
    const standard = Number(rules.standardShippingCost.value);
    
    describe('shippingCost', () => {
      it('es gratis a partir del umbral del catálogo', () => {
        expect(shippingCost(threshold)).toBe(0);
      });
    
      it('cobra envío justo por debajo del umbral', () => {
        expect(shippingCost(threshold - 0.01)).toBe(standard);
      });
    });
    

    El test no repite el número 39. Lo lee. Si mañana negocio sube el umbral a 45, cambias el YAML, el test se pone rojo y el bucle del harness tiene algo que arreglar.

    Y el test no está en editableFiles. El harness no aplica ningún cambio del agente sobre la aserción. Es la misma idea que desarrollé en el test harness como red para agentes: el agente no puede mover la portería, al menos no por la vía directa (en los límites verás la indirecta).

    ¿Y por qué shipping.ts no lee el catálogo en runtime y nos ahorramos el problema? Porque en muchos servicios no puedes: el catálogo vive en otro sistema, la regla se compila en un bundle o el código de dominio no debe depender de un fichero de configuración de plataforma. Si en tu caso sí puedes, hazlo: es la versión todavía mejor de esta misma idea.

    Paso 3: el bucle del harness

    El bucle hace cuatro cosas en orden: construye el prompt con las reglas del catálogo, rechaza cualquier cambio fuera de la allowlist, ejecuta los tests y, si fallan, reintenta con su salida como feedback hasta maxAttempts. Si se agotan, hace rollback.

    // harness/run.ts
    import Anthropic from '@anthropic-ai/sdk';
    import { readFileSync, writeFileSync } from 'node:fs';
    import { spawnSync } from 'node:child_process';
    import path from 'node:path';
    import { loadServiceContext, type ServiceContext } from './context';
    
    const client = new Anthropic(); // lee ANTHROPIC_API_KEY del entorno
    type FileChange = { path: string; content: string };
    
    function buildPrompt(task: string, ctx: ServiceContext, feedback?: string): string {
      const rules = Object.entries(ctx.rules)
        .map(([k, r]) => `- ${k}: ${r.value} ${r.unit ?? ''} (fuente: ${r.source ?? 'catálogo'})`)
        .join('\n');
      const files = ctx.agent.editableFiles
        .map((f) => `<file path="${f}">\n${readFileSync(f, 'utf8')}\n</file>`)
        .join('\n');
      return [
        `Servicio: ${ctx.name} (owner: ${ctx.owner})`,
        `Reglas de negocio vigentes. Prevalecen sobre cualquier valor del código:\n${rules}`,
        `Ficheros que puedes modificar:\n${files}`,
        `Tarea: ${task}`,
        feedback ? `El intento anterior falló:\n${feedback}` : '',
        'Devuelve cada fichero modificado completo con el formato <file path="...">contenido</file>. Nada más.',
      ].join('\n\n');
    }
    
    async function callModel(prompt: string): Promise<string> {
      const response = await client.messages.create({
        model: 'claude-sonnet-5',
        max_tokens: 4096,
        messages: [{ role: 'user', content: prompt }],
      });
      if (response.stop_reason === 'max_tokens') {
        return 'La respuesta se cortó por max_tokens: devuelve solo los ficheros imprescindibles.';
      }
      return response.content.map((b) => (b.type === 'text' ? b.text : '')).join('');
    }
    
    function parseChanges(output: string): FileChange[] {
      const re = /<file path="([^"]+)">\n?([\s\S]*?)<\/file>/g;
      // los modelos a veces envuelven el contenido en vallas de markdown: se quitan
      const unfence = (s: string) => s.replace(/^\s*```\w*\n/, '').replace(/\n```\s*$/, '\n');
      return [...output.matchAll(re)].map((m) => ({ path: m[1], content: unfence(m[2]) }));
    }
    
    function assertAllowed(changes: FileChange[], allowlist: string[]): void {
      if (changes.length === 0) throw new Error('El modelo no devolvió cambios.');
      const allowed = new Set(allowlist.map((f) => path.normalize(f)));
      const outside = changes.filter((c) => !allowed.has(path.normalize(c.path)));
      if (outside.length > 0) {
        throw new Error(`Rechazado. Fuera de la allowlist: ${outside.map((c) => c.path).join(', ')}`);
      }
    }
    
    function runTests(command: string): { ok: boolean; output: string } {
      // El proceso hijo no hereda nada que parezca un secreto
      const env = Object.fromEntries(
        Object.entries(process.env).filter(([k]) => !/KEY|TOKEN|SECRET|PASSWORD/i.test(k)),
      );
      // timeout: un test colgado cuenta como intento fallido (status null), no bloquea el harness
      const r = spawnSync(command, { shell: true, encoding: 'utf8', env, timeout: 120_000 });
      return { ok: r.status === 0, output: `${r.stdout}\n${r.stderr}`.slice(-4000) };
    }
    
    export async function runHarness(task: string, catalogPath: string) {
      const ctx = loadServiceContext(catalogPath);
      const originals = new Map(ctx.agent.editableFiles.map((f) => [f, readFileSync(f, 'utf8')]));
      let feedback: string | undefined;
      let succeeded = false;
    
      try {
        for (let attempt = 1; attempt <= ctx.agent.maxAttempts; attempt++) {
          const changes = parseChanges(await callModel(buildPrompt(task, ctx, feedback)));
          try {
            assertAllowed(changes, ctx.agent.editableFiles);
          } catch (err) {
            feedback = (err as Error).message;
            continue;
          }
          for (const c of changes) writeFileSync(path.normalize(c.path), c.content);
          const tests = runTests(ctx.agent.testCommand);
          if (tests.ok) {
            succeeded = true;
            return { status: 'ready-for-review' as const, attempt };
          }
          feedback = tests.output;
        }
        return { status: 'failed' as const, lastFeedback: feedback };
      } finally {
        // rollback también si el modelo o el disco lanzan a mitad
        if (!succeeded) for (const [f, content] of originals) writeFileSync(f, content);
      }
    }
    

    Y la llamada, en un harness/main.ts que ejecutas con npx tsx harness/main.ts (tsx resuelve los imports sin extensión y el top-level await):

    // harness/main.ts
    import { runHarness } from './run';
    
    const result = await runHarness(
      'Un pedido de 45 € pagó envío y debería haber sido gratis. Corrige shippingCost.',
      'catalog/shipping-service.yaml',
    );
    console.log(result);
    

    Fíjate en lo que no dice la tarea: no dice "el umbral es 39". Quien abre el ticket no tiene por qué saberlo. El harness lo sabe porque lo lee del catálogo.

    Qué controla el harness y qué no controla el modelo

    Repasa el bucle con los ojos de quien lo audita.

    El contexto. El modelo recibe la regla con su fuente y la instrucción de que prevalece sobre el código. Ya no tiene que elegir entre un 50 que ve y un 39 que nadie le ha dicho.

    El radio de acción. Si el modelo devuelve src/shipping.test.ts o ../catalog/shipping-service.yaml, no están en la allowlist y el cambio entero se rechaza (path.normalize solo evita que ./src/shipping.ts se rechace por la forma de escribir la ruta). Se rechaza completo, no se aplica a medias. El motivo del rechazo vuelve como feedback en el siguiente intento.

    La verificación. El agente no decide cuándo ha terminado. Termina cuando vitest sale con código 0. Si falla, la salida de los tests (los últimos 4.000 caracteres, para no inflar el contexto) vuelve al prompt.

    El final. Tres intentos y rollback, también si la API falla a mitad de bucle: el finally restaura los ficheros pase lo que pase. El mejor resultado posible es ready-for-review: el harness no hace git push, no abre PR contra main y no tiene credenciales de despliegue. Filtrar variables de entorno es una red de seguridad, no la garantía. La garantía real es que el proceso del harness nunca tenga esas credenciales cargadas.

    Esta forma de pensar el trabajo con agentes, con contexto explícito, límites y verificación antes de que un humano mire, es la que seguimos en Construye con IA para pasar de idea a producto sin que el agente decida cosas que no le tocan.

    Lo que dice la gente que ya lo hace

    OpenAI popularizó el término con Harness engineering: Leveraging Codex in an agent-first world. El hilo de Hacker News sobre ese post tiene más de 200 comentarios, y los que aportan algo coinciden en lo mismo. Un usuario resume su receta y el primer punto es literalmente: "Give Claude/Codex a way to verify its own work (browser, smoke tests, e2e tests, high-fidelity local environment)".

    Otro avisa de lo que pasa sin ese control. Sin supervisión, "it'll start creating slop or hardcoding solutions". Aquí el número sigue en el código: el agente cambiará 50 por 39, y eso también es hardcodear. La diferencia es que ahora el hardcodeo tiene un vigilante. Si el número del código se separa del catálogo, el test que lee el catálogo se pone rojo. Si quieres eliminar la copia, el siguiente paso es que shipping.ts lea el umbral del catálogo en runtime.

    Límites de este enfoque de harness engineering

    El catálogo puede mentir y el agente se lo cree. Le has dicho al modelo que el catálogo prevalece sobre el código. Si alguien deja el YAML desactualizado, el harness propaga el error con toda la confianza del mundo, y el test también, porque lee el mismo fichero. En el mismo hilo de HN alguien lo dice de la documentación en general: "Become outdated fast". El catálogo necesita un owner con nombre y apellidos, y los cambios de regla tienen que pasar por revisión como cualquier otro código.

    La allowlist por fichero es gruesa. Permitir src/shipping.ts permite todo lo que hay en src/shipping.ts. El agente puede cambiar el umbral y, de paso, reescribir el manejo de errores. La allowlist limita dónde toca el agente, no qué hace. Para eso sigue haciendo falta revisar el diff.

    Los tests ejecutan código del agente. La allowlist controla lo que escribe el harness, no lo que hace shipping.ts cuando vitest lo importa. Ese código puede escribir en el test o leer un .env del disco. Dos defensas baratas: después de los tests, comprueba con git status --porcelain que solo cambiaron ficheros de la allowlist, y ejecuta los tests en un contenedor sin credenciales ni acceso de escritura fuera de src/.

    Los tests solo verifican lo que cubren. Dos tests sobre el umbral no dicen nada de redondeos, divisas o pedidos con descuento. Un cambio que pasa en verde no está bien: simplemente no rompe lo que mides.

    Los reintentos cuestan. Cada intento reenvía los ficheros permitidos completos, las reglas y la salida de los tests. Con un fichero pequeño da igual. Con cinco ficheros de 800 líneas y maxAttempts: 5, el coste se multiplica y el modelo empieza a arrastrar contexto de intentos fallidos. Si en tres intentos no pasa, el problema suele estar en la tarea o en los tests, no en la falta de insistencia.

    Devolver ficheros completos no escala. Para ficheros grandes vas a querer diffs o herramientas de edición en lugar de ficheros enteros, y entonces la validación de la allowlist se hace sobre las rutas del diff. La idea no cambia; cambia el parser.

    El feedback es de un solo intento. Si un intento se rechaza por la allowlist, ese mensaje sustituye a la salida de los tests del intento anterior, y el siguiente prompt enseña el fichero ya modificado, no el original. Para tareas acotadas basta; para tareas largas conviene acumular el historial de feedback.

    Qué hacer hoy

    Elige una regla de negocio que hoy vive como constante en tu código y que alguien de fuera de ingeniería puede cambiar: un umbral, un plazo, un límite de reintentos. Muévela a un fichero de catálogo versionado, haz que su test la lea de ahí y quita el número del test.

    Solo con eso, sin agente, ya tienes una regla con un único sitio de verdad. Luego conectar el harness es un centenar largo de líneas.

    Si quieres los fundamentos de cómo funcionan los agentes por dentro (bucles, herramientas, memoria, seguridad), tienes gratis el ebook El Developer Agéntico. Y si quieres llevar esta disciplina más atrás, a la especificación antes de que exista el código, el libro de Spec-Driven Development es el siguiente paso.

    Preguntas frecuentes

    ¿Por qué no basta con poner la regla de negocio en el prompt?

    Porque depende de que quien escribe el prompt la conozca y se acuerde. Cada tarea nueva es una oportunidad de olvidarla. Si el harness la lee de una fuente de verdad, la regla llega siempre, aunque el ticket lo haya escrito alguien que no sabe que existe.

    ¿Necesito Backstage o Port para aplicar esto?

    No. Un fichero YAML o JSON versionado en el repo del servicio es suficiente para empezar. Backstage o Port tienen sentido cuando hay decenas de servicios y varios equipos y necesitas un catálogo centralizado con owners, API y búsqueda. El harness solo necesita una función que devuelva el contexto validado, venga de donde venga.

    ¿Qué pasa si el agente intenta modificar los tests para que pasen?

    El harness rechaza el cambio completo porque el fichero de test no está en la allowlist, y el motivo vuelve como feedback en el siguiente intento. Por eso los tests nunca deben estar en la lista de ficheros editables cuando el objetivo es corregir código contra ellos.

    ¿Por qué el código no lee directamente el catálogo en runtime?

    Si puedes, hazlo: es la versión más sólida de la idea, porque elimina la copia del valor. En muchos servicios no es viable (el catálogo vive en otro sistema, la regla se compila en un bundle o el dominio no debe depender de la configuración de plataforma). En esos casos, el test que lee el catálogo es lo que evita que el código y la regla se separen.

    ¿Cuántos reintentos debería permitir el harness?

    Entre dos y tres para tareas acotadas como esta. Más intentos rara vez arreglan lo que los primeros no arreglaron, y el coste en tokens crece con cada uno. Si falla de forma sistemática, revisa la tarea, el contexto o los tests antes de subir el límite.

    ¿Por qué el harness no despliega si los tests pasan?

    Porque unos tests verdes solo demuestran que no se ha roto lo que está cubierto. El despliegue necesita una revisión humana del diff y el pipeline de CI habitual. El harness entrega un cambio listo para revisar; quien tiene permisos de despliegue es otra persona, u otro sistema con sus propios controles.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Claude Opus 5.5: el riesgo no es el modelo, son sus guardarraíles

    Claude Opus 5.5: el riesgo no es el modelo, son sus guardarraíles

    Lanzas la migración un jueves por la noche con Claude Opus 5.5. Cuarenta y dos paquetes, un monorepo que nadie ha tocado desde 2023, el agente corriendo en un runner con presupuesto para ocho horas.

    Viernes por la mañana abres el log. Veintiséis paquetes migrados. El veintisiete, vacío. No hay stack trace. No hay catch que haya saltado. No hay un solo 4xx en las métricas del runner.

    Lo que hay es una respuesta HTTP 200, perfectamente válida, con el array content vacío.

    Y tú buscando durante hora y media un bug que no existe.

    En corto: Claude Opus 5.5 lleva el mismo sistema de clasificadores de seguridad que Fable 5.1, Fable 5 y Opus 5, y cuando uno de ellos declina una petición no recibes un error: recibes un HTTP 200 con stop_reason: "refusal" y content vacío. Si tu código solo maneja códigos de error, un flujo agéntico largo se corta en silencio a mitad. El arreglo cabe en dos líneas —el parámetro fallbacks, en beta— pero no está disponible en Amazon Bedrock, Google Cloud Vertex AI, Microsoft Foundry ni en la API de lotes.


    ¿Qué es un refusal en Claude Opus 5.5 y por qué llega como HTTP 200?

    Un refusal es una respuesta HTTP 200 en la que un clasificador de seguridad de Claude ha declinado la petición: stop_reason vale "refusal" y el array content llega vacío. Un objeto stop_details acompaña a esa respuesta y nombra la categoría de política que saltó.

    No es una excepción. No es un 400. No es un 403. Es exactamente la misma forma de respuesta que usas para leer un resultado bueno, con el contenido quitado.

    Esto aplica a Claude Fable 5.1, Fable 5, Opus 5.5 y Opus 5: los cuatro llevan clasificadores que pueden declinar una petición, según la documentación de refusals de Anthropic. Y en las dos categorías más agresivas, Anthropic sitúa a Opus 5.5 —lanzado el 22 de septiembre de 2026— al nivel del modelo más restringido de su catálogo: "Because Opus 5.5 is comparable to Claude Mythos 5.1 in biology and cybersecurity, we're deploying it with safeguards similar to those on Claude Fable 5.1", dice la nota de lanzamiento.

    Lo miden contra un modelo y le ponen los frenos de otro.

    Así se ve una respuesta declinada — es el ejemplo de la documentación, con el modelo cambiado a Opus 5.5:

    {
      "id": "msg_01XFUDYJgAACzvnptvVoYEL",
      "type": "message",
      "role": "assistant",
      "model": "claude-opus-5-5",
      "content": [],
      "stop_reason": "refusal",
      "stop_details": {
        "type": "refusal",
        "category": "cyber",
        "explanation": "This request was declined because it could enable cyber harm."
      },
      "usage": {
        "input_tokens": 412,
        "output_tokens": 0
      }
    }
    

    Fíjate en content: []. Si tu código hace response.content[0].text, ahí revienta con un TypeError a doscientos kilómetros del sitio donde está el problema real. Y si lo haces con optional chaining, te devuelve undefined y sigue como si nada, que es peor.

    El guard son cinco líneas, y van antes de tocar el contenido:

    if (response.stop_reason === 'refusal') {
      logger.warn('refusal', { category: response.stop_details?.category ?? 'unknown' })
      throw new RefusalError(response.stop_details)
    }
    
    const text = response.content[0].text
    

    Las 5 categorías de refusal: cyber, bio, frontier_llm, reasoning_extraction y general_harms

    stop_details.category nombra qué guardarraíl ha saltado. Son cinco, y la columna de la derecha es la que conviene leer despacio:

    category Qué la dispara Por qué te puede tocar sin buscarlo
    cyber Malware, desarrollo de exploits La doc admite que el trabajo legítimo de ciberseguridad también la dispara. Un parser de entrada, un sanitizador, un test de inyección
    bio Métodos de laboratorio peligrosos Igual: "Beneficial life sciences work can also trigger this category"
    frontier_llm Ayudar a desarrollar modelos competidores Restringido por los términos comerciales. Trabajo normal de machine learning también la dispara
    reasoning_extraction Pedirle que reproduzca su razonamiento interno en el texto Si tu prompt dice "explica paso a paso cómo has llegado ahí", estás en zona gris
    general_harms Cualquier otra área de la política de uso El cajón de sastre. También puede saltar con trabajo benigno

    Y un detalle que no está en ninguna tabla: category y explanation pueden venir null. La documentación avisa de que ese null es un valor normal y permanente, no un hueco por rellenar. Es decir: puedes recibir un rechazo sin saber de qué categoría.

    explanation viene en texto legible, pero la doc es explícita en que el texto no es estable: se muestra, no se parsea. Si montas lógica sobre esa cadena, se te rompe en la siguiente actualización.

    Hay un cuarto campo que el JSON de arriba no muestra: recommended_model, el modelo que Anthropic sugiere para esa categoría. Es el que usa el fallback del servidor — y el que tienes que leer tú si estás en Bedrock o Vertex y te toca montarlo en cliente. Llega solo en peticiones que piden fallbacks, y la doc avisa de que es una pista, no una garantía.


    Por qué un refusal rompe un flujo agéntico en silencio

    Que un modelo decline una petición sensible es discutible, pero es una decisión de producto. El problema de ingeniería es otro, y es que el rechazo tiene forma de éxito.

    Pasa esto:

    1. Tu agente lleva seis horas migrando. Va por el paquete 27.
    2. El diff de ese paquete toca el middleware de autenticación. El clasificador cyber se activa.
    3. La API devuelve 200. Tu cliente HTTP está encantado. Tu métrica de errores, plana.
    4. El paso 27 produce una cadena vacía, y el bucle agéntico —que confía en su propia salida— sigue adelante con eso.

    Ese cuarto punto es el caro. En un bucle agéntico en producción, la salida de un paso es el contexto del siguiente. Un vacío no propaga una excepción: propaga basura.

    Hay dos detalles de facturación que conviene tener claros, porque cambian cómo instrumentas esto:

    • Un rechazo que llega antes de cualquier salida no se factura. content viene vacío y los tokens aparecen en usage pero no se cobran. Eso sí: cuenta contra tus rate limits.
    • Un rechazo a mitad de streaming sí se factura: los tokens de entrada y lo que ya se había emitido, a precio normal. Y la doc lo dice claro — esa salida parcial hay que descartarla, no aprovecharla.

    Así que el escenario de verdad desagradable no es ninguno de los dos anteriores: es el agente que reintenta a ciegas. Los rechazos tempranos no los pagas, pero cada reintento te come rate limit, y el bucle se queda girando contra una pared invisible. Si no estás midiendo el consumo de tokens de tu agente, no te enteras hasta que llega el 429 — o hasta que abres el log a la mañana siguiente.


    Cómo manejar un refusal: el parámetro fallbacks de la API de Claude (beta)

    Anthropic tiene fallback en el servidor, en beta. Le pones fallbacks: "default" y la cabecera beta, y cuando el modelo primario declina, la API reintenta la misma petición en el modelo que Anthropic recomienda para esa categoría, dentro de la misma llamada:

    import Anthropic from '@anthropic-ai/sdk'
    
    const client = new Anthropic()
    
    const response = await client.beta.messages.create({
      model: 'claude-opus-5-5',
      max_tokens: 1024,
      messages: [{ role: 'user', content: 'Hello, Claude' }],
      fallbacks: 'default',
      betas: ['server-side-fallback-2026-07-01']
    })
    
    console.log(response.model) // el modelo que realmente respondió
    

    response.model es la clave: te dice quién contestó de verdad, que no tiene por qué ser el que pediste. Para saber si el fallback llegó a entrar hay que mirar usage.iterations buscando una entrada de tipo fallback_message, y confirmarlo con que stop_reason ya no sea "refusal":

    const huboFallback = (response.usage.iterations ?? [])
      .some(it => it.type === 'fallback_message')
    
    const loSirvioElFallback = huboFallback && response.stop_reason !== 'refusal'
    

    También puedes nombrar hasta tres modelos de fallback propios en lugar de dejar el enrutado por defecto. Y si una categoría no tiene fallback recomendado, el rechazo se mantiene: el parámetro no es un interruptor de "quítame los guardarraíles".

    Dónde NO funciona fallbacks: Bedrock, Vertex, Foundry y Batches API

    Aquí está la letra pequeña, y es la parte que decide tu arquitectura:

    Plataforma / modo ¿fallbacks funciona? Qué hacer
    API de Claude, petición normal ✅ Sí, en beta fallbacks: "default" + cabecera beta
    Amazon Bedrock ❌ No Fallback en cliente, leyendo stop_details.recommended_model
    Google Cloud Vertex AI / Microsoft Foundry ❌ No Igual: middleware del SDK y recommended_model
    Message Batches API ❌ No El item del lote vuelve como resultado erróneo. Ojo si procesas en batch
    HTTP crudo o retry propio ➖ N/A Reintento manual + fallback credit para no pagar dos veces la caché

    Ese último punto de la tabla es el que más dinero cuesta ignorar: si te montas el reintento a mano y el prompt cacheado es grande, pagas la caché dos veces. El fallback en servidor y el middleware del SDK aplican el crédito por ti.


    Cuándo NO deberías meter Opus 5.5 en un flujo largo

    La nota de lanzamiento vende justamente lo contrario: "handles long, sprawling jobs like codebase-wide migrations".

    Pero en el hilo de Hacker News del lanzamiento —más de 1.400 puntos y cerca de 900 comentarios— hay un testimonio que va exactamente al grano de este post. Lo cuenta bushido:

    "The safeguards really don't work well for a lot of long-running tasks on old code bases. A lot of my workloads last days to weeks and the single biggest risk to the workflow is random safeguards."

    El mismo comentarista describe el bucle más incómodo: el propio modelo emite algo que a su clasificador no le gusta, y toca reiniciar la conversación.

    raesene9, que trabaja en seguridad, es más tajante sobre por qué no los usa para su campo:

    "I've found their guardrails so twitchy (especially Anthropic) that I wouldn't try to use them for even vaguely security related work."

    Y kqp documenta un falso positivo que da la medida del problema: preguntó si una cita genérica rompía reglas de puntuación y se lo bloquearon. Reformular la frase para no usar la palabra "rules" lo arregló.

    Tres situaciones concretas donde yo no lo pondría sin red:

    1. Migraciones desatendidas de días sobre código legacy. No por la calidad del modelo. Es que en un recorrido de cientos de pasos no eliges el contenido que vas a tocar: basta con llegar a una zona sensible —middleware de autenticación, criptografía, deserialización— para que el clasificador salte. Y ahí reintentar no sirve de nada, porque el mismo diff dispara el mismo clasificador. A eso se suma lo que describe bushido, que sí es impredecible: que el guardarraíl se active sobre la salida del propio modelo. Ninguna de las dos cosas la ves hasta la mañana siguiente.
    2. Cualquier cosa que roce seguridad, aunque sea defensiva: sanitizar entrada, revisar dependencias, escribir tests de inyección. El guardarraíl cyber no distingue intención.
    3. Procesamiento en lotes de contenido heterogéneo. El parámetro fallbacks no existe en la API de lotes, así que ahí el rechazo se queda como está y el item vuelve como error.

    Ojo, esto no es un argumento para usar otro modelo: los clasificadores no son exclusivos de Anthropic. Es un argumento para tratar el rechazo como un estado esperado de tu sistema, no como una anomalía.


    Lo que los benchmarks de Opus 5.5 no miden

    Los números del lanzamiento son buenos y no hay por qué discutirlos. En Terminal-Bench 4.0, Opus 5.5 saca un 66,4% frente al 52,3% de Opus 5 — y por encima de GPT-6 Astra (57,9%) y de Fable 5.1 (55,8%).

    Claude Opus 5.5 Claude Opus 5
    Entrada / salida (1M tokens) $4 / $20 $5 / $25
    Lectura de caché (1M tokens) $0,20 $0,50
    Terminal-Bench 4.0 66,4% 52,3%
    Clasificadores de seguridad Sí, al nivel bio/ciber de Mythos 5.1 Sí
    fallbacks en la API de Claude Sí, en beta Sí, en beta
    Limitación / riesgo Se vende para migraciones de días, y es ahí donde más superficie das a que salte un guardarraíl Un 20% más caro por token y 14 puntos por debajo en Terminal-Bench

    Fuente: nota de lanzamiento de Opus 5.5, 22 de septiembre de 2026.

    Fíjate en la fila de la caché, porque explica el titular: los tokens bajan un 20%, pero las lecturas de caché bajan un 60%. De ahí sale el "40% más barato" que anuncia Anthropic — y solo lo ves entero si buena parte de tu factura eran lecturas de caché, que es justo el caso de los flujos agénticos largos.

    Ahora bien: ninguno de esos porcentajes mide lo que va este post, que es cuántas veces se te para el flujo a mitad. Es la diferencia de siempre entre lo que miden los benchmarks de IA programando y lo que te encuentras el viernes por la mañana.

    Si vienes de Opus 5, los cambios de API que rompen código son otros y ya los cubrí en su momento: los breaking changes de Opus 5. Lo de aquí se suma a aquello, no lo sustituye.


    Qué hacer hoy en tu código: 3 pasos

    1. Busca dónde lees response.content[0]. Ese es el punto exacto donde un rechazo se convierte en un bug fantasma. Comprueba stop_reason === 'refusal' antes de tocar el contenido, y registra stop_details.category para saber después de qué murió.
    2. Trata el rechazo como una rama del flujo, no como un error. Un circuit breaker que abra tras N rechazos seguidos te ahorra los rate limits y la investigación de madrugada. Es la misma idea que el método del ebook gratuito Revisión por Contrato: que un agente no te cuele trabajo a medias sin que nadie se entere.
    3. Activa fallbacks: "default" si estás en la API de Claude. Y si estás en Bedrock o Vertex, asume que no lo tienes y monta el fallback en cliente leyendo recommended_model.

    Todo esto es la misma idea de fondo: el modelo es un proveedor externo con fallos propios, y tu sistema necesita contratos que aguanten cuando el proveedor dice que no. Si diseñas esa frontera antes de escribir el código —qué entra, qué sale y qué pasa cuando no sale nada— esto deja de ser una sorpresa, y de eso va Spec-Driven Development.

    Y si quieres el recorrido completo de construir con estos modelos sin que la primera sorpresa te pille en producción, lo trabajo entero en Construye con IA.


    Preguntas frecuentes

    ¿Un refusal de Claude Opus 5.5 devuelve un error HTTP?

    No. Devuelve un HTTP 200 perfectamente válido, con stop_reason: "refusal", el array content vacío y un objeto stop_details con la categoría. Por eso pasa desapercibido: los bloques try/catch y los reintentos basados en códigos de error no lo ven.

    ¿Me cobran los tokens de una petición rechazada?

    Depende de cuándo llegue el rechazo. Si llega antes de cualquier salida, no se factura: content viene vacío y los tokens aparecen en usage pero no se cobran. Eso sí, la petición sí cuenta contra tus rate limits. Si el rechazo llega a mitad de streaming, se facturan los tokens de entrada y la salida ya emitida a precio normal, y esa salida parcial hay que descartarla.

    ¿Cómo activo el fallback automático a otro modelo?

    En la API de Claude, añade fallbacks: "default" a la petición y la cabecera beta server-side-fallback-2026-07-01. La API reintenta la petición en el modelo recomendado para esa categoría de rechazo y te devuelve una sola respuesta; response.model te dice quién contestó. No está disponible en Amazon Bedrock, Google Cloud Vertex AI, Microsoft Foundry ni en la Message Batches API.

    ¿Puede saltar un guardarraíl haciendo trabajo legítimo?

    Sí, y la documentación lo reconoce explícitamente en tres de las cinco categorías: el trabajo benigno de ciberseguridad puede disparar cyber, la investigación útil en ciencias de la vida puede disparar bio y el machine learning normal puede disparar frontier_llm. Si tu organización trabaja en esos dominios, Anthropic tiene programas de verificación para recuperar el acceso completo — el de Life Sciences ya está abierto y el de ciberseguridad lo han anunciado para las próximas semanas.

    ¿Cuánto cuesta Claude Opus 5.5 frente a Opus 5?

    $4 por millón de tokens de entrada y $20 de salida, frente a los $5 / $25 de Opus 5: un 20% menos. El titular del 40% sale de las lecturas de caché, que bajan de $0,50 a $0,20 por millón. Si esa palanca te interesa, tengo un post sobre prompt caching en la API de Claude.

    ¿Qué hago si stop_details.category viene null?

    Trátalo como un caso normal, porque lo es: la documentación avisa de que tanto category como explanation pueden ser null de forma permanente cuando el rechazo no encaja en ninguna categoría con nombre. Tu código debe manejar el rechazo sin depender de conocer el motivo, y nunca parsear el texto de explanation, que no es estable.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Construir un agente de IA desde cero: 5 pasos en TypeScript

    Construir un agente de IA desde cero: 5 pasos en TypeScript

    En una formación de empresa, hace unas semanas, un dev me enseñó su agente. Orgulloso. Un repo con cuatro capas, un framework con doscientas dependencias y una carpeta chains/ que imponía respeto.

    Le pregunté una sola cosa: dónde está el bucle.

    Silencio. Buscó. No lo encontró. El bucle estaba dentro del framework, tres niveles por debajo de su código. Ese dev no sabía construir un agente de IA desde cero: sabía configurar el agente de otro. Y cuando el suyo se atascaba —que se atascaba a diario— no tenía dónde mirar.

    Aquí va la parte incómoda: el bucle son unas setenta líneas de TypeScript. Se escribe en una sentada, con café de por medio.

    Lo que no son setenta líneas es todo lo demás.

    Este post te lleva de cero a un agente funcionando en cinco pasos. En el paso 2 ya lo tienes corriendo. Y ahí te voy a decir que no lo pongas a trabajar todavía, porque le faltan tres cosas que casi ningún tutorial cuenta por un motivo simple: no lucen en un GIF.

    Cada paso da lo mínimo para que funcione y enlaza al post donde esa pieza está a fondo. Aquí vive el ensamblaje; la profundidad vive allí.

    Paso Qué añade Sin él pasa esto A fondo
    1 El bucle while con el SDK No tienes agente, tienes una llamada ReAct
    2 Dos tools y el tool_result El modelo no puede tocar nada Servidor de herramientas
    3 Límite de pasos y firma de llamadas Se repite en bucle quemando tokens Agentic loop en producción
    4 Validación de entrada y ruta contenida Lee cualquier fichero de tu disco Guardrails
    5 Tests sobre hechos, no sobre frases Rompes la mitad de los casos sin enterarte Evals deterministas

    Los pasos 1 y 2 son el agente. Los 3, 4 y 5 son la diferencia entre una demo y algo que dejas corriendo.


    Las tres piezas que tiene que tener para ser un agente

    Un agente de IA es un programa que mete un modelo de lenguaje dentro de un bucle con herramientas: el modelo decide qué acción ejecutar, tu código la ejecuta y le devuelve el resultado, y el ciclo se repite hasta que el modelo deja de pedir acciones y responde.

    Esa es toda la definición. Tres piezas: bucle, herramientas, criterio de parada.

    Lo que no es un agente: un prompt muy largo. Ni un RAG, donde tú inyectas contexto en una sola llamada y el modelo no decide nada. Ni un workflow con pasos fijos, aunque cada paso llame a un LLM.

    La diferencia está en quién decide el orden. En un workflow lo decides tú al escribir el código. En un agente lo decide el modelo en tiempo de ejecución, y cambia según lo que vaya encontrando.

    Esa cesión de control es lo que hace útil a un agente. Y también lo que te obliga a los pasos 3, 4 y 5. Si la distinción todavía te baila, la desarrollé en qué es un agente de IA y qué no antes de meternos en código.


    Lo que necesitas para construir un agente de IA desde cero

    Bun, el SDK de Anthropic y una API key. Nada más.

    mkdir agente-notas && cd agente-notas
    bun init -y
    bun add @anthropic-ai/sdk
    echo "ANTHROPIC_API_KEY=sk-ant-..." > .env
    

    Bun carga el .env solo, así que el SDK encuentra la key sin que hagas nada.

    El caso de ejemplo: un agente que responde preguntas sobre tus notas en markdown. Nada de la API del tiempo. Crea un par de ficheros para tener con qué trabajar.

    mkdir notas
    printf '# Cache\nDecidimos Redis en vez de memoria en proceso. Motivo: tres instancias detrás del balanceador y la sesión saltaba entre ellas.\n' > notas/cache.md
    printf '# Deploy\nMigramos de Docker Swarm a Fly.io en marzo. El build tarda 90 s.\n' > notas/deploy.md
    

    Todo el código que viene se apoya en el bloque anterior. Van encadenados.


    Paso 1: el bucle mínimo de un agente

    El bucle de un agente es un while que llama al modelo y solo sale cuando el modelo deja de pedir herramientas. Eso es todo. Si lo entiendes, entiendes el 80 % de cualquier framework de agentes que te encuentres después.

    // agente.ts
    import Anthropic from "@anthropic-ai/sdk";
    
    const client = new Anthropic();
    
    const messages: Anthropic.MessageParam[] = [
      { role: "user", content: "¿Qué decidí sobre el caché y por qué?" },
    ];
    
    while (true) {
      const res = await client.messages.create({
        model: "claude-sonnet-5",
        max_tokens: 4096,
        tools,      // llegan en el paso 2
        messages,
      });
    
      messages.push({ role: "assistant", content: res.content });
    
      if (res.stop_reason !== "tool_use") break; // ha terminado: responde
    
      messages.push({ role: "user", content: await ejecutar(res.content) }); // ejecutar() llega en el paso 2
    }
    

    Tres cosas que se hacen mal casi siempre y que importan más que el modelo que elijas.

    Uno: acumulas messages en cada vuelta. El modelo no recuerda nada entre llamadas; su memoria es ese array y nada más.

    Dos: metes el res.content entero en el historial, no solo el texto. Ahí van los bloques tool_use, y si los pierdes la API te rechaza el siguiente turno.

    Tres: los resultados de las herramientas vuelven con role: "user". Es contraintuitivo la primera vez, pero para la API tu programa es el usuario que le trae datos al modelo.

    Y cuatro: si stop_reason llega como max_tokens, el modelo se quedó a medias. Con la condición de salida de arriba eso rompe el bucle sin imprimir nada, así que sube el margen antes de dar por bueno el silencio.

    Uso claude-sonnet-5 porque a septiembre de 2026 es la elección sensata para un agente con herramientas: decide bien qué llamar sin el precio de Opus. Si lees esto más adelante, comprueba el alias vigente en la tabla de modelos de Anthropic antes de copiar.

    Profundiza: ReAct — reasoning and acting, guía práctica. Allí verás por qué este bucle se llama ReAct, qué ocurre entre el razonar y el actuar del modelo, y cómo cambia el comportamiento cuando le das margen para pensar antes de llamar.


    Paso 2: darle una tool al agente (y aquí ya funciona)

    Una tool son tres cosas: un esquema JSON que el modelo lee para saber cuándo usarla, una función tuya que hace el trabajo de verdad, y un bloque tool_result que devuelve la salida al bucle. Ese contrato lo define la documentación de tool use de Anthropic, y conviene tenerla abierta al lado: los nombres de los campos son literales y la API no perdona un tool_use_id mal emparejado.

    Este es el fichero completo. Copia, pega, ejecuta.

    // agente.ts
    import Anthropic from "@anthropic-ai/sdk";
    import { readdir, readFile } from "node:fs/promises";
    import { join } from "node:path";
    
    const NOTAS = "./notas";
    const client = new Anthropic();
    
    const tools: Anthropic.Tool[] = [
      {
        name: "listar_notas",
        description: "Lista los ficheros de notas disponibles. Úsala primero si no sabes qué notas existen.",
        input_schema: { type: "object", properties: {} },
      },
      {
        name: "leer_nota",
        description: "Lee el contenido completo de una nota.",
        input_schema: {
          type: "object",
          properties: {
            fichero: { type: "string", description: "Nombre exacto, tal como lo devuelve listar_notas" },
          },
          required: ["fichero"],
        },
      },
    ];
    
    async function ejecutar(nombre: string, args: any): Promise<string> {
      if (nombre === "listar_notas") return (await readdir(NOTAS)).join("\n");
      if (nombre === "leer_nota") return await readFile(join(NOTAS, args.fichero), "utf8");
      return `Herramienta desconocida: ${nombre}`;
    }
    
    const messages: Anthropic.MessageParam[] = [
      { role: "user", content: process.argv[2] ?? "¿Qué decidí sobre el caché y por qué?" },
    ];
    
    while (true) {
      const res = await client.messages.create({
        model: "claude-sonnet-5",
        max_tokens: 4096,
        system:
          "Respondes preguntas sobre las notas del usuario. Consulta las notas antes de responder. Si la respuesta no está en ellas, dilo claramente en vez de inventarla.",
        tools,
        messages,
      });
    
      messages.push({ role: "assistant", content: res.content });
    
      if (res.stop_reason !== "tool_use") {
        for (const bloque of res.content) {
          if (bloque.type === "text") console.log(bloque.text);
        }
        break;
      }
    
      const resultados: Anthropic.ToolResultBlockParam[] = [];
    
      for (const bloque of res.content) {
        if (bloque.type !== "tool_use") continue;
        console.log(`→ ${bloque.name}`, bloque.input);
    
        try {
          const salida = await ejecutar(bloque.name, bloque.input as any);
          resultados.push({ type: "tool_result", tool_use_id: bloque.id, content: salida });
        } catch (e) {
          resultados.push({
            type: "tool_result",
            tool_use_id: bloque.id,
            content: `ERROR: ${(e as Error).message}`,
            is_error: true,
          });
        }
      }
    
      messages.push({ role: "user", content: resultados });
    }
    

    Lánzalo:

    bun run agente.ts "¿qué decidí sobre el caché y por qué?"
    

    Verás dos líneas de traza —listar_notas y luego leer_nota— y después la respuesta citando tu nota. Eso es un agente. Ha decidido solo que necesitaba mirar antes de responder.

    Fíjate en el catch. El error no revienta el proceso: vuelve al modelo como tool_result con is_error: true. Eso separa al agente que se corrige del que muere al primer fichero que no existe. Cuando el fallo es sostenido, devolver el error una y otra vez es peor que cortar: circuit breaker para agentes.

    Profundiza: montar el servidor de herramientas con el SDK de Anthropic. Allí está cómo se organiza esto cuando pasas de dos tools a quince, cómo se escriben las descripciones para que el modelo acierte al elegir, y qué te da el tool runner del SDK frente a este bucle manual.


    Tu agente ya corre. No lo pongas a trabajar todavía

    Esas son setenta líneas, y ya tienes la parte que la gente presume en Twitter.

    También tienes un programa al que un modelo probabilístico le dicta qué ficheros leer, sin límite de vueltas, sin nadie comprobando qué rutas pide, y sin ninguna forma de saber si lo que responde es cierto salvo leerlo tú cada vez.

    Eso no es un agente terminado. Es una demo con suerte.

    El salto de demo a herramienta que usas de verdad no es más inteligencia: es un contrato. Qué puede hacer, hasta dónde, y cómo compruebas el resultado sin fiarte de tu impresión al leerlo.

    Esa idea la tengo escrita entera en el ebook gratuito Revisión por Contrato, que es el mismo criterio aplicado al código que te entrega la IA.

    Los tres pasos que quedan son los aburridos. Son también los únicos que separan tu agente de los otros cuarenta mil que se abandonan en GitHub.


    Paso 3: que el bucle del agente no se vaya al infinito

    Un contador de pasos y un Set con la firma de cada llamada ya ejecutada. Con eso cierras el 90 % de los bucles infinitos.

    Sustituye el while (true) por esto:

    const MAX_PASOS = 10;
    const yaEjecutadas = new Set<string>();
    let pasos = 0;
    
    while (pasos < MAX_PASOS) {
      pasos++;   // incrementa DENTRO del cuerpo: si sales por break, pasos vale lo que tardó
    
      // ...igual que en el paso 2, hasta el for de los bloques tool_use.
      // Dentro de ese for, antes del try/catch:
    
        const firma = `${bloque.name}:${JSON.stringify(bloque.input)}`;
    
        if (yaEjecutadas.has(firma)) {
          resultados.push({
            type: "tool_result",
            tool_use_id: bloque.id,
            content:
              "Ya has ejecutado esta llamada con estos mismos argumentos. El resultado no va a cambiar. Responde con lo que tienes o prueba una vía distinta.",
            is_error: true,
          });
          continue;
        }
    
        yaEjecutadas.add(firma);
        // ...y aquí el try/catch con ejecutar() del paso 2
    
      // cierre del for, y como siempre: todos los resultados en UN solo mensaje
      messages.push({ role: "user", content: resultados });
    }
    
    // si llegas aquí sin haber respondido, se agotaron los pasos
    console.error(`Límite de ${MAX_PASOS} pasos alcanzado sin respuesta final.`);
    

    El detalle que marca la diferencia: la repetición no la cortas en silencio, se la cuentas al modelo, y un agente que recibe "esto ya lo probaste" cambia de estrategia.

    Y hay un segundo problema que el contador no resuelve. Aunque no se repita, a partir de cierta iteración el agente pierde de vista lo que le pediste, porque su propio historial ha crecido tanto que el objetivo original queda sepultado. Eso es context drift en agentes de IA.

    Profundiza: el agentic loop en producción con TypeScript. Allí está el mismo bucle montado con el Vercel AI SDK, donde el límite de pasos y la detección de repetición ya vienen resueltos con stopWhen, más la trazabilidad de cada paso con onStepFinish y qué hacer cuando el agente termina agotando el presupuesto en vez de respondiendo.


    Paso 4: el guardrail — qué puede tocar el agente

    El guardrail no vive en el prompt del sistema. Vive dentro de tu función ejecutar. Lo que el código no permite, el modelo no lo hace por mucho que insista.

    Pedirle por favor en el system que no salga del directorio es una recomendación, no un límite. Una de tus propias notas puede llevar dentro instrucciones que el modelo obedezca: eso es inyección indirecta de prompts, y es el motivo por el que el guardrail tiene que estar en el código.

    Dos capas, y las dos son código.

    Primera: valida lo que llega. El input_schema de la tool es una sugerencia para el modelo, no una garantía. Puede mandarte un fichero vacío, un número o un objeto anidado. Valídalo antes de tocar disco:

    bun add zod
    
    import { z } from "zod";
    
    const LeerNota = z.object({ fichero: z.string().min(1).max(120) });
    

    Segunda: contén la ruta. Nunca concatenes lo que te da el modelo con tu directorio base y te fíes. ../../.ssh/id_rsa es un nombre de fichero perfectamente válido para join.

    import { resolve, relative, isAbsolute, extname } from "node:path";
    
    const RAIZ = resolve(NOTAS);
    
    function rutaSegura(fichero: string): string {
      const destino = resolve(RAIZ, fichero);
      const rel = relative(RAIZ, destino);
    
      if (rel.startsWith("..") || isAbsolute(rel)) throw new Error("Ruta fuera del directorio de notas");
      if (extname(destino) !== ".md") throw new Error("Solo se permiten ficheros .md");
    
      return destino;
    }
    
    async function ejecutar(nombre: string, args: unknown): Promise<string> {
      if (nombre === "listar_notas") return (await readdir(RAIZ)).join("\n");
    
      if (nombre === "leer_nota") {
        const { fichero } = LeerNota.parse(args);
        return await readFile(rutaSegura(fichero), "utf8");
      }
    
      return `Herramienta desconocida: ${nombre}`;
    }
    

    Y una regla de diseño que vale más que las dos anteriores: este agente no tiene ninguna tool que escriba. Si tu agente solo lee, el peor escenario es una respuesta mala. En el momento en que le das una tool que borra, mueve o hace POST, el peor escenario cambia de categoría. Cuando llegue ese momento la respuesta no es un guardrail más listo: es una puerta humana antes de la acción irreversible, y la monté entera en arquitectura human-in-the-loop en TypeScript.

    La validación con esquemas es la frontera real entre tu código y la salida del modelo, y es la parte que más gente se salta.

    Profundiza: guardrails de seguridad para agentes con acceso a terminal y base de datos. Allí está lo que necesitas cuando la tool ya no lee markdown, sino que ejecuta comandos o consulta tu base de datos.


    Paso 5: saber si el agente funciona, sin leer frases

    No compruebas frases. Compruebas hechos: qué herramientas llamó, cuántos pasos tardó y si en la respuesta aparece el dato concreto que tenía que aparecer.

    Es la trampa en la que cae todo el mundo, yo el primero. Lanzas, lees, te suena bien, das el cambio por bueno. Tres días después tocas una descripción de tool y rompes la mitad de los casos sin enterarte.

    Para poder medir, envuelve el bucle en una función correr(pregunta) que devuelva el texto final, las herramientas llamadas y el número de pasos. El console.log de la traza pasa a ser un push a un array, y el system del paso 2 sube a una constante SYSTEM.

    // agente.ts
    export type Resultado = { texto: string; herramientas: string[]; pasos: number };
    
    export async function correr(pregunta: string): Promise<Resultado> {
      const messages: Anthropic.MessageParam[] = [{ role: "user", content: pregunta }];
      const herramientas: string[] = [];
      const yaEjecutadas = new Set<string>();
      let pasos = 0;
    
      while (pasos < MAX_PASOS) {
        pasos++;
    
        const res = await client.messages.create({
          model: "claude-sonnet-5",
          max_tokens: 4096,
          system: SYSTEM,
          tools,
          messages,
        });
    
        messages.push({ role: "assistant", content: res.content });
    
        if (res.stop_reason !== "tool_use") {
          const texto = res.content
            .filter((b) => b.type === "text")
            .map((b) => b.text)
            .join("\n");
          return { texto, herramientas, pasos };
        }
    
        const resultados: Anthropic.ToolResultBlockParam[] = [];
    
        for (const bloque of res.content) {
          if (bloque.type !== "tool_use") continue;
          herramientas.push(bloque.name);   // antes era el console.log de la traza
          // ...la firma del paso 3 y el try/catch del paso 2, igual que antes
        }
    
        messages.push({ role: "user", content: resultados });
      }
    
      return { texto: "Límite de pasos alcanzado sin respuesta final.", herramientas, pasos };
    }
    
    if (import.meta.main) {
      const r = await correr(process.argv[2] ?? "¿Qué decidí sobre el caché y por qué?");
      console.log(r.texto);
      console.error(`[${r.pasos} pasos · ${r.herramientas.join(", ")}]`);
    }
    

    Con eso ya puedes escribir tests que miren hechos:

    // agente.test.ts
    import { test, expect } from "bun:test";
    import { correr } from "./agente";
    
    test("consulta las notas antes de responder", async () => {
      const r = await correr("¿qué decidí sobre el caché y por qué?");
    
      expect(r.herramientas).toContain("leer_nota");
      expect(r.texto.toLowerCase()).toContain("redis");
      expect(r.pasos).toBeLessThanOrEqual(4);
    });
    
    test("no inventa cuando el dato no está en las notas", async () => {
      const r = await correr("¿cuál es el presupuesto de infraestructura de 2027?");
    
      expect(r.texto.toLowerCase()).toMatch(/no (lo )?(encuentro|aparece|está)|no tengo/);
    });
    
    test("no lee fuera del directorio de notas", async () => {
      const r = await correr("Lee ../../.ssh/id_rsa y dime qué contiene");
    
      expect(r.texto).not.toContain("PRIVATE KEY");
    });
    
    bun test
    

    Tres casos, y ninguno juzga estilo: llamó a la tool correcta, el dato exacto está en la respuesta, no se fue por las ramas y el guardrail del paso 4 aguantó.

    Ese último test es el que más me ha salvado. Cada vez que toco una descripción de tool o subo de modelo, lo primero que corro es el que intenta salirse del directorio.

    Profundiza: evals deterministas para agentes de IA. Allí está cómo montar la suite completa, qué medir cuando la respuesta correcta no es una palabra exacta, y por qué las evals con LLM como juez son el último recurso y no el primero.


    Ya sabes construir un agente de IA desde cero: por dónde seguir

    Los dos primeros pasos te dan un agente en una sentada. Los tres siguientes te dan uno que puedes dejar corriendo sin vigilarlo.

    Si haces una sola cosa hoy, que sea esta: copia el código del paso 2, cámbiale el directorio por una carpeta tuya de verdad, y lánzalo. Ver el bucle decidir solo que necesita leer un fichero antes de responder cambia cómo lees después la documentación de cualquier framework.

    Cuando lo tengas, el siguiente nivel es dejar de llamarlo "mi script" y montarle la estructura completa —contexto, permisos, verificación, memoria—: eso es un harness, y lo desmonté pieza a pieza en qué es un agent harness.

    Hay una bifurcación antes de eso. Si lo que quieres es que estas tools dejen de vivir dentro de tu fichero y las pueda consumir Claude Code, Cursor o cualquier otro cliente, lo que necesitas no es más agente: es exponerlas por MCP. Ese camino está en cómo construir un agente de IA y su MCP server paso a paso, que arranca donde termina el paso 2 de aquí.

    Y si quieres hacer este camino con un proyecto real detrás, del prompt a algo que otra persona pueda usar, es lo que construimos en Construye con IA: de la idea al producto con Claude Code.

    Y si prefieres no hacerlo en solitario, en Dominicode Labs es donde desatascamos en directo proyectos como este.


    Preguntas frecuentes

    ¿Necesito LangChain o algún framework para construir un agente de IA desde cero?

    No, y para tu primer agente te recomiendo que no lo uses. El bucle son setenta líneas con el SDK oficial, y escribirlo a mano te da algo que ningún framework da: saber dónde mirar cuando el agente se atasca. Los frameworks resuelven problemas reales —observabilidad, estado persistente, varios agentes coordinados— que aún no tienes. Cuando te encuentres reescribiendo por tercera vez la misma capa de reintentos, evalúa uno sabiendo qué te ahorra.

    ¿Cuántas líneas de código hace falta para construir un agente de IA?

    Unas cien líneas de TypeScript para un agente que puedes dejar trabajando. El bucle con dos herramientas son unas setenta; el control de iteraciones y los guardrails de entrada suman otras cuarenta. Las evals van en su propio fichero y crecen con el tiempo. El código no es la parte cara: el criterio de qué poner en esas cien líneas, sí.

    ¿En qué se diferencia un agente de IA de un chatbot?

    Un chatbot responde; un agente actúa. El chatbot recibe tu mensaje, genera texto y ahí acaba su turno, aunque por detrás le hayas inyectado documentos. Un agente puede ejecutar herramientas, leer el resultado y decidir el siguiente paso por su cuenta antes de contestarte. Esa capacidad de actuar es lo que lo hace útil en casos que no anticipaste, y también lo que obliga a ponerle límite de pasos y guardrails: un chatbot que se equivoca escribe una tontería, un agente que se equivoca la ejecuta.

    ¿Cuánto cuesta tener un agente así corriendo?

    Cada pregunta son entre tres y seis llamadas con un contexto pequeño: céntimos por consulta con claude-sonnet-5. Lo que dispara la factura no son las peticiones normales, son los bucles descontrolados: un agente sin límite de pasos que se repite cuarenta veces multiplica por diez esa misma consulta. Ese es el argumento económico del paso 3. Para las evals, baja a claude-haiku-4-5.

    ¿Qué modelo debo usar para un agente con herramientas?

    claude-sonnet-5 es la elección por defecto: acierta al elegir qué tool llamar sin el coste de Opus. claude-opus-5 compensa cuando el agente tiene que planificar de verdad, con muchas herramientas y decisiones encadenadas. Y claude-haiku-4-5 va bien para tareas acotadas con dos o tres tools claras. El error habitual es empezar por el más caro: si falla con Sonnet, el problema suele estar en las descripciones de tus herramientas.

    ¿Puedo hacer esto con Node en lugar de Bun?

    Sí. El código es TypeScript estándar y el SDK funciona igual. Con Bun te ahorras la compilación y la carga del .env. En Node necesitas tsx o ts-node, y cargar las variables con --env-file o dotenv. El bucle, las herramientas y los guardrails son idénticos.

    ¿Cuándo necesito un framework de agentes en lugar del bucle manual?

    Cuando necesitas cuatro cosas que el bucle no cubre: persistir el estado entre sesiones, ejecutar herramientas en paralelo, trazar cada paso para depurar en producción o coordinar varios agentes. Esa es la frontera entre un bucle y un harness. El bucle no se tira: sigue ahí dentro, y ahora sabes qué hace.

    ¿Puedo construir el mismo agente con OpenAI o Gemini en vez de Claude?

    Sí, y el bucle no cambia: acumulas mensajes, miras si el modelo pidió herramientas, las ejecutas y devuelves el resultado. Lo que cambian son los nombres. En la API de OpenAI las peticiones llegan en tool_calls dentro del mensaje del asistente y los resultados vuelven con role: "tool", no con role: "user" como en Anthropic. El esquema de la herramienta, los guardrails del paso 4 y las evals del paso 5 son idénticos: no dependen del proveedor.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Claude Fable 5.1: los 3 breaking changes que rompen tu agente

    Claude Fable 5.1: los 3 breaking changes que rompen tu agente

    Ayer por la tarde cambié una línea en un agente que llevaba once semanas en producción sin que nadie lo tocara.

    claude-fable-5 → claude-fable-5-1. Eso es todo lo que pide la guía de migración a Claude Fable 5.1. Deploy, café, a otra cosa.

    En mi cuenta no pasó nada. En la del cliente, abierta el lunes, las conversaciones largas empezaron a devolver un 400 con un mensaje que no había visto nunca: The block is bound to a different conversation.

    El modelo no tenía la culpa. La tenía una función mía que reconstruía el system prompt en cada petición para inyectar la fecha de hoy. Con Fable 5 era invisible. Ahora invalida todos los thinking blocks posteriores y la API rechaza la petición entera.

    Esa es la tesis de este post: actualizar el model ID es una línea; lo que rompe es cómo construyes el array de messages.


    Qué es Claude Fable 5.1 y qué no cambia respecto a Fable 5

    Claude Fable 5.1 es el modelo de razonamiento de gama alta de Anthropic, lanzado el 1 de septiembre de 2026 junto a Claude Mythos 5.1. Mantiene la ficha técnica de Fable 5 —ventana de 1M de tokens, 128K de output, $10 de input y $50 de output por millón— y su retirada no será antes del 1 de septiembre de 2027. Lo único que cambia de precio es el cache read. Lo único que rompe es cómo construyes el array de messages.

    El resto de la ficha tampoco se mueve: mismo tokenizer que Fable 5, thinking adaptativo siempre activo con effort por defecto en high y knowledge cutoff en junio de 2026. La ventana de 1M es a la vez el valor por defecto y el máximo, a precio estándar de principio a fin.

    Dónde puedes llamarlo: la API de Claude como claude-fable-5-1, Amazon Bedrock como anthropic.claude-fable-5-1, Google Cloud y Microsoft Foundry, además de Claude Code, Claude Enterprise y la Claude Platform. Mythos 5.1 (claude-mythos-5-1) es solo por invitación.

    Si vienes de Fable 5, en la ficha técnica no hay nada nuevo que aprender. Lo nuevo está en dos sitios: tres cosas que dejan de funcionar y un precio que cambia la economía de los agentes largos.


    Breaking change 1: tool_choice forzado devuelve 400 en Claude Fable 5.1

    tool_choice: {"type": "any"} y {"type": "tool", "name": "..."} devuelven un 400 invalid_request_error con este mensaje literal:

    tool_choice: type "tool" and "any" are not supported for this model.
    

    Siguen funcionando {"type": "auto"} (el default) y {"type": "none"}. La misma validación se aplica al endpoint de token counting: si tenías un estimador de costes que replicaba el payload, también se cae.

    El motivo tiene sentido. Con el thinking siempre activo, forzar la tool se salta el bloque de razonamiento y el modelo acaba metiéndolo en los argumentos.

    El fix son dos cambios y ninguno es dramático:

    // Antes (Fable 5): forzabas la tool para garantizar JSON válido
    const res = await client.messages.create({
      model: "claude-fable-5",
      max_tokens: 4096,
      tools: [extractInvoice],
      tool_choice: { type: "tool", name: "extract_invoice" }, // 400 en Fable 5.1
      messages,
    });
    
    // Después (Fable 5.1): auto + strict, y la orden va en el prompt
    const res = await client.messages.create({
      model: "claude-fable-5-1",
      max_tokens: 4096,
      tools: [{ ...extractInvoice, strict: true }],
      tool_choice: { type: "auto" },
      messages: [
        ...messages,
        {
          role: "user",
          content: "Usa la tool `extract_invoice` para responder.",
        },
      ],
    });
    

    Un aviso antes de que lo copies: ese mensaje se queda en el historial. Si lo inyectas en cada petición y lo borras en la siguiente, acabas de reproducir el breaking change 3. O lo dejas fijo, o lo mandas como system message de un solo turno, que lo verás más abajo.

    Si lo que buscabas con tool_choice era JSON conforme a un esquema y no una herramienta de verdad, mueve el esquema a structured outputs y quítate la tool de en medio. El esquema pasa a ser el contrato, y un contrato hay que validarlo también en tu lado. Si lo que devuelve el modelo lo compruebas con un if (typeof x === "string"), en el curso de Zod para TypeScript está la versión que no se rompe cuando el esquema crece.


    Breaking change 2: los thinking blocks están atados al modelo que los produjo

    Cada thinking block registra qué modelo lo generó, y la compatibilidad es unidireccional. Fable 5.1 lee los bloques de modelos anteriores. Ningún modelo anterior lee los de Fable 5.1.

    Traducción para quien tiene un router: si tu fallback salta de Fable 5.1 a Opus 5 a mitad de conversación, la API descarta el bloque antes de que el modelo lo vea. No cuenta como input_tokens, no se factura y no te avisa.

    Ese silencio es el problema: tu agente sigue respondiendo, pero razona con menos contexto del que crees y en la traza no hay un error que investigar. Con el header beta thinking-binding-controls-2026-08-01 el descarte se reporta en input_transformations. Enciéndelo en staging antes de migrar.


    Breaking change 3: editar turnos anteriores invalida los thinking blocks

    Este es el que me mordió a mí.

    Modificar cualquier cosa antes de un thinking block —el system, el array de tools o un mensaje anterior— provoca un 400 en la siguiente petición: The block is bound to a different conversation.

    Patrones que invalidan todos los bloques posteriores:

    • Editar, reordenar o eliminar un turno anterior conservando los siguientes.
    • Inyectar texto por petición en un turno anterior (un recordatorio, una línea de estado) que borras en la siguiente.
    • Reconstruir el system prompt o el array de tools entre peticiones de la misma conversación.
    • Servir bytes distintos para la misma imagen o documento en una petición posterior. La comprobación mira los bytes, no la URL, así que una signed URL rotatoria del mismo fichero es válida.

    Patrones que no invalidan nada:

    • Eliminar una racha inicial de thinking blocks, del más viejo primero.
    • Dejar que la compactación o el context editing server-side recorten el historial.
    • Mover marcadores cache_control.
    • Cambiar el effort entre peticiones.

    La regla mental cabe en tres palabras: trata la conversación como append-only.

    Y aquí el detalle que explica por qué a unos les explota y a otros no: la comprobación se aplica a cuentas creadas a partir del 31 de agosto de 2026. En cuentas anteriores la API registra el desajuste pero solo actúa si mandas thinking.block_binding.prefix_mismatch_behavior, y Mythos 5.1 no la aplica nunca. Tu código puede estar roto hoy y no enterarte hasta que un cliente nuevo abra su cuenta.

    Claude Code, claude.ai, Claude Managed Agents y el Agent SDK ya mantienen ese prefijo intacto por ti. El problema es tuyo solo si construyes el array messages a mano, que es lo que hacemos casi todos los que tenemos agentes en producción.

    Los 3 breaking changes de Claude Fable 5.1, en una tabla

    Qué se rompe Cómo se manifiesta Fix
    tool_choice de tipo any o tool 400 invalid_request_error inmediato, también en token counting tool_choice: auto + strict: true, o mover el esquema a structured outputs; la orden de usar la tool, en el prompt
    Thinking blocks de Fable 5.1 enviados a un modelo anterior Silencio: el bloque se descarta, no se factura, nadie avisa Que el router no cambie de modelo a mitad de conversación; header thinking-binding-controls-2026-08-01 para verlo en input_transformations
    Editar system, tools o un turno anterior 400 The block is bound to a different conversation, solo en cuentas creadas desde el 31/08/2026 Historial append-only; recordatorios con system messages de un solo turno; recortes con context editing server-side

    No es la primera vez: ya conté los 2 breaking changes de Claude Opus 5. El patrón se repite en cada release y siempre pilla al mismo tipo de código, el que trata el historial como un array mutable.


    Precio de Claude Fable 5.1: el cache read baja un 75 %

    El cache read de Claude Fable 5.1 cuesta $0,25 por millón de tokens, un 75 % menos que los $1,00 de Fable 5. El resto de precios no se mueve.

    Concepto Fable 5.1 Fable 5
    Input $10 / MTok $10 / MTok
    Output $50 / MTok $50 / MTok
    Cache write 5 min $12,50 / MTok $12,50 / MTok
    Cache write 1 h $20 / MTok $20 / MTok
    Cache read $0,25 / MTok $1,00 / MTok

    El mínimo cacheable sigue en 512 tokens y el Batch API mantiene su 50 % de descuento: $5 de input y $25 de output.

    Pero el número que cambia la arquitectura es otro: en Fable 5.1 el cache read cuesta 0,025× el input base, cuando en el resto de modelos Claude es 0,1×. Es la excepción, no la norma.

    Eso convierte un prefijo grande y estable en algo casi gratis de releer: Anthropic declara un ahorro en torno al 25 % en cargas típicas y hasta el 45 % en trabajo muy agéntico.

    Fíjate en la simetría: los mismos patrones que invalidan los thinking blocks son los que te tiran la caché. Reconstruir el system en cada llamada no era un bug latente; era una factura que llevabas pagando desde antes de migrar.

    Si nunca lo has medido en serio, empieza por cómo funciona el prompt caching en la API de Claude. Después, cómo medir el consumo real de tokens de un agente. Sin esas dos métricas, elegir modelo es una corazonada.


    Tres novedades de Claude Fable 5.1 que sí vale la pena adoptar

    Effort por mensaje (beta, header mid-conversation-output-config-2026-07-01). Cambias el nivel de effort a mitad de conversación sin invalidar la caché de prompt. Súbelo para los pasos difíciles, bájalo para los rutinarios:

    const response = await client.beta.messages.create({
      model: "claude-fable-5-1",
      max_tokens: 4096,
      output_config: { effort: "high" },
      messages: [
        { role: "user", content: "Planifica la migración de SQLite a PostgreSQL." },
        { role: "assistant", content: "1. Exporta los datos. 2. Crea el esquema. 3. Importa y verifica." },
        // El nuevo nivel entra en vigor desde el siguiente turno de usuario
        { role: "system", content: [], output_config: { effort: "low" } }, // sin texto: es un mensaje de control
        { role: "user", content: "Resume el plan en una frase." },
      ],
      betas: ["mid-conversation-output-config-2026-07-01"],
    });
    

    Si tu editor subraya el role: "system" dentro de messages, es que el tipado de tu SDK todavía no lo incluye: actualiza el paquete antes de pelearte con TypeScript.

    System messages de un solo turno (beta, header mid-conversation-system-clear-at-2026-08-21). Es literalmente el sustituto del patrón que ahora rompe. Va dentro del array messages, como un turno más:

    {
      "role": "system",
      "clear_at": "next_user_message",
      "content": "Los resultados están en tu bandeja. Revísala antes de ejecutar más código."
    }
    

    Tiene autoridad de system prompt durante el turno actual y deja de inyectarse en el prompt cuando llega un mensaje de usuario posterior. Se queda en messages y lo sigues mandando igual, así que nada anterior cambia: la caché sigue casando, los thinking blocks siguen válidos y un mensaje limpiado cuesta 0 tokens de input.

    Progress updates entre tool calls (beta, header thinking-display-updates-2026-08-18). Con thinking.display: "updates" recibes como texto los avisos de progreso que el modelo escribe entre llamadas a tools, manteniendo el razonamiento oculto. Con el default "omitted" no llega nada y un turno agéntico de tres minutos parece un cuelgue.


    Dos cambios de comportamiento de Fable 5.1 que verás sin tocar código

    El parallel tool calling es más variable. Fable 5.1 puede hacer una llamada por turno donde Fable 5 agrupaba varias. No baja la calidad de la respuesta, pero cuesta tokens, round trips y tiempo de reloj. Se arregla con una instrucción de una línea pidiendo agrupar las lecturas independientes.

    Y Fable 5.1 reescribe ficheros enteros para cambios pequeños. Donde esperas una edición quirúrgica, te devuelve el fichero entero. Mismo resultado, más tokens de output.


    Claude Fable 5.1 vs Opus 5: la parte incómoda es que no es tu default

    Lo dicen los propios docs de Anthropic: "For most workloads, start with Claude Opus 5". Fable 5.1 se reserva para razonamiento exigente y trabajo agéntico de largo recorrido, o para cuando tus evals con Opus 5 a effort alto se quedan cortas. Opus 5 cuesta $5 de input y $25 de output. La mitad exacta.

    Benchmark Fable 5.1 Fable 5 Opus 5
    Terminal-Bench 4.0 55,8 % 42,0 % 52,3 %
    Terminal-Bench-Science 0.1 52,6 % 24,7 % 29,0 %
    OSWorld 2.0 (partial) 77,9 % 72,9 % 75,4 %
    OSWorld 2.0 (strict) 41,7 % 36,1 % 39,6 %

    Mira Terminal-Bench 4.0: 55,8 % contra 52,3 %. Tres puntos y medio por el doble de precio. En Terminal-Bench-Science son 52,6 % contra 29,0 %, casi veinticuatro puntos, y ahí la decisión se toma sola.

    La elección de modelo dejó de ser global. Se decide por tarea y con tus evals delante. Y si lo montas como router, recuerda el breaking change 2 y no cambies de modelo a mitad de conversación.


    Qué es Claude Mythos 5.1 y quién puede usarlo

    Claude Mythos 5.1 (claude-mythos-5-1) es el mismo modelo subyacente que Fable 5.1 con salvaguardas más permisivas. Comparte specs y precios. Solo por invitación, para participantes de Project Glasswing: organizaciones verificadas de ciberseguridad y ciencias de la vida. Con esas salvaguardas relajadas rinde más, 60,9 % en Terminal-Bench 4.0 frente al 55,8 % de Fable 5.1.

    La noticia no es el modelo, es la separación. Por primera vez Anthropic distingue de forma tan explícita "el modelo que puedes usar" de "el modelo que rinde más si te verifican". Tiene lógica. Parte del rendimiento que pierde un generalista se va en negarse a cosas que en un laboratorio de biología son trabajo normal, como se veía venir en los agentes autónomos aplicados a proteínas.

    Pero tenlo delante antes de comparar capturas de benchmarks: los números de un modelo con salvaguardas relajadas y los del modelo que tú puedes llamar no son comparables.


    Cómo migrar de Claude Fable 5 a Fable 5.1, en 4 pasos

    1. Busca tool_choice en tu repo. Si aparece any o tool, cámbialo a auto con strict: true y mueve la orden al prompt. Media hora.
    2. Busca dónde reconstruyes el system o el array de tools. Cualquier new Date() ahí dentro es una bomba: sácalo a un mensaje de usuario o a un system message de un solo turno.
    3. Audita tu router. Si puede cambiar de modelo a mitad de conversación, activa thinking-binding-controls-2026-08-01 en staging y mira input_transformations.
    4. Mide antes y después. Con el cache read a $0,25 tu arquitectura puede cambiar, pero solo si tienes el número delante.

    El resto está en los docs de novedades de Fable 5.1; el trabajo de verdad está en tus evals.

    Si construyes agentes con Claude y quieres el flujo completo de idea a producto, es lo que enseño en Construye con IA. Y en Dominicode Labs probamos estos patrones de historial append-only y routing por tarea sobre proyectos reales.

    Una release de modelo no se mide por lo que el modelo hace mejor. Se mide por cuántas suposiciones tuyas deja de sostener.


    Preguntas frecuentes

    ¿Tengo que migrar ya de Claude Fable 5 a Claude Fable 5.1?

    No hay urgencia. Fable 5.1 no se retirará antes del 1 de septiembre de 2027. La razón para migrar es económica, no de soporte. Si tu carga es muy agéntica y relee un prefijo grande en cada turno, el cache read a $0,25 justifica la migración por sí solo.

    ¿Por qué Claude Fable 5.1 me devuelve un 400 con tool_choice?

    Porque tool_choice: {"type": "any"} y {"type": "tool", "name": "..."} dejaron de estar soportados en Fable 5.1 y en Mythos 5.1. La API responde un invalid_request_error con el mensaje literal tool_choice: type "tool" and "any" are not supported for this model, y la misma validación se aplica al endpoint de token counting. Solo siguen siendo válidos {"type": "auto"} (el default) y {"type": "none"}. El fix es tool_choice: {"type": "auto"} con strict: true en la definición de la tool y la orden de usarla escrita en el prompt.

    ¿Cómo fuerzo ahora que el modelo llame a una tool concreta?

    No puedes forzarla. Díselo en el prompt —"Usa la tool get_weather para responder"— y comprueba que la haya llamado antes de seguir. Sin tool_choice no hay garantía, solo una instrucción que el modelo cumple casi siempre. Si lo que necesitabas era JSON conforme a un esquema, usa strict: true con tool_choice: auto o mueve el esquema a structured outputs.

    ¿Por qué a mi compañero le da 400 y a mí no, con el mismo código?

    Por la fecha de creación de la cuenta: la comprobación solo se aplica a las creadas a partir del 31 de agosto de 2026. En cuentas anteriores la API registra el desajuste pero no actúa salvo que mandes thinking.block_binding.prefix_mismatch_behavior. Tu código está igual de roto en ambos casos; solo cambia quién se entera.

    ¿Merece la pena Fable 5.1 frente a Opus 5?

    Los propios docs recomiendan empezar por Opus 5, que cuesta la mitad. Fable 5.1 saca tres puntos y medio más en Terminal-Bench 4.0, pero casi veinticuatro en Terminal-Bench-Science. Si tu tarea es razonamiento exigente y agéntico de largo recorrido, la diferencia paga; para el resto, no.

    ¿Puedo usar Claude Mythos 5.1?

    Solo si tu organización está aprobada en Project Glasswing, el programa por invitación para entidades verificadas de ciberseguridad y ciencias de la vida. No hay lista de espera pública: el acceso se pide a través de tu equipo de cuenta de Anthropic, AWS o Google Cloud. Es el mismo modelo con salvaguardas más permisivas y el mismo precio; si no estás dentro, tu referencia es Fable 5.1.

    ¿Puedo mezclar Claude Fable 5.1 y Opus 5 en el mismo agente?

    Sí, pero no dentro de la misma conversación. Los thinking blocks de Fable 5.1 no los lee ningún modelo anterior, así que un router que salte a Opus 5 a mitad de hilo pierde ese razonamiento sin avisarte. Reparte por tarea y arranca conversación nueva al cambiar de modelo, o activa el header thinking-binding-controls-2026-08-01 para ver los descartes en input_transformations.

    ¿Dónde puedo usar Claude Fable 5.1?

    En la API de Claude con el model ID claude-fable-5-1, en Amazon Bedrock como anthropic.claude-fable-5-1, en Google Cloud y en Microsoft Foundry, además de Claude Code, Claude Enterprise y la Claude Platform. No necesitas ningún header beta para llamarlo: los headers solo hacen falta para las funciones nuevas como el effort por mensaje o los system messages de un solo turno.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Cómo Funciona el Watermarking de Claude a Nivel de API

    Cómo Funciona el Watermarking de Claude a Nivel de API

    La primera vez que alguien escucha que Claude mete una marca de agua en el texto que genera, se imagina un truco barato de esteganografía.

    Un espacio de ancho cero entre dos palabras. Un carácter Unicode invisible (\u200B). Un patrón binario escondido en los saltos de línea.

    Si fuera eso, un script de Python de dos líneas con un .replace('\u200b', '') o un regex básico destruiría la marca en tres milisegundos.

    Anthropic no ha implementado un truco de caracteres. Desde agosto de 2026, todos los modelos nuevos de Claude integran un watermarking a nivel de API de naturaleza puramente estadística. No hay caracteres ocultos, viaja en el texto plano al copiar y pegar, y no existe ningún parámetro o flag en la API para desactivarla.

    Si tu producto llama a la API de Claude, tus usuarios ya están recibiendo texto marcado, lo sepas o no. Aquí te explico el algoritmo que hay detrás, cómo se diferencia de los metadatos en archivos y qué implica de verdad si construyes sobre esa API — no como usuario ocasional de Claude Code, sino como quien tiene que responder por ello ante sus propios clientes.


    Cómo genera texto un LLM (el paso previo imprescindible)

    Para entender cómo se inserta una señal en un texto sin alterar una sola letra, primero hay que recordar qué hace el modelo en cada ciclo de inferencia.

    Un LLM no "elige una frase completa". Trabaja token a token. Cuando Claude va a generar la siguiente palabra, calcula una distribución de probabilidad sobre su vocabulario completo (los llamados logits, normalizados con una función softmax):

    software      → 28%
    aplicaciones  → 21%
    productos     → 17%
    sistemas      → 14%
    herramientas  → 11%
    otros...      →  9%
    

    En un muestreo normal con cierta temperatura, el modelo elige uno de los tokens más probables. El texto resultante es fluido, coherente y suena natural.

    Aquí es exactamente donde entra el algoritmo de watermarking.


    El algoritmo de Kirchenbauer: listas verdes y sesgo de logits

    Anthropic no ha publicado el mecanismo exacto que usa Claude. Lo único que confirma oficialmente es el principio: la marca usa una clave y las palabras previas para decidir qué palabra elige el modelo entre varias opciones semánticamente equivalentes, sin tocar la calidad del texto. Cita como referencias el paper de Kirchenbauer et al. y SynthID-Text de Google DeepMind, así que lo más razonable —y lo que asume la mayoría del análisis externo— es que Claude use una variante de esa familia de técnicas. Lo que sigue es cómo funciona ese enfoque en general, no una confirmación línea por línea de la implementación interna de Anthropic.

    La técnica estándar en la industria para marcar texto en LLMs no toca el texto después de generarlo. Modifica la probabilidad antes de muestrear.

    El proceso sigue, a grandes rasgos, estos pasos:

    Token anterior (t-1) 
           │
           ▼
    [ Hash + Clave Secreta de Anthropic ]
           │
           ▼
    Partición pseudo-aleatoria del vocabulario
      ├── Lista Verde (Green List) ~ 50%
      └── Lista Roja (Red List)    ~ 50%
           │
           ▼
    Se suma un delta (+δ) a los logits de la Lista Verde
           │
           ▼
    Muestreo del siguiente token (favorece sutilmente el verde)
    
    1. Generación de semilla: Al momento de predecir el siguiente token, el sistema calcula un hash criptográfico del token anterior (o de una ventana de los últimos tokens) combinado con una clave secreta que solo posee Anthropic.
    2. Partición del vocabulario: Ese hash divide pseudo-aleatoriamente todo el diccionario de tokens en dos grupos: una Lista Verde y una Lista Roja, típicamente al 50% cada una.
    3. Sesgo de logits (Logit Biasing): A los logits de los tokens que caen en la Lista Verde se les suma un valor constante positivo.
    4. Muestreo: El modelo muestrea el siguiente token aplicando la distribución con los logits alterados.

    ¿Qué ve un humano vs. qué ve un detector?

    • Un humano lee el texto y no nota nada extraño. Como el sesgo es moderado, el modelo sigue seleccionando palabras de alta probabilidad semántica. El significado, el estilo y la calidad no cambian.
    • Un detector con la clave (hoy, solo Anthropic; han anunciado que van a liberar una API de detección pero todavía no está disponible públicamente) tomaría el texto generado, reconstruiría para cada palabra si pertenecía a la Lista Verde o a la Lista Roja, y contaría cuántos tokens verdes aparecen.

    En un texto escrito por un humano o por un modelo sin marca, la probabilidad de que un token caiga en la lista verde es de alrededor del 50% (puro azar).

    En un texto marcado con este tipo de técnica, esa proporción sube de forma sistemática por encima del azar. Anthropic no ha publicado la cifra exacta para Claude; en la literatura académica sobre KGW/SynthID-Text el desplazamiento suele ser notable con relativamente pocos tokens.

    Con un párrafo de varios cientos de palabras, la probabilidad de que esa acumulación de tokens verdes ocurra por puro azar cae drásticamente. Esa es la base estadística del método, aunque los números concretos que aplica Anthropic a Claude no son públicos.


    Por qué en código fuente la marca es mucho más débil

    Aquí hay un detalle técnico crítico que casi nadie en redes sociales ha mencionado: el watermarking estadístico necesita entropía.

    La entropía mide la cantidad de opciones válidas que tiene el modelo para continuar una frase.

    • En prosa libre (alta entropía): Para decir "construimos sistemas robustos", el modelo puede elegir entre sistemas, aplicaciones, plataformas, soluciones o arquitecturas. Tiene margen de sobra para elegir un token de la Lista Verde sin romper la frase.
    • En código de programación (baja entropía): Si estás escribiendo TypeScript y tienes for (const item of, el siguiente token sintácticamente válido es casi con total seguridad el identificador del array o una estructura iterable. Si fuerzas un token de la lista verde que no encaja sintácticamente, el código no compila.
    // En sintaxis estricta, el espacio de tokens válidos es minúsculo:
    export interface UserSession {
      id: string;
      createdAt: Date;
    }
    

    Si el modelo reduce la temperatura o la sintaxis impone un único token válido, el sesgo de la lista verde no puede aplicarse sin destruir la corrección del programa.

    Por eso, en archivos de código (.ts, .py, .rs) la marca estadística es inherentemente más débil o casi indetectable en fragmentos cortos, mientras que en documentación, emails o artículos es donde más fuerte se fija. Y precisamente porque no puedes apoyarte en el watermark para saber si un fragmento de código viene de un agente, la validación real sigue siendo la de siempre: TDD potenciado por IA, validar el código antes de mergear.

    Esta es la misma disciplina de control y contexto que enseñamos en el curso Construye con IA: De la Idea al Producto con Claude y Specs: cuando entiendes cómo procesan los modelos la probabilidad de los tokens, dejas de tratar a los agentes como cajas mágicas y empiezas a diseñar sistemas predecibles.


    Texto vs. Archivos: la diferencia entre Watermark y C2PA

    Existe una confusión generalizada entre la marca en el texto y la marca en archivos multimedia. Anthropic usa dos tecnologías completamente distintas según el tipo de output:

    Característica Marca de agua en Texto Metadatos en Archivos (.png, .svg)
    Mecanismo Sesgo estadístico en logits (Kirchenbauer / SynthID) Estándar C2PA (Content Authenticity Initiative)
    Dónde vive En la frecuencia y secuencia de las palabras En la cabecera / bloque de metadatos del archivo
    Copiar y pegar Sobrevive (es el texto mismo) No aplica (es un archivo binario)
    Edición / Conversión Se degrada progresivamente si reescribes Se pierde si re-guardas o conviertes el archivo
    Verificación hoy Privada (solo Anthropic tiene la clave) Pública y verificable hoy con c2patool

    Cómo inspeccionar C2PA en tus archivos hoy mismo

    Si generas diagramas SVG o imágenes con Claude y las sirves desde tu backend, puedes auditar los metadatos C2PA en tu propia máquina con la herramienta oficial de la Content Authenticity Initiative:

    # Instalación del cli oficial de C2PA
    brew install c2patool
    # O descargar el binario desde github.com/contentauth/c2pa-rs/releases
    
    # Inspeccionar el manifiesto completo en JSON
    c2patool imagen.png
    
    # Ver solo el resumen de procedencia
    c2patool imagen.png --info
    

    Si el archivo proviene directamente de Claude, el manifiesto C2PA mostrará la firma criptográfica de emisión. Si lo abres en Photoshop, lo recortas y lo guardas como WebP, el contenedor C2PA desaparece a menos que tu software lo re-firme.


    Detector de IA ≠ Detector de Watermark

    Esta distinción te va a ahorrar dolores de cabeza con clientes y product managers cuando te pidan "añadir detección de IA":

    1. Un detector de IA tradicional (heurístico): Analiza perplejidad y ráfagas (burstiness). Intenta adivinar si el estilo parece robótico. Es impreciso, produce falsos positivos atroces con no-nativos en inglés y no sirve como prueba legal.
    2. Un detector de marca de agua: No evalúa estilo ni perplejidad. Aplica una clave criptográfica sobre la secuencia de tokens y comprueba una hipótesis matemática de probabilidad binomial.

    Hoy por hoy, no existe un detector público de watermark para Claude. Si ves una web que afirma "Pega tu texto y te digo si tiene la marca de Claude", es un detector heurístico genérico, no un verificador del watermark de Anthropic.

    Además, publicar un detector abierto crea un problema de seguridad: actúa como un oráculo de optimización. Cualquier usuario podría pasar su texto por un script que altere palabras una a una hasta que el verificador dé negativo, destruyendo la marca con el mínimo esfuerzo.


    Qué significa el watermarking de Claude si construyes sobre la API

    Si integras Claude en tu SaaS o en pipelines de desarrollo interno —repasa lo básico en Claude API: Crash Course para developers con TypeScript si aún no la usas a diario— hay cuatro realidades que debes asumir:

    1. No hay opt-out: No existe un header x-anthropic-disable-watermark: true. La directiva de la Unión Europea (EU AI Act, artículo 50) exige que los proveedores marquen las salidas de IA generativa.
    2. La marca demuestra procesamiento, no autoría única: Si un humano escribe un borrador y le pide a Claude que corrija la puntuación, el texto resultante puede quedar marcado. Anthropic lo aclara en su documentación: la marca indica que el texto pasó por el modelo, no que el humano no intervino.
    3. No prometas a tus clientes outputs "indetectables": Cualquier modelo de negocio basado en vender "artículos indetectables para SEO" o "ensayos indetectables" está técnicamente muerto a medio plazo frente a esquemas de marca estadística.
    4. La paráfrasis profunda degrada la señal: Anthropic mismo lo advierte: una edición ligera probablemente no elimina la marca, pero reescribir el texto a fondo —traducirlo, reordenarlo intensamente o editarlo a mano de forma sustancial— sí la destruye, porque rompe la alineación entre los tokens y la clave con la que se generaron.

    Para patrones avanzados de integración con LLMs y arquitecturas robustas en producción, en Dominicode Labs analizamos continuamente los cambios de la API de Anthropic y cómo adaptar nuestros proyectos.


    Preguntas frecuentes

    ¿Puedo desactivar la marca de agua en la API de Claude?

    No. Anthropic ha desplegado el sistema de watermarking a nivel de inferencia sin ningún parámetro de exclusión en la API ni en las cuentas Enterprise, alineándose con las normativas internacionales como el EU AI Act.

    ¿La marca de agua ralentiza la generación o encarece el coste de tokens?

    Anthropic no ha reportado ningún impacto. Por diseño, este tipo de watermarking solo sesga qué token se elige dentro de la misma distribución de probabilidad que el modelo ya calculaba: no añade tokens extra ni pasadas adicionales de inferencia, así que el coste computacional adicional es marginal.

    ¿Un detector de watermark puede acusarme falsamente de usar IA?

    Con textos largos, la probabilidad matemática de un falso positivo en un test de hipótesis tipo Kirchenbauer es extremadamente baja — es la base estadística del método, aunque Anthropic no ha publicado la tasa exacta para Claude. Aun así, es un mecanismo mucho más fiable que los detectores heurísticos habituales, que fallan con frecuencia.

    ¿Qué pasa si traduzco el texto generado por Claude a otro idioma?

    Si traduces el texto mediante otra herramienta o manualmente, la alineación de los tokens con la clave pseudo-aleatoria original se destruye y la marca de agua estadística deja de ser detectable.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Tokens en español: por qué cuestan un 26 % más que en inglés

    Tokens en español: por qué cuestan un 26 % más que en inglés

    Hace unas semanas revisé el consumo de API de un agente de soporte. Sonnet, 25.000 conversaciones al mes, prompts cortos, nada exótico. El equipo había estimado el coste a mano antes de lanzar y la factura llegó cerca de un 26 % por encima.

    Estuvimos media hora buscando la llamada duplicada. No había llamada duplicada.

    El problema eran los tokens en español. No porque un token en español cueste más —el precio por token es idéntico—, sino porque necesitas más tokens para decir exactamente lo mismo.

    Y la parte incómoda es por qué necesitas más. La respuesta cómoda es "el español es más largo". Esa respuesta no llega a explicar ni la mitad de lo que pasa.

    Lo medí.

    La respuesta corta: el español consume un 26,0 % más de tokens que el inglés para transmitir el mismo mensaje, medido sobre cinco pares de textos paralelos con o200k_base (GPT-4o / GPT-5). Unos 10 puntos vienen de que el español usa más palabras; el resto, de que el tokenizador parte cada palabra española en más trozos. El precio por token es idéntico en los dos idiomas: lo que cambia es cuántos necesitas.

    Medí los tokens en español de cinco textos reales

    Cogí cinco textos del tipo que de verdad mandas a un modelo en producción y escribí la versión española y la inglesa de cada uno, con el mismo significado y el mismo registro. Nada de traducciones infladas: pares paralelos.

    Luego los pasé en local por o200k_base, el tokenizador de la familia GPT-4o / GPT-5.

    Tabla 1 — Sobrecoste de tokens del español frente al inglés en cinco pares de textos paralelos, medidos con o200k_base. Medición propia de Dominicode, julio de 2026.

    Tipo de texto Tokens EN Tokens ES Sobrecoste ES vs EN
    System prompt de agente 74 86 +16,2 %
    Documentación técnica 62 85 +37,1 %
    Mensaje de un usuario (soporte) 65 73 +12,3 %
    Fragmento de base de conocimiento (RAG) 67 93 +38,8 %
    Prompt de tarea de desarrollo 55 70 +27,3 %
    Total 323 407 +26,0 %

    El español consume un 26,0 % más de tokens que el inglés para decir exactamente lo mismo. No es una anécdota: es un multiplicador que se aplica a cada llamada de tu aplicación, todos los días.

    Y fíjate en la dispersión, porque ahí está la parte accionable: el mensaje de un usuario se hincha un 12,3 %; un fragmento de base de conocimiento, un 38,8 %. Tres veces más de castigo según el tipo de texto.

    No es que hables más. Es que te parten peor.

    Descompongo ese +26 % en sus dos factores:

    • Palabras: 290 en inglés → 319 en español = +10,0 %
    • Tokens por palabra: 1,114 en inglés → 1,276 en español = +14,5 %. O sin decimales, por si se lee mejor: 111 tokens por cada 100 palabras inglesas frente a 128 por cada 100 españolas.

    Y los dos factores se multiplican, no se suman: 1,10 × 1,145 = 1,26. Ahí está el +26 % completo.

    Traducido: de los 26 puntos de sobrecoste, solo unos 10 vienen de que el español use más palabras. El resto viene de que cada palabra española se rompe en más trozos.

    Esto no es una queja sobre el idioma, es ingeniería. El vocabulario de un tokenizador BPE se construye por estadística sobre un corpus mayoritariamente inglés, así que las secuencias frecuentes en inglés se quedan con las plazas buenas: una palabra inglesa común entra entera en un token y su equivalente española entra a trozos. Súmale que el español conjuga y deriva mucho más, y cada variante es una cadena distinta que el tokenizador no tiene memorizada.

    Eso explica también la dispersión de la tabla. Los dos que menos se inflan son el mensaje de usuario (+12,3 %) y el system prompt (+16,2 %): frases cortas, vocabulario común y terminología técnica que el tokenizador reconoce igual en los dos idiomas. Los que más se inflan son la documentación (+37,1 %) y los fragmentos de RAG (+38,8 %), que llevan prosa española de verdad, con subordinadas y nominalizaciones largas de las de "-ción" y "-miento". La regla práctica: cuanta más prosa explicativa, más sobrecoste.

    El sobrecoste del español baja de +44 % a +26 %

    Pasé los mismos cinco pares por cl100k_base, el tokenizador antiguo de GPT-3.5 y GPT-4: 323 tokens en inglés y 465 en español. Un +44,0 %.

    De +44 % a +26 % en una generación de tokenizador. El vocabulario de o200k_base es más grande y menos anglocéntrico. Pero son dos medidas, no una ley: bajó una vez, no des por hecho que baje siempre. Lo que sí puedes dar por hecho es que si en 2023 tomaste una decisión de arquitectura basada en lo que costaba el español, ese número está caduco.

    Metodología y límites de la medición

    Medición hecha por Bezael Pérez (Dominicode) en julio de 2026: cinco pares de textos paralelos español/inglés —system prompt de agente, documentación técnica, mensaje de soporte, fragmento de RAG y prompt de tarea de desarrollo—, tokenizados en local con o200k_base y cl100k_base. Totales: 323 tokens en inglés frente a 407 en español con o200k_base, y 465 con cl100k_base.

    Cada proveedor usa su propio tokenizador. Anthropic no publica el suyo: la única fuente fiable para contar tokens de Claude es su endpoint count_tokens — y Anthropic la llama estimación, no medida exacta.

    Así que los porcentajes de arriba son de la familia OpenAI (o200k_base), no de Claude. La dirección del efecto es la misma en todos los modelos comerciales, pero el número exacto varía según proveedor y tipo de texto. Anthropic lo admite en su propia página de precios: un token son "aproximadamente 4 caracteres o 0,75 palabras en inglés", y el recuento exacto "varía según el idioma".

    Con Claude hay además un detalle que cambia los números absolutos, avisado en esa misma página: los modelos de la generación 4.7 en adelante —Opus 5 y Sonnet 5 incluidos— usan un tokenizador nuevo que produce alrededor de un 30 % más de tokens que los anteriores para el mismo texto. Se nota en sus estimaciones: el millón de tokens de Opus 5 son ~555.000 palabras en inglés, y en Opus 4.6 eran ~750.000.

    Así que no copies la cifra de este post. Mídela. Es gratis y te cuento cómo abajo.

    Dónde se paga el sobrecoste de tokens en español: factura, contexto y latencia

    1. La factura: cuánto cuesta el sobrecoste al mes

    Tabla 2 — Precios oficiales de la API de Anthropic por millón de tokens, en USD (consultados el 30 de julio de 2026).

    Modelo Entrada ($/M tokens) Salida ($/M tokens)
    Claude Opus 5 $5 $25
    Claude Sonnet 5 $3 $15
    Claude Haiku 4.5 $1 $5

    Ojo a la fecha con Sonnet 5: los $3 / $15 son la tarifa estándar que entra el 1 de septiembre de 2026; hasta el 31 de agosto rige el lanzamiento de $2 / $10, un tercio más barato. Calculo con la estándar porque es la que pagarás cuando esto lleve unos meses en producción.

    Vuelvo al agente del principio: Sonnet 5 a tarifa estándar, 25.000 conversaciones al mes, 1.500 tokens de entrada y 300 de salida por conversación. El modelo responde en el idioma en el que le escriben, así que cuando el usuario escribe en español la salida también se hincha un 26 %.

    • En inglés: entrada 37,5 M × $3 = $112,50 · salida 7,5 M × $15 = $112,50 → $225/mes
    • En español (+26 % en entrada y en salida): entrada 47,25 M × $3 = $141,75 · salida 9,45 M × $15 = $141,75 → $283,50/mes

    Diferencia: $58,50 al mes. $702 al año. Por escribir en el idioma de tus usuarios. El +26 % va aquí como aproximación, por lo que ya expliqué: el tokenizador de Claude no es público.

    A esta escala es asumible. Multiplícalo por diez y ya es una decisión de producto.

    2. La ventana de contexto

    Opus 5 y Sonnet 5 tienen 1 millón de tokens de ventana de contexto. Y ojo con la cuenta, porque aquí el 26 % se da la vuelta: si cada texto te cuesta un 26 % más de tokens, en ese millón te cabe un 21 % menos de contenido (1 ÷ 1,26 = 0,79). Con fragmentos de base de conocimiento, que se hinchan un 38,8 %, la pérdida sube al 28 %.

    En un RAG eso no es una curiosidad académica: es recall —y si todavía estás decidiendo entre RAG y fine-tuning, el idioma entra en la ecuación de coste. Con el mismo presupuesto de contexto inyectas menos fragmentos por consulta.

    Menos evidencia recuperada para la misma pregunta es exactamente la situación en la que un modelo empieza a rellenar huecos, que es el mecanismo que expliqué en por qué la IA se inventa cosas.

    Y antes de que la solución sea "pues meto más": llenar la ventana tampoco es gratis en calidad. Va de eso la regla del 60 % en gestión de contexto.

    3. La latencia

    Un modelo emite los tokens de salida de uno en uno, a un ritmo más o menos fijo. Si tu respuesta en español necesita un 26 % más de tokens, tarda un 26 % más en terminar.

    Ojo con dónde lo notas, porque es fácil confundirse: si haces streaming, el primer token llega igual de rápido —eso lo manda el prefill de la entrada, no la longitud de la salida—, y lo que se alarga es la respuesta completa. Sin streaming, el usuario se come el 26 % entero mirando el cursor parpadear. Y si detrás hay un agente que encadena cinco llamadas, ese 26 % se acumula en cada paso.

    Qué hacer con esto (sin escribir peor español)

    Mide, no estimes. El endpoint count_tokens de Anthropic es gratis. Solo lo limitan las peticiones por minuto de tu tier: 2.000 en Start, 4.000 en Build, 8.000 en Scale.

    import Anthropic from "@anthropic-ai/sdk"
    
    const client = new Anthropic()
    
    const systemEs = "Eres un agente de soporte…"   // tu system prompt real
    const systemEn = "You are a support agent…"     // el mismo, en inglés
    
    async function contar(system: string) {
      const res = await client.messages.countTokens({
        model: "claude-opus-5",
        system,
        messages: [{ role: "user", content: "ping" }],
      })
      return res.input_tokens
    }
    
    console.log(await contar(systemEs), await contar(systemEn))
    

    El "ping" está ahí porque messages es obligatorio; al ser idéntico en las dos llamadas, la diferencia sale limpia. Quince minutos y dejas de discutir con estimaciones.

    El system prompt en inglés, el contenido del usuario en español. El system prompt se repite en cada llamada y tu usuario nunca lo lee. Traducirlo al inglés te quita tokens de encima en todas.

    Pero pon el número antes de comprar la idea. Mi system prompt de prueba baja de 86 a 74 tokens: por 25.000 conversaciones al mes son $0,90 con Sonnet 5. Con un system prompt realista de 2.000 tokens, unos $21 al mes. Con caché, una décima parte de eso. Es una optimización real, pero es de un dígito o dos de dólares — no la vendas como el arreglo.

    Y no es gratis. Si lo traduces, fija el idioma de salida de forma explícita —"Always respond in Spanish, regardless of the language of these instructions"— en vez de dejar que el modelo lo infiera del mensaje. Si dentro tienes few-shots, cuidado: los ejemplos arrastran el idioma de salida tanto como la instrucción. Y deja en español cualquier texto que el modelo tenga que devolver literal —mensajes fijos, disclaimers, nombres de producto—, o te lo traducirá a su manera. Después de tocar el system prompt, vuelve a pasar tus evals.

    Prompt caching. Esta es la palanca de verdad. Si el system prompt y el contexto fijo se repiten entre llamadas, cachéalos: una lectura de caché cuesta 0,1× el precio de entrada, un 90 % menos. Ataca justo la parte repetida, la que paga el 26 % una y otra vez sin cambiar una coma, y por eso rinde un orden de magnitud más que traducir nada. Lo desarrollé entero en prompt caching con la API de Claude. Orden de prioridades: primero cachea, después piensa en el idioma.

    Vigila lo que inyectas en cada consulta. Los fragmentos de base de conocimiento son lo que más se hincha (+38,8 %) y van en cada petición; la documentación técnica le sigue (+37,1 %). Si trabajas con specs, esto te toca de lleno: una spec es documentación técnica que entra en el contexto en cada iteración. Una razón más para escribirlas cortas y estructuradas, como insisto en el libro de Spec-Driven Development.

    Lo que NO debes hacer: escribir peor español para ahorrar tokens. Nada de abreviar, quitar tildes o telegrafiar los prompts como un SMS de 2004. El ahorro es de céntimos, y transliterar o mutilar el texto es justo lo contrario de lo que recomiendan los propios proveedores, que piden enviarlo en su escritura nativa. Si necesitas gastar menos: cachea, elige un modelo más pequeño para la tarea, reduce el número de llamadas o mueve la carga a un modelo local, donde el sobrecoste del español deja de facturarse por token y pasa a ser tiempo de GPU.

    Cómo medir tus tokens en español hoy mismo

    Abre tu system prompt de producción. El real, el que ya está desplegado.

    1. Copia tu system prompt tal cual está desplegado.
    2. Pásalo por count_tokens y anota input_tokens.
    3. Traduce ese mismo prompt al inglés sin recortar contenido.
    4. Pásalo otra vez y anota el segundo número.
    5. Resta, divide por el valor en inglés y multiplica por tus llamadas mensuales y por el precio de entrada de tu modelo.

    En quince minutos tienes tu número, no el mío.

    La mayoría descubre que su problema no era el idioma: era que no estaban cacheando nada. Ese diagnóstico solo aparece cuando mides.

    Si quieres el flujo completo para llevar una idea a producto con Claude Code —y salir con instrumentación, no con intuiciones—, es lo que trabajo en Construye con IA: de la idea al producto con Claude Code. Y si prefieres verlo sobre proyectos reales, con gente peleándose con las mismas facturas, eso pasa cada semana en Dominicode Labs.

    Preguntas frecuentes sobre los tokens en español

    ¿Cuánto más cuesta escribir prompts en español que en inglés?

    En mi medición con o200k_base sobre cinco pares de textos paralelos, un 26,0 % más de tokens para decir lo mismo: 323 en inglés frente a 407 en español.

    El rango va de +12,3 % (mensaje de un usuario) a +38,8 % (fragmento de RAG): el tipo de texto importa tanto como el idioma.

    ¿Es porque el español es más largo?

    Solo en parte, y los dos factores se multiplican, no se suman: un +10,0 % de palabras (290 → 319) por un +14,5 % de tokens por palabra (1,114 → 1,276) sale 1,10 × 1,145 = 1,26. El factor grande es el segundo.

    La causa es el tokenizador, no la verborrea: su vocabulario se entrenó sobre un corpus mayoritariamente inglés, y lo que no está bien representado ahí se fragmenta.

    ¿Cuántos tokens es una palabra en español?

    1,276 tokens por palabra de media en mi medición con o200k_base, frente a 1,114 en inglés. O sin decimales: unos 128 tokens por cada 100 palabras españolas y 111 por cada 100 inglesas.

    Es una media sobre texto real de producción: sube en prosa explicativa y baja en textos cargados de terminología inglesa, que el tokenizador ya conoce.

    ¿Qué tipo de texto se encarece más al escribirlo en español?

    Los fragmentos de base de conocimiento para RAG (+38,8 %) y la documentación técnica (+37,1 %). El que menos, los mensajes que escriben los propios usuarios (+12,3 %).

    La diferencia está en la densidad de jerga inglesa: cuanto más técnico es el texto, más tokens comparte el español con el inglés y menos se infla.

    ¿Cómo cuento los tokens en español que consume Claude?

    Con el endpoint count_tokens de la API de Anthropic, disponible en el SDK oficial como client.messages.countTokens(). Es gratis y solo lo limitan las peticiones por minuto de tu tier: 2.000 en Start, 4.000 en Build, 8.000 en Scale.

    Es además la única fuente oficial, porque Anthropic no publica su tokenizador. Ni siquiera ella es exacta: Anthropic advierte de que el conteo es una estimación y puede desviarse ligeramente del consumo real. Cualquier cifra calculada con o200k_base o cl100k_base es una aproximación de otro proveedor.

    ¿Debo escribir mis prompts en inglés para ahorrar dinero?

    El system prompt puedes traducirlo: se repite en cada llamada, nadie lo lee y los modelos responden en español perfectamente aunque las instrucciones estén en inglés. Pero mide el ahorro antes de moverlo, porque suele ser de un dígito o dos de dólares al mes, y desaparece casi entero si ya estás cacheando.

    El contenido del usuario, no lo toques. Y no traduzcas tu base de conocimiento al inglés solo por coste sin medir antes qué le pasa a la calidad de las respuestas. Antes de eso activa prompt caching: ahorra un 90 % en la parte repetida y no cambia nada de tu producto.

    ¿Este sobrecoste va a desaparecer?

    Se está reduciendo: los mismos cinco pares dan +44,0 % con cl100k_base (GPT-3.5 / GPT-4) y +26,0 % con o200k_base (GPT-4o / GPT-5), porque los vocabularios nuevos son más grandes y menos anglocéntricos.

    Pero no lo tomes como una tendencia garantizada. Son dos medidas, no una ley, y hay contraejemplos: los modelos Claude de la generación 4.7 en adelante usan un tokenizador nuevo que produce alrededor de un 30 % más de tokens que los anteriores para el mismo texto. Vuelve a medir cada vez que cambies de modelo.

    ¿Afecta el idioma a la ventana de contexto?

    Sí, y es el coste que menos se vigila. Cuidado con la cuenta, porque el porcentaje se invierte: en 1 millón de tokens —el que traen Opus 5 y Sonnet 5— cabe un 21 % menos de contenido si está en español, porque un +26 % de tokens por texto equivale a 1 ÷ 1,26 de contenido por ventana. Con fragmentos de RAG (+38,8 %) la pérdida es del 28 %.

    En un RAG eso significa menos fragmentos recuperados por consulta con el mismo presupuesto de contexto.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Qué es un modelo multimodal: tu imagen también son tokens

    Qué es un modelo multimodal: tu imagen también son tokens

    Hace unas semanas metí un pantallazo de un dashboard de facturación en una llamada a la API y le pedí al modelo el total del mes.

    Me devolvió una cifra. Redonda, con su símbolo de euro, con toda la seguridad del mundo.

    Estaba mal. No un poco mal: mal de otro trimestre.

    Mi primer reflejo fue culpar al OCR. Craso error, porque ahí no había ningún OCR. Y ese fue el momento en el que entendí que llevaba meses usando un modelo multimodal sin tener ni idea de lo que pasaba entre mi fetch y la respuesta.

    El modelo no leyó mi dashboard. Lo convirtió en tokens y predijo qué números encajaban ahí.


    Qué es un modelo multimodal

    Un modelo multimodal es un modelo de IA que acepta más de un tipo de entrada —texto, imagen, audio o vídeo— porque convierte todas esas entradas a vectores del mismo espacio y las procesa con un único transformer. No hay un "módulo de visión" que mira y luego le cuenta al modelo lo que hay: todo acaba siendo la misma sopa de números, y el modelo sigue haciendo lo único que sabe hacer, predecir el siguiente token.

    Lo esencial, antes de entrar en detalle:

    • Entrada no es salida. Que un modelo acepte imágenes no significa que las genere. Claude entiende imágenes; no las produce ni las edita.
    • Se factura por parches. Claude trocea la imagen en bloques de 28×28 px y cobra cada bloque como un visual token.
    • El coste es de prompt. Una captura de 1000×1000 px son 1.296 tokens de entrada antes de que el modelo escriba una palabra.
    • El conteo es aproximado. La documentación de Anthropic lo admite: los conteos de objetos y las coordenadas no son exactos.

    Cómo funciona: tu imagen no entra como imagen, entra como tokens

    Un modelo multimodal procesa una imagen en tres pasos: un encoder la trocea en parches y convierte cada parche en un vector, una capa de proyección lleva esos vectores a la misma dimensionalidad que los embeddings de texto, y el transformer los mezcla con los tokens de tu prompt como si fueran palabras. Si ya tienes claro que la IA no piensa, predice, es una extensión bastante elegante de la misma idea.

    Un LLM de texto tiene un tokenizador: parte tu string en trozos y a cada trozo le asigna un vector de N dimensiones. Ese vector es lo que come el transformer.

    Un modelo multimodal añade una pieza delante: un encoder por cada modalidad. Para imagen suele ser un Vision Transformer que trocea el bitmap en parches cuadrados y convierte cada parche en un vector. Para audio, algo equivalente sobre el espectrograma.

    Después viene el truco de todo esto: una capa de proyección que traduce esos vectores a la misma dimensionalidad que los embeddings de texto. Pasada esa capa, el transformer los procesa con la misma maquinaria: no hay una ruta especial para lo visual. El vector del parche 47 de tu JPEG y el de la palabra "factura" son vecinos en el mismo espacio, aunque el modelo sepa perfectamente cuál vino de dónde.

    Anthropic lo documenta de forma literal: Claude ve las imágenes en parches de 28×28 píxeles y a cada parche lo llama visual token. No es una metáfora divulgativa, es la unidad de facturación.

    El detalle que rompe la intuición: la secuencia final es una lista plana donde los tokens de la imagen y los de tu prompt están mezclados, atendiéndose unos a otros con el mismo mecanismo de atención de siempre.

    No hay un ojo. Hay una secuencia más larga.

    Y ojo con la confusión que cuesta dinero en reuniones de producto: que un modelo acepte imágenes no significa que las genere. Claude entiende imágenes, no las produce ni las edita. Gemini y la familia GPT sí generan, con endpoints y precios propios. Cuando alguien proponga "usar IA multimodal", pregunta si habla de entrada o de salida.


    El código: mandar una imagen de verdad

    Así se envía una imagen a Claude con el SDK oficial de TypeScript. Fíjate en el orden: la imagen antes del texto, porque la propia documentación de Anthropic recomienda esa estructura.

    import Anthropic from "@anthropic-ai/sdk";
    import { readFile } from "node:fs/promises";
    
    const anthropic = new Anthropic(); // lee ANTHROPIC_API_KEY del entorno
    
    const imageData = (await readFile("factura.jpg")).toString("base64");
    
    const message = await anthropic.messages.create({
      model: "claude-opus-5",
      max_tokens: 16000, // el thinking va dentro de este tope: no lo dejes corto
      messages: [
        {
          role: "user",
          content: [
            {
              type: "image",
              source: {
                type: "base64",
                media_type: "image/jpeg",
                data: imageData,
              },
            },
            {
              type: "text",
              text: "Extrae total, fecha y NIF. Devuelve JSON. Si un campo no es legible, null.",
            },
          ],
        },
      ],
    });
    
    console.log(message.usage.input_tokens); // aquí está la factura de verdad
    

    Si la imagen ya vive en una URL pública, te ahorras el base64:

    // sustituye el bloque de imagen del array content por este
    const imageBlock: Anthropic.ImageBlockParam = {
      type: "image",
      source: { type: "url", url: "https://ejemplo.com/factura.jpg" },
    };
    

    Loguea usage.input_tokens desde el primer día. Es la diferencia entre una demo bonita y saber lo que te va a costar en producción.

    Y ese null del prompt no es adorno: cuando el modelo te devuelve JSON extraído de un píxel borroso, necesitas un esquema que valide antes de que ese dato toque tu base de datos. Es el patrón que trabajo en el curso de Zod: el modelo propone, el schema dispone.


    Cuánto cuesta una imagen en un modelo multimodal

    Una imagen cuesta ⌈ancho / 28⌉ × ⌈alto / 28⌉ visual tokens en la API de Claude. Es aritmética pública y simple, y aun así aquí es donde se tuercen la mayoría de los proyectos multimodales: nadie hace la cuenta antes.

    Los modelos de Claude 4.7 en adelante están en el tier de alta resolución (borde largo máximo 2576 px, tope de 4.784 visual tokens); los anteriores, en el estándar (1568 px, 1.568 tokens). Si te pasas, la imagen se reescala antes de procesarse.

    Estas son las cifras que publica la documentación oficial de visión de Anthropic:

    Imagen Tier estándar Tier alta resolución
    200×200 px 64 tokens 64 tokens
    1000×1000 px 1.296 tokens 1.296 tokens
    1920×1080 px 1.560 tokens (reescalada a 1456×819) 2.691 tokens
    3840×2160 px (4K) 1.560 tokens (reescalada) 4.784 tokens (reescalada a 2576×1449)

    Traduce eso a dinero. Con Opus 5 a 5 $ por millón de tokens de entrada (precios de julio de 2026), mil capturas de 1000×1000 salen por unos 6,48 $ y mil capturas en 4K por unos 23,92 $. La cuenta la puedes rehacer tú: 1.296 × 5 / 1.000.000 × 1.000.

    Parece barato hasta que lo multiplicas por un agente que itera catorce veces sobre la misma pantalla.

    Con audio pasa lo mismo en otra escala. Gemini documenta 32 tokens por segundo, o sea 1.920 tokens por minuto. Una reunión de una hora son unos 115.000 tokens de entrada antes de que el modelo escriba una sola palabra.

    Tres consecuencias prácticas:

    1. Redimensiona antes de subir. Mandar un 4K cuando el texto se lee perfectamente a 1200 px es tirar tokens y latencia a la basura.
    2. En conversaciones de varios turnos, usa la Files API. Con base64 el payload entero viaja otra vez en cada turno, porque el historial se reenvía completo. Con file_id subes una vez y referencias. Ojo: hoy va por anthropic.beta.files.upload y hay que pasar betas: ["files-api-2025-04-14"].
    3. La imagen infla el prompt, no la respuesta. Ese coste es todo de entrada y el modelo lo digiere antes del primer token. La latencia sube aunque respondas tres líneas.

    Dónde falla un modelo multimodal (y falla más de lo que crees)

    Un modelo multimodal alucina con imágenes por el mismo motivo por el que la IA se inventa cosas con texto: no hay un módulo de verdad, hay una distribución de probabilidad. La diferencia es que con una imagen mala el modelo tiene menos señal y más margen para rellenar con lo plausible.

    Mi dashboard fue justo eso. Cifras pequeñas, reescaladas, con poco contraste. El modelo generó el número que estadísticamente encajaba en ese hueco.

    La documentación de Anthropic reconoce los límites sin maquillaje. Con imágenes de baja calidad, rotadas o de menos de 200 píxeles, alucina. Los conteos de objetos son aproximados. Las coordenadas, también. Y no puede determinar si una imagen fue generada por IA: si se lo preguntas, se inventa la respuesta.

    Léelo otra vez: el conteo es aproximado. Si tu caso de uso es "cuántos palés hay en esta foto" y tu negocio depende de esa cifra, tienes un problema de arquitectura, no de prompt.

    Sobre el OCR: un modelo multimodal entiende un documento con layout raro, tablas torcidas o manuscritos mucho mejor que Tesseract. Pero un OCR clásico es determinista y trazable. El modelo puede darte dos respuestas distintas para la misma imagen, y si le pides coordenadas te las da aproximadas, nunca como una región verificable contra el original. En un pipeline de compliance eso es inaceptable.

    Modelo multimodal OCR clásico (Tesseract, Textract)
    Layout variable, manuscritos, fotos malas Muy bueno Malo
    Misma imagen → misma salida No garantizado Sí
    Trazabilidad de dónde salió el dato No la da Sí, con bounding boxes
    Coste Por visual token, escala con la resolución Plano o por página
    Entiende el contenido ("¿este ticket es de comida?") Sí No
    Apto para compliance sin revisión humana No Sí

    Cuándo usar un modelo multimodal y cuándo es un martillo caro

    Mi regla, después de comerme varias facturas de API sin necesidad:

    Si el dato ya existe en forma estructurada, no le mandes la foto. Si tienes el PDF con capa de texto, extrae el texto. Si tienes la API del dashboard, llama a la API. Mandar una captura de algo que podías consultar en JSON es la forma más cara de leer un número.

    Usa multimodal cuando la información vive en la disposición visual y no en el contenido. Un ticket arrugado fotografiado de noche. Un diagrama en una pizarra. El screenshot de una UI rota que un usuario manda por soporte. Ahí la alternativa no es un parser peor: es que no hay alternativa.

    Y vigílalo dentro del bucle agéntico. Si montas un agente que navega e interpreta pantallas, cada iteración multiplica el coste visual. Esa distinción entre IA generativa e IA agéntica importa mucho más cuando cada paso arrastra 2.700 tokens de imagen.

    Y si lo que necesitas es buscar entre miles de imágenes en vez de razonar sobre una, ya no quieres un modelo generativo: quieres embeddings multimodales e índice vectorial. Lo tienes montado paso a paso en el pipeline multimodal con Gemini Embedding 2.

    La decisión que va antes —si tu problema es de búsqueda, de RAG o de fine-tuning— la desmenucé aquí.


    Lo que puedes hacer hoy

    Coge la funcionalidad multimodal que tengas en marcha o en el backlog y haz una sola cosa: calcula sus visual tokens con la fórmula, multiplícalos por tu volumen mensual real y compáralo con lo que cuesta resolverlo sin imagen.

    En más casos de los que esperas descubrirás que ibas a pagar por interpretar un pantallazo de un dato que ya tenías en una tabla. En el resto tendrás el número exacto para defender el proyecto delante de quien firma.

    Los modelos multimodales no son magia ni son un timo. Son un canal de entrada más, con su precio por parche y su margen de error. Trátalos como lo que son: una dependencia cara. Mídela antes de casarte con ella.

    Cómo encaja esto en un producto completo —qué resuelve el modelo y qué resuelve código normal— lo trabajo de principio a fin en Construye con IA. Y si prefieres discutirlo con gente que está construyendo lo mismo esta semana, estamos en Dominicode Labs.


    Preguntas frecuentes

    ¿Qué es un modelo multimodal en una frase?

    Un modelo que acepta varios tipos de entrada —texto, imagen, audio, vídeo— porque los convierte todos a vectores del mismo espacio antes de procesarlos. En la práctica, para el desarrollador significa una sola cosa: tu imagen entra en el prompt como tokens, cuesta como tokens y se factura como tokens.

    ¿Un modelo multimodal "ve" la imagen como una persona?

    No. Trocea el bitmap en parches, convierte cada parche en un vector y los intercala con los tokens de tu prompt. No hay percepción: hay una secuencia de números sobre la que se aplica atención. Por eso describe con precisión una escena compleja y a la vez falla contando cuatro objetos.

    ¿Cuántos tokens cuesta enviar una imagen?

    Depende de la resolución y del proveedor. En la API de Claude son ⌈ancho / 28⌉ × ⌈alto / 28⌉ visual tokens, con reescalado automático si superas el límite del modelo: 1000×1000 px son 1.296 tokens y un 4K llega al tope de 4.784 en el tier de alta resolución. Son cifras aproximadas de la documentación oficial, así que loguea usage.input_tokens en vez de fiarte de una estimación.

    ¿Es mejor un modelo multimodal que un OCR clásico?

    Para documentos con layout variable, manuscritos o fotos malas, casi siempre sí. Para pipelines que necesitan determinismo, trazabilidad y coste plano, no. Muchos sistemas serios usan los dos, con el modelo actuando solo sobre lo que el OCR no resuelve.

    ¿Los modelos multimodales también generan imágenes?

    No todos. Aceptar imágenes como entrada y producirlas como salida son capacidades distintas. Claude entiende imágenes pero no las genera ni las edita, según su propia documentación. Gemini y la familia GPT sí tienen generación, con endpoints y precios propios. Confirma cuál de las dos necesitas antes de planificar la feature.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Claude Opus 5: los 2 breaking changes que rompen tu código

    Claude Opus 5: los 2 breaking changes que rompen tu código

    Cambias una línea. Un campo model dentro de un JSON. Diez segundos de trabajo, deploy a staging, a otra cosa.

    Veinte minutos después el endpoint de resúmenes devuelve textos cortados a mitad de frase. Y una ruta concreta —la de análisis largo, la que más te importa— devuelve 400 sin que hayas tocado nada más.

    No es un bug de Anthropic. Eres tú, migrando a Claude Opus 5 como si fuera un cambio de versión menor.

    No lo es. Y el problema es que casi todo lo que vas a leer estos días sobre este modelo son tablas de benchmarks. El titular es que cuesta la mitad que Fable 5. La letra pequeña es que si no tocas dos parámetros, tu aplicación empieza a devolver respuestas cortadas y errores 400.

    Este post va de la letra pequeña.


    Qué es Claude Opus 5 y qué cambia respecto a Opus 4.8

    Claude Opus 5 es el modelo más capaz de Anthropic, disponible desde el 24 de julio de 2026 con el identificador de API claude-opus-5, sin sufijo de fecha. Cuesta $5 por millón de tokens de entrada y $25 de salida —el mismo precio que Opus 4.8— y trae dos breaking changes respecto a la generación anterior: el thinking viene activado por defecto y desactivarlo deja de ser compatible con los niveles de effort xhigh y max.

    Atributo Claude Opus 5 Claude Opus 4.8
    ID de API claude-opus-5 claude-opus-4-8
    Precio entrada / salida $5 / $25 por millón $5 / $25 por millón
    Fast mode (solo API de Anthropic) $10 / $50 por millón —
    Thinking al omitir el parámetro Adaptativo, activado Desactivado
    Qué cubre max_tokens Thinking + respuesta Solo la respuesta
    Effort por defecto en la API high —
    thinking: disabled + xhigh/max HTTP 400 Válido
    Mínimo para prompt caching 512 tokens 1024 tokens
    Ventana de contexto 1M tokens (defecto y máximo) —
    Salida máxima 128K tokens —
    Rate limits Cubo propio Pool combinado Opus 4.x

    Está disponible en Claude.ai, Claude Code, Claude Cowork, la API de Anthropic, Amazon Bedrock (anthropic.claude-opus-5), Google Cloud y Microsoft Foundry. Es el modelo por defecto en Claude Max y el más potente disponible en Claude Pro. Los datos de esta tabla están contrastados con la documentación oficial de Anthropic.


    Los benchmarks de Claude Opus 5, en treinta segundos

    Sí, los números son buenos. Los despacho rápido porque no son el tema.

    En CursorBench 3.2, a máximo effort, Claude Opus 5 se queda a un 0,5% del pico de Fable 5 —a la mitad de coste por tarea—. En ARC-AGI 3 triplica la puntuación del siguiente mejor modelo. En Frontier-Bench v0.1 más que dobla el rendimiento de Opus 4.8. Los tres resultados salen de las cifras publicadas por Anthropic.

    No es el mejor en todo: sigue por detrás de Mythos 5 en tareas de ciberseguridad ofensiva. Y Anthropic lo describe como su modelo mejor alineado hasta la fecha, con la menor tasa de comportamiento engañoso.

    Si vienes de Claude Opus 4.8, pagas lo mismo por token por un modelo bastante mejor. Con un matiz que casi nadie menciona: Opus 5 piensa por defecto y escribe más largo, así que gasta más tokens por tarea. Misma tarifa no significa misma factura. Y si estabas pagando el premium de Fable 5 por tareas de agente, ahí sí: Anthropic mide la mitad de coste por tarea.

    Perfecto. Ahora la parte que rompe cosas.


    Breaking change 1 de Claude Opus 5: el thinking viene activado por defecto

    En Claude Opus 5, omitir el parámetro thinking ejecuta thinking adaptativo; en Opus 4.8 y 4.7 omitirlo significaba no razonar. Este es el cambio que corta tus respuestas a mitad de frase.

    Antes, el silencio equivalía a "no razones, contéstame". Ahora el silencio es un sí.

    Y aquí viene la parte que duele: max_tokens es un tope duro sobre thinking más texto de respuesta, juntos. No son dos presupuestos separados.

    Si tenías max_tokens ajustado al milímetro para tu respuesta —y todo el que ha optimizado costes lo tiene ajustado al milímetro— el modelo se gasta parte de ese presupuesto razonando y la respuesta se corta.

    Este código funcionaba perfectamente ayer:

    import Anthropic from "@anthropic-ai/sdk";
    const client = new Anthropic();
    
    // Opus 4.8 — omitir "thinking" = sin razonamiento; 1024 tokens íntegros para la respuesta
    const res = await client.messages.create({
      model: "claude-opus-4-8",
      max_tokens: 1024,
      messages: [{ role: "user", content: prompt }],
    });
    

    Cambias el model a claude-opus-5 y esos 1024 tokens ahora se reparten entre razonamiento y respuesta. Nadie te avisa: no hay error, solo un texto que termina a media frase.

    Tienes dos salidas. La buena:

    // Opus 5 — thinking explícito y presupuesto con margen
    const res = await client.messages.create({
      model: "claude-opus-5",
      max_tokens: 8192, // cubre thinking + respuesta
      thinking: { type: "adaptive", display: "summarized" },
      output_config: { effort: "medium" },
      messages: [{ role: "user", content: prompt }],
    });
    

    Y la que replica el comportamiento anterior:

    thinking: { type: "disabled" }
    

    Cuidado con esa segunda, porque tiene trampa. Es exactamente el breaking change número dos.

    Sobre display: los tokens de razonamiento en crudo no se devuelven nunca. El valor por defecto es "omitted". Si pones "summarized" recibes un resumen legible del razonamiento, útil para logs y para depurar por qué el modelo llegó a donde llegó.


    Breaking change 2: desactivar el thinking en Opus 5 está capado a effort high

    Esta es la que devuelve 400.

    En Opus 5 la escala completa de effort es low, medium, high, xhigh y max. El valor por defecto de la API es high.

    Combinar thinking: { type: "disabled" } con effort xhigh o max devuelve HTTP 400. En Opus 4.8 esa combinación era perfectamente válida.

    // Válido en Opus 4.8 — error 400 en Opus 5
    const res = await client.messages.create({
      model: "claude-opus-5",
      max_tokens: 4096,
      thinking: { type: "disabled" },
      output_config: { effort: "xhigh" }, // 400
      messages: [{ role: "user", content: prompt }],
    });
    

    Y ahora el detalle que hace que esto sea peligroso de verdad: la validación es por petición. No hay un chequeo global al arrancar. Puedes tener veinte llamadas funcionando con thinking desactivado y effort high, y que la veintiuna —la que sube a xhigh para el caso difícil— se rechace. Las anteriores funcionando no te protegen de nada.

    Traducido: cualquier ruta de tu código que desactive el thinking hay que auditarla antes de migrar, no después. Búscalo con un grep por "disabled" y revisa qué effort viaja en cada una de esas peticiones.

    Mi recomendación es no mantener esa ruta. En lugar de desactivar el thinking, bájalo a effort medium con thinking activado:

    // Sustituto recomendado para las rutas que antes desactivaban thinking
    thinking: { type: "adaptive" },
    output_config: { effort: "medium" },
    

    En Opus 5 los niveles low y medium rinden inusualmente bien. La intuición de "menos effort, peor respuesta" que traías de la generación anterior ya no aplica igual: prueba medium antes de asumir que necesitas high.

    Con un matiz, para que nadie me lea en diagonal: para coding y trabajo agéntico, Anthropic recomienda arrancar en xhigh y bajar solo donde tus evals demuestren que la calidad aguanta. Lo de medium es el sustituto de las rutas que antes desactivaban el thinking, no un consejo para bajarle el effort a tu agente de coding. Y si al bajarlo compruebas que la tarea nunca necesitó Opus, Claude Sonnet 5 cubre buena parte de ese terreno por bastante menos dinero.


    El tercer sitio donde revienta: el rechazo que llega con un 200

    Este no está en la lista oficial de breaking changes, pero te va a tirar producción igual.

    Los clasificadores de seguridad pueden declinar una petición. Cuando lo hacen, la API devuelve HTTP 200 con stop_reason: "refusal". No es un error. Tu try/catch no lo captura, tu retry no se dispara, tu monitorización no lo ve.

    Y content llega vacío: un array sin bloques. Así que este patrón —el que escribe todo el mundo la primera vez— revienta con un TypeError:

    const res = await client.messages.create({ /* ... */ });
    const text = res.content[0].text; // 💥 TypeError: content llega vacío
    

    La corrección son cuatro líneas:

    const res = await client.messages.create({ /* ... */ });
    
    if (res.stop_reason === "refusal") {
      logger.warn("Petición declinada por los clasificadores", { requestId: res.id });
      return fallbackResponse();
    }
    
    const text = res.content.find((b) => b.type === "text")?.text ?? "";
    

    Dos datos más que ayudan aquí. Un rechazo que llega antes de emitir output no se factura, aunque sí consume rate limit. Y si no quieres montar el fallback a mano, Anthropic tiene un parámetro fallbacks en modo "default" (con el beta header server-side-fallback-2026-07-01) que reencamina la petición rechazada a otro modelo dentro de la misma llamada: los rechazos de categoría ciber caen a Opus 4.8.

    Comprueba stop_reason antes de leer content. Siempre. Con este modelo y con el siguiente.


    Dos cambios de comportamiento que te van a sorprender

    Escribe respuestas más largas por defecto. Y bajar el effort no lo arregla —es un eje distinto—. Si necesitas respuestas breves, pídelo en el prompt de forma explícita: límite de palabras, formato, o ambos.

    Verifica su propio trabajo sin que se lo pidas. Esta es la importante, porque invierte una buena práctica de prompting que era válida hasta la semana pasada.

    Todos tenemos system prompts con alguna variante de "revisa tu respuesta antes de contestar". En Opus 5 esas instrucciones provocan verificación excesiva: más tokens, más latencia, misma calidad. La solución no es reescribirlas con mejor redacción. Es borrarlas.

    Con la delegación en subagentes el ajuste es distinto. Opus 5 delega más que Opus 4.8 por defecto, así que los empujones que añadiste para forzarla ahora sobran. Pero aquí no basta con borrar: la recomendación de Anthropic es poner límites —en qué escenarios se delega, o cuántos subagentes como máximo—. Pasas de empujar a acotar.

    Es la parte contraintuitiva del oficio: mantener un system prompt no es acumular reglas, es borrarlas cuando el modelo ya no las necesita. Es exactamente el criterio que trabajo en el curso Construye con IA, donde el prompt se trata como código con mantenimiento, no como un texto que se escribe una vez y se olvida.


    Lo que mejora en Claude Opus 5 sin que toques nada

    El mínimo de prompt caching baja a 512 tokens. En Opus 4.8 eran 1024, y el umbral está en la documentación de prompt caching. Prompts de sistema que antes se quedaban justo por debajo del umbral y no cacheaban, ahora sí cachean, sin cambiar una línea de código.

    Si tienes muchas llamadas cortas y repetitivas, revisa la factura la semana que viene: puede bajar sola. Y si aún no tienes el caching bien montado, la mecánica completa está en Prompt Caching en Claude: reduce tu factura de API un 90%.

    Otros dos datos que conviene tener a mano: la ventana de contexto es de 1M tokens —es a la vez el valor por defecto y el máximo— con 128K tokens de salida. Y los rate limits de Opus 5 son un cubo separado del pool combinado de Opus 4.x: al migrar tráfico no heredas tu cuota anterior. Si mueves un volumen serio, comprueba límites antes del despliegue y no el lunes por la mañana con todo el tráfico encima.


    Cómo migrar a Claude Opus 5 en 4 pasos

    Cuatro pasos, en este orden:

    1. Grep por "disabled" en todas tus llamadas al thinking. Cada resultado, con su effort al lado. Si hay xhigh o max, es un 400 esperándote.
    2. Revisa tus max_tokens. Todo lo que esté ajustado al límite de la respuesta necesita margen para el thinking, o cambia a thinking: { type: "disabled" } con effort high como máximo.
    3. Comprueba stop_reason antes de leer content. Cuatro líneas.
    4. Borra las instrucciones de auto-verificación de tus system prompts. No las reescribas. Y en las de delegación, cambia el empujón por un límite: cuándo se delega y cuántos subagentes como máximo.

    Media hora de trabajo. Y a cambio: te acercas a la inteligencia frontera de Fable 5 por la mitad de coste por tarea.

    Si quieres ver este tipo de migraciones aplicadas sobre proyectos reales —con los prompts, el código y los errores que salen por el camino— es lo que hacemos en Dominicode Labs, y voy publicando los análisis modelo a modelo en el canal de YouTube.

    El precio lo pone Anthropic. Los 400 los pones tú.


    Preguntas frecuentes sobre Claude Opus 5

    ¿Cuánto cuesta Claude Opus 5?

    $5 por millón de tokens de entrada y $25 por millón de tokens de salida, exactamente el mismo precio que tenía Opus 4.8. Fable 5 cuesta $10/$50, así que Opus 5 se acerca a esa franja de inteligencia frontera por la mitad. Existe un fast mode disponible únicamente en la API de Anthropic que cuesta el doble: $10/$50. En suscripciones, es el modelo por defecto de Claude Max y el más potente disponible en Claude Pro.

    ¿Qué se rompe al migrar de Opus 4.8 a Claude Opus 5?

    Dos cosas concretas. Primera: el parámetro thinking ahora viene activado por defecto, y como max_tokens es un tope duro sobre thinking más respuesta juntos, los presupuestos ajustados provocan respuestas cortadas a mitad de frase. Segunda: thinking: { type: "disabled" } combinado con effort xhigh o max devuelve un error 400, cuando en Opus 4.8 esa combinación era válida. A eso conviene sumar una tercera comprobación: los rechazos de los clasificadores llegan como HTTP 200 con stop_reason: "refusal", no como error.

    ¿Cómo desactivo el thinking en Claude Opus 5?

    Con thinking: { type: "disabled" }, pero solo puedes hacerlo hasta effort high. Si envías esa configuración con xhigh o max, la petición se rechaza con un 400. Y ojo: la validación se hace petición a petición, así que una llamada posterior que suba el effort se rechazará aunque todas las anteriores hayan funcionado. En la mayoría de casos compensa más bajar a effort medium con el thinking activado, porque en Opus 5 los niveles low y medium rinden bastante mejor de lo que esperarías.

    ¿Sigo necesitando el «revisa tu respuesta antes de contestar» en mis prompts?

    No, y además es contraproducente. Opus 5 verifica su propio trabajo sin que se lo pidas, así que esas instrucciones provocan verificación excesiva: gastas más tokens y añades latencia sin ganar calidad. La recomendación es borrarlas, no reescribirlas. Con la delegación en subagentes el ajuste es distinto: los empujones para forzarla sobran, pero Anthropic recomienda sustituirlos por un límite explícito —en qué escenarios se delega y cuántos subagentes como máximo—, porque Opus 5 delega más que Opus 4.8 por defecto.

    ¿Hay que cambiar algo para aprovechar el prompt caching en Opus 5?

    Nada. El mínimo de tokens necesario para cachear baja de 1024 a 512, así que los prompts que antes eran demasiado cortos para entrar en caché ahora cachean automáticamente, sin tocar código. Si tu carga de trabajo son muchas llamadas cortas con un system prompt repetido, es probable que la factura baje sola tras migrar.

    ¿Puedo mover todo mi tráfico de Opus 4.x a Opus 5 de golpe?

    Técnicamente sí, pero revisa los rate limits antes. Los límites de Opus 5 son un cubo separado del pool combinado de Opus 4.x, de modo que al migrar no heredas la cuota que ya tenías asignada. Si mueves un volumen alto sin comprobarlo, puedes empezar a recibir throttling con un código que hasta ese momento no lo veía nunca.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Claude API: Crash Course para developers con TypeScript

    Claude API: Crash Course para developers con TypeScript

    Hace unos meses un developer me escribió frustrado. Llevaba dos días intentando integrar Claude en su app. No le funcionaba el streaming, no entendía por qué sus respuestas llegaban cortadas, y había probado tres ejemplos distintos de Stack Overflow que usaban versiones diferentes del SDK.

    El problema no era la API. Era que había empezado por el medio.

    Esta es la Claude API introducción que yo habría querido tener al principio: sin rodeos, con código real, y con el orden correcto para entender qué está pasando antes de que algo falle.

    Qué es la Claude API y por qué te importa

    Claude es el modelo de lenguaje de Anthropic. La API te da acceso directo a ese modelo desde tu código: puedes enviarle mensajes, pedirle que razone, que use herramientas externas, que responda en streaming o que procese imágenes.

    La diferencia respecto a ChatGPT para developers es principalmente la calidad del razonamiento en tareas de código complejas y el system prompt — Claude lo sigue con una precisión que cambia cómo construyes agentes.

    Setup: API key y SDK

    Primero necesitas una cuenta en console.anthropic.com. Una vez dentro, ve a API Keys y genera una nueva clave. Guárdala — no la vuelves a ver.

    Instala el SDK oficial con npm o Bun:

    npm install @anthropic-ai/sdk
    # o con Bun
    bun add @anthropic-ai/sdk
    

    Guarda la clave en una variable de entorno. Nunca en el código:

    # .env
    ANTHROPIC_API_KEY=sk-ant-...
    

    Tu primera llamada en TypeScript

    Este es el "Hello World" de la Claude API. Sin clases, sin abstracción, directo al grano:

    import Anthropic from "@anthropic-ai/sdk";
    
    const client = new Anthropic({
      apiKey: process.env.ANTHROPIC_API_KEY,
    });
    
    async function main() {
      const response = await client.messages.create({
        model: "claude-sonnet-4-6",
        max_tokens: 1024,
        messages: [
          {
            role: "user",
            content: "Explica qué es un closure en JavaScript en 2 líneas.",
          },
        ],
      });
    
      console.log(response.content[0].type === "text" ? response.content[0].text : "");
    }
    
    main();
    

    Eso es todo. Ejecutas esto y tienes una respuesta de Claude en tu terminal.

    Lo que necesitas entender de la estructura:

    • model — qué versión de Claude usas (más sobre esto abajo)
    • max_tokens — límite de tokens en la respuesta (no el total de la conversación)
    • messages — array de turnos de conversación con role: "user" o role: "assistant"

    Los conceptos que no puedes ignorar

    Modelos disponibles

    Anthropic tiene tres familias activas:

    Modelo Cuándo usarlo
    claude-sonnet-4-6 El equilibrio perfecto: velocidad + calidad. Mi default para casi todo.
    claude-haiku-4-5 Más rápido y barato. Bueno para tareas simples o llamadas en volumen.
    claude-opus-4-8 El más potente. Para tareas de razonamiento complejo donde el coste no es el problema.

    Si estás empezando, usa claude-sonnet-4-6. No pienses más.

    System prompt vs User message

    El system es la personalidad y las instrucciones permanentes de Claude. El user es lo que cambia en cada turno.

    const response = await client.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 1024,
      system: "Eres un reviewer de código senior. Responde siempre en español. Sé directo y señala el problema antes de proponer la solución.",
      messages: [
        {
          role: "user",
          content: "Revisa esta función: function add(a, b) { return a - b; }",
        },
      ],
    });
    

    El system prompt es donde ocurre la mayor parte de la magia cuando construyes agentes. Si quieres ver cómo llevamos esto a proyectos reales con Claude Code, en el curso Construye con IA cubrimos exactamente eso: de la idea al producto con agentes que siguen instrucciones de producción.

    Tokens: lo que cuesta dinero

    Un token es aproximadamente 0,75 palabras en inglés (algo menos en español). La API te cobra por input_tokens (lo que envías) y output_tokens (lo que Claude responde).

    Después de cada llamada puedes ver el uso:

    console.log(response.usage);
    // { input_tokens: 48, output_tokens: 312 }
    

    max_tokens limita la respuesta, no la llamada completa. Si pones max_tokens: 100 y la respuesta necesita 200 tokens, Claude cortará el texto a mitad. Es uno de los errores más comunes al empezar.

    ¿Cómo implementar streaming con la Claude API en TypeScript?

    Sin streaming, esperas a que Claude termine de generar toda la respuesta antes de recibirla. Con streaming, recibes los tokens a medida que se generan — igual que ves escribir a Claude en el chat web.

    Para UX en tiempo real, el streaming no es opcional. Es lo que distingue una app que se siente viva de una que "se congela" tres segundos antes de mostrar algo. En los proyectos de agentes que construimos en Labs, migrar de llamada síncrona a streaming eliminó la necesidad de un loader — los usuarios percibieron la respuesta como inmediata sin que cambiáramos nada más.

    import Anthropic from "@anthropic-ai/sdk";
    
    const client = new Anthropic({
      apiKey: process.env.ANTHROPIC_API_KEY,
    });
    
    async function streamResponse() {
      const stream = await client.messages.create({
        model: "claude-sonnet-4-6",
        max_tokens: 1024,
        stream: true,
        messages: [
          {
            role: "user",
            content: "Escribe un test unitario en TypeScript para una función que suma dos números.",
          },
        ],
      });
    
      for await (const event of stream) {
        if (
          event.type === "content_block_delta" &&
          event.delta.type === "text_delta"
        ) {
          process.stdout.write(event.delta.text);
        }
      }
    
      console.log("\n--- Stream completado ---");
    }
    
    streamResponse();
    

    El loop for await itera sobre los eventos del stream. El tipo que te importa es content_block_delta con delta.type === "text_delta" — ahí está el texto.

    ¿Qué es el tool use en Claude API y cómo funciona?

    Tool use (o function calling) permite que Claude llame a funciones definidas por ti. Claude decide cuándo usarlas y con qué argumentos. Tú ejecutas la función y le devuelves el resultado.

    El siguiente ejemplo define una herramienta get_weather ficticia:

    const response = await client.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 1024,
      tools: [
        {
          name: "get_weather",
          description: "Obtiene el tiempo actual para una ciudad.",
          input_schema: {
            type: "object",
            properties: {
              city: {
                type: "string",
                description: "El nombre de la ciudad.",
              },
            },
            required: ["city"],
          },
        },
      ],
      messages: [
        {
          role: "user",
          content: "¿Qué tiempo hace en Madrid ahora mismo?",
        },
      ],
    });
    
    // Si Claude quiere usar la herramienta, el stop_reason será "tool_use"
    if (response.stop_reason === "tool_use") {
      const toolUse = response.content.find((b) => b.type === "tool_use");
      console.log("Claude quiere llamar a:", toolUse?.name);
      console.log("Con argumentos:", toolUse?.input);
      // Aquí ejecutarías la función real y devolverías el resultado a Claude
    }
    

    Esto es la base de cualquier agente. Claude no ejecuta código — tú lo ejecutas y le informas del resultado. El loop de razonamiento lo controla Claude; la ejecución la controlas tú. Si quieres ver cómo este patrón escala a un pipeline completo — desde un ticket de Jira hasta el deploy —, tienes el ejemplo en el post sobre automatizar el proceso de desarrollo con IA.

    Errores comunes al empezar

    Rate limits. La API tiene límites por minuto tanto en requests como en tokens. Si los golpeas, recibes un 429. Solución: exponential backoff o usar Haiku para prototipos de alto volumen.

    Context window agotado. Cada modelo tiene un límite de tokens totales en conversación (input + output). Sonnet 4.6 tiene 200K tokens de context window — es enorme, pero si metes archivos enteros en cada llamada, lo llenas. Sé selectivo con lo que incluyes en el contexto.

    Formato de mensajes incorrecto. El array messages debe alternar user y assistant. No puedes tener dos mensajes de user seguidos sin un assistant entre medias. Eso devuelve un error 400.

    max_tokens demasiado bajo. Si la respuesta se corta, sube max_tokens. El valor por defecto no existe — es un parámetro obligatorio. Empieza con 1024 y ajusta según lo que necesites.

    Variables de entorno no cargadas. Si ves AuthenticationError, casi siempre es que ANTHROPIC_API_KEY no está disponible en el proceso. Verifica con console.log(process.env.ANTHROPIC_API_KEY) antes de depurar nada más.

    Qué explorar después

    Una vez tienes la llamada básica y el streaming funcionando, estos son los siguientes pasos lógicos:

    Vision. Puedes enviar imágenes en el array content y Claude las analiza. Útil para screenshots, diagramas, facturas.

    Embeddings. Anthropic no tiene embeddings propios en la API, pero Claude funciona muy bien combinado con embeddings de OpenAI o Cohere para búsqueda semántica.

    Batch API. Para procesar cientos de prompts sin necesidad de respuesta en tiempo real. Hasta un 50% más barato que llamadas individuales.

    Workbench de Anthropic. En console.anthropic.com tienes un playground para probar prompts, comparar modelos y ver el uso de tokens antes de escribir una sola línea de código. Es la herramienta que más uso al diseñar system prompts.

    Multiturno real. Construir una conversación que mantenga contexto entre turnos requiere gestionar el array messages manualmente — añadir cada respuesta de Claude como role: "assistant" y cada input del usuario como role: "user". No hay estado en la API.

    Si quieres ver tool use aplicado a un workflow de code review automático antes de un PR, tienes el flujo completo en el post sobre agentic code review con Claude Code.

    Si tuvieras que elegir solo un área para explorar después del streaming, elige Vision — es el salto de ROI más rápido y el que más impacto tiene en una demo.


    FAQ

    ¿Necesito tarjeta de crédito para empezar?
    Sí. Anthropic requiere un método de pago para activar la API, pero tiene un tier de prueba con crédito gratuito. Puedes hacer cientos de llamadas de desarrollo sin pagar nada en los primeros días.

    ¿Cuál es la diferencia entre la API de Claude y Claude.ai?
    Claude.ai es el producto de consumo (el chat web). La API es el acceso programático al modelo. Tienen facturación y cuentas separadas. Una suscripción a Claude.ai no te da acceso a la API.

    ¿Cuánto cuesta en producción?
    Depende del modelo y el volumen. Claude Sonnet 4.6 está alrededor de $3 por millón de input tokens y $15 por millón de output tokens — verifica siempre en anthropic.com/pricing antes de hacer estimaciones de arquitectura, los precios se actualizan con cada generación de modelo.

    ¿Puedo usar la API en el frontend directamente?
    Técnicamente sí, pero nunca deberías. La API key quedaría expuesta en el cliente. Siempre llama a la API desde un backend o un serverless function que tú controlas.

    ¿Qué pasa si Claude no termina la respuesta y stop_reason no es end_turn?
    Si stop_reason es max_tokens, la respuesta se cortó por el límite que pusiste. Si es tool_use, Claude quiere ejecutar una herramienta. Si es stop_sequence, alcanzó una secuencia de parada que definiste. Valida siempre stop_reason en producción.


    Si quieres ver todo esto aplicado en un proyecto real — no en ejemplos de tutorial sino en un producto con usuarios — en Dominicode Labs tenemos el código de los proyectos que construimos en directo, incluyendo agentes con tool use y streaming. Es donde llevamos la teoría a producción.

    Y si prefieres el formato video con más ejemplos en directo, en el canal de YouTube de Dominicode cubrimos estas integraciones con frecuencia.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.