Tag: LLM

  • Qwen 3.7: El nuevo gigante de razonamiento que cambia las reglas

    Qwen 3.7: El nuevo gigante de razonamiento que cambia las reglas

    Cuando pensamos en modelos de razonamiento avanzado y agentes autónomos capaces de operar durante horas, la mente siempre nos viaja a Silicon Valley. Nos imaginamos los laboratorios de OpenAI en San Francisco o las oficinas de Anthropic. Como vimos en nuestra guía de mejores modelos de IA para correr en local, Qwen destaca en programación local, pero sus capacidades propietarias en la nube van mucho más allá.

    Sin embargo, el verdadero competidor a batir hoy en día en lógica pura y desarrollo de software viene de Hangzhou.

    Alibaba Cloud ha dado un golpe de autoridad con el lanzamiento de su nueva familia insignia: Qwen 3.7.

    Hoy te quiero contar qué hace especial a Qwen 3.7, por qué su modelo de razonamiento Max está poniendo en aprietos a Claude 3.5 Sonnet y cómo puedes integrarlo hoy mismo en tus pipelines de desarrollo.


    La familia Qwen 3.7: Max y Plus

    Presentada oficialmente en la cumbre tecnológica de Alibaba, la serie Qwen 3.7 introduce una arquitectura optimizada para la toma de decisiones lógicas en entornos complejos. El lanzamiento se ha estructurado en torno a dos variantes principales:

    • Qwen 3.7-Max: Un modelo de texto puro centrado en razonamiento matemático, generación de código complejo y resolución de tareas autónomas de largo recorrido (long-horizon autonomous tasks). Cuenta con una ventana de contexto colosal de 1 millón de tokens.
    • Qwen 3.7-Plus: Un modelo multimodal nativo que integra visión y texto, optimizado para la interacción autónoma con interfaces gráficas de usuario (GUI), automatización de navegación web y tareas de diseño visual.

    A diferencia de las versiones anteriores, Qwen 3.7 es de código cerrado (proprietario) por ahora. No puedes descargar sus pesos para correrlos en local, pero sus capacidades como motor de backend de agentes justifican plenamente pagar por su API.


    ¿Por qué importa Qwen 3.7 para tus Agentes de IA?

    Si estás construyendo pipelines agénticos con frameworks como Hermes, sabes que el principal cuello de botella de los modelos tradicionales es la falta de consistencia lógica en planes de muchos pasos. La IA tiende a desviarse del objetivo o a cometer errores de sintaxis a medida que la conversación se alarga.

    Qwen 3.7 soluciona esto a tres niveles clave:

    1. Memoria de contexto masiva: Su ventana de 1M de tokens te permite pasarle repositorios enteros de código o historiales comerciales completos sin tener que recurrir a complejas técnicas de fragmentación o RAG restrictivos.
    2. Razonamiento profundo integrado: Al igual que los modelos o1/o3, Qwen 3.7-Max calcula silenciosamente sus rutas lógicas antes de emitir la primera palabra, reduciendo los errores lógicos en refactorizaciones de código a niveles ínfimos.
    3. Continuous execution: Está especialmente entrenado para interactuar con herramientas externas mediante MCP (Model Context Protocol), respondiendo de forma ultra-consistente ante llamadas repetidas de base de datos o consolas.

    Cómo conectar Qwen 3.7 a Hermes Agent

    Aunque los pesos son cerrados, la gran ventaja es que puedes consumir la API de Qwen 3.7-Max a través de proveedores compatibles con la interfaz de OpenAI, como OpenRouter o Fireworks AI.

    Para usar Qwen 3.7 como el motor cerebral de tu agente Hermes en segundo plano, solo debes configurar tu archivo ~/.hermes/config.yaml apuntando al endpoint del router:

    model:
      default: qwen/qwen-3.7-max
      provider: custom
      base_url: https://openrouter.ai/api/v1
      context_length: 1000000
    
    custom_providers:
      - name: openrouter-qwen
        base_url: https://openrouter.ai/api/v1
        models:
          qwen/qwen-3.7-max:
            context_length: 1000000
    

    Y asegurar que tu archivo .env local contenga el token de autenticación de OpenRouter para autorizar las consultas:

    OPENROUTER_API_KEY=tu_token_de_acceso_privado
    

    Con esta configuración, tu agente operará con la potencia de uno de los mejores LLMs del mercado actual, delegando la inferencia pesada a la nube por una fracción del coste de otros proveedores propietarios.

    Este enfoque híbrido (agente local + cerebro en la nube) es la arquitectura de producción que defendemos en el curso de Construye con IA y la que llevamos a cabo para automatizar flujos complejos en el nuevo curso de Hermes Agent.


    Conclusión: La IA ya no es monopolio de Occidente

    El lanzamiento de Qwen 3.7 demuestra que la brecha de rendimiento entre las corporaciones de Silicon Valley y los gigantes asiáticos se ha cerrado por completo. Para tareas lógicas pesadas y desarrollo de software, evaluar alternativas como Qwen 3.7-Max es obligatorio si quieres mantener la máxima velocidad de ejecución al coste de API más bajo.

    Si estás experimentando con Qwen 3.7 en tus herramientas y quieres debatir sus resultados con otros desarrolladores senior de nuestra comunidad, te espero en Dominicode Labs.


    Preguntas Frecuentes (FAQ)

    ¿Qwen 3.7 tendrá versiones de código abierto (open-weights) en el futuro?

    Alibaba tiene un historial sólido de publicar versiones open-source (Qwen-Coder, Qwen-Instruct) meses después del lanzamiento de sus APIs de pago insignia. Aunque a día de hoy Qwen 3.7-Max y Plus son cerrados, es altamente probable que veamos variantes destiladas de menor tamaño (como 7B o 14B) publicadas en Hugging Face en los próximos meses.

    ¿Qué coste tiene la API de Qwen 3.7 en comparación con Claude 3.5 Sonnet?

    Históricamente, los precios de API de Alibaba Cloud y sus distribuidores autorizados en OpenRouter son extremadamente competitivos, situándose habitualmente a una fracción del coste por millón de tokens en comparación con los precios oficiales de Anthropic o OpenAI para modelos del mismo rango de rendimiento.

    ¿Qué ventajas aporta Qwen 3.7-Plus en la automatización de interfaces (GUI)?

    Qwen 3.7-Plus está entrenado con capacidades avanzadas de visual grounding (localización de coordenadas en pantalla). Esto significa que al pasarle una captura de pantalla de una aplicación web, el modelo puede identificar con precisión matemática dónde debe hacer clic o qué campo de texto debe rellenar un agente de navegación web autónomo.

    ¿Se puede usar la ventana de contexto de 1 millón de tokens en OpenRouter?

    Sí. OpenRouter soporta y adapta los límites de contexto declarados por el proveedor oficial de Alibaba. Sin embargo, recuerda que enviar contextos masivos de cientos de miles de tokens aumentará proporcionalmente el coste de la consulta y la latencia inicial de respuesta (TTFT) debido al volumen de datos a pre-procesar.


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

  • Las 4 capas de la IA Local: Evita errores comunes

    Las 4 capas de la IA Local: Evita errores comunes

    Cuando un desarrollador decide meterse en el mundo de la IA local, lo primero que suele hacer es clonar tres repositorios distintos de GitHub, descargar un modelo desde Hugging Face y cruzar los dedos.

    A los diez minutos, algo falla y escribe frustrado en un foro: "Ayuda, Hermes Agent no soporta archivos GGUF" o "¿Ollama es compatible con LangChain?".

    Es un error clásico. Ocurre porque la industria avanza tan rápido que no nos ha dado tiempo a trazar un mapa conceptual de lo que estamos construyendo. Mezclar las capas de abstracción en tu servidor te llevará a diagnósticos erróneos y a un código insostenible. Puedes consultar nuestro post sobre cómo configurar oMLX en Mac para ver cómo operan de forma desacoplada las capas de inferencia y agente.

    Hoy te quiero enseñar las 4 capas de la IA local que debes dominar para estructurar tus proyectos de forma profesional y entender qué herramienta hace qué en tu máquina.


    El Mapa del Stack de IA Local

    Para evitar dolores de cabeza, debes entender que un entorno de inteligencia artificial local se compone de cuatro capas independientes y bien delimitadas:

    ┌─────────────────────────────────────────┐
    │ 1. AGENTE / ORQUESTADOR (Hermes Agent)  │  <-- Controla la lógica y flujo
    └─────────────────────────────────────────┘
                         │  (Habla vía API HTTP / JSON)
                         ▼
    ┌─────────────────────────────────────────┐
    │ 2. SERVIDOR DE INFERENCIA / SERVING     │  <-- Expone endpoints OpenAI/Anthropic
    │    (oMLX, Ollama, llama.cpp, vLLM)       │
    └─────────────────────────────────────────┘
                         │  (Compila y optimiza para el chip)
                         ▼
    ┌─────────────────────────────────────────┐
    │ 3. RUNTIME ENGINE (MLX, TensorRT-LLM)   │  <-- Gestiona el hardware (GPU/Metal)
    └─────────────────────────────────────────┘
                         │  (Lee los bytes binarios)
                         ▼
    ┌─────────────────────────────────────────┐
    │ 4. FORMATO DE PESOS (GGUF, safetensors) │  <-- Los archivos del modelo (.bin)
    └─────────────────────────────────────────┘
    

    Capa 1: El Agente / Orquestador (El Cerebro Lógico)

    Aquí es donde viven herramientas como Hermes Agent, LangChain o LlamaIndex. Esta capa no sabe matemáticas de tensores, no lee archivos de pesos binarios ni sabe qué es una GPU.

    • Su función: Orquestar la conversación, manejar la persistencia de sesiones en SQLite, gestionar el historial de prompts y llamar a herramientas externas mediante MCP (Model Context Protocol).
    • Cómo se comunica: Es 100% API-céntrico. Solo le importa hablar con un puerto web (como http://localhost:8000) que le devuelva texto en formato compatible con OpenAI o Anthropic.

    Capa 2: El Servidor de Inferencia (El Gestor de Tráfico)

    Aquí encontramos a oMLX, Ollama, LM Studio o vLLM. Su trabajo es actuar como puente entre el protocolo web del agente y los motores de bajo nivel del hardware.

    • Su función: Exponer los endpoints HTTP compatibles (/v1/chat/completions), gestionar las colas de peticiones entrantes (continuous batching), optimizar el uso de memoria RAM y administrar las cachés de claves y valores (KV cache).

    Capa 3: El Runtime Engine (El Acelerador de Hardware)

    El motor de bajo nivel matemático. Aquí están MLX (el framework oficial de Apple), ONNX Runtime, TensorRT-LLM de Nvidia o la librería core de llama.cpp.

    • Su función: Traducir las operaciones lógicas de la red neuronal a comandos nativos de tu hardware específico (núcleos CUDA de Nvidia o aceleración Metal en los chips Apple Silicon M1/M2/M3/M4).

    Capa 4: El Formato de Pesos (Los Datos)

    La capa más baja. Los archivos binarios reales descargados de Hugging Face en formatos como MLX, GGUF, safetensors u ONNX. Son simplemente las matrices de números y coeficientes del modelo (por ejemplo, Qwen 2.5 Coder o Llama 3).


    Por qué esta distinción te salvará la vida

    Cuando entiendes este mapa, te das cuenta de que la pregunta: "¿Hermes Agent soporta modelos GGUF?" no tiene sentido.

    Hermes es formato-agnóstico a nivel de pesos. A Hermes no le importa si tu modelo es un archivo GGUF o una carpeta MLX. A Hermes solo le importa que el Servidor de Inferencia (como Ollama o oMLX) le exponga un puerto compatible. Si el servidor lee GGUF y responde HTTP, Hermes funcionará sin cambiar una línea de código.

    Este enfoque modular y arquitectónico es clave para construir sistemas escalables. Es la filosofía que defendemos en el libro de SDD: Spec-Driven Development para estructurar el desarrollo antes de codificar, y la que aplicamos para conectar microservicios locales en el curso de Construye con IA y en el nuevo curso de Hermes Agent.


    Conclusión: Diseña con desacoplamiento

    No ates tu lógica agéntica a un formato de archivo o a un hardware concreto. Al separar el plano de control (Hermes) del plano de inferencia (oMLX/Ollama), puedes cambiar de modelo, escalar tu servidor a una máquina con GPU dedicada en Linux o correr localmente en tu Mac sin tener que reescribir una sola línea del comportamiento de tu agente de IA.

    Si quieres debatir sobre la arquitectura óptima de IA local para tu equipo y resolver dudas de infraestructura, te espero en Dominicode Labs.


    Preguntas Frecuentes (FAQ)

    ¿Por qué Hermes Agent no carga directamente los pesos del modelo?

    Para mantener la eficiencia y el desacoplamiento. Cargar pesos de modelos de miles de millones de parámetros requiere optimizaciones extremas de memoria GPU a bajo nivel. Delegar esto a servidores dedicados (como oMLX o vLLM) permite que Hermes se concentre únicamente en la lógica de control, la orquestación de herramientas y el flujo de la conversación.

    ¿Qué formatos de pesos debo usar en Mac con Apple Silicon?

    Para obtener el máximo rendimiento nativo en procesadores Apple Silicon, se recomienda utilizar el formato MLX servido por herramientas como oMLX. Si buscas portabilidad o usas hardware variado, el formato GGUF servido por Ollama o llama.cpp es la alternativa estándar.

    ¿Cómo se comunican el Agente y el Servidor de Inferencia?

    Se comunican a través del protocolo HTTP utilizando formatos de petición y respuesta compatibles con la API de OpenAI o Anthropic. Esto significa que el agente simplemente envía un JSON con los mensajes y el servidor le devuelve el texto generado (habitualmente en modo streaming).

    ¿Puedo cambiar el servidor de inferencia sin alterar mi agente?

    Sí. Al estar desacoplados mediante APIs estándar, puedes cambiar tu backend de inferencia local de oMLX en Mac a vLLM en un servidor Linux en la nube simplemente actualizar la URL base en el archivo de configuración del agente (config.yaml), sin alterar sus habilidades ni su lógica operativa.


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

  • La comparativa definitiva en Mac: oMLX vs llama.cpp

    La comparativa definitiva en Mac: oMLX vs llama.cpp

    Cuando decides correr modelos de lenguaje locales en tu Mac, casi toda la documentación y tutoriales de internet te apuntan al mismo lugar: descarga Ollama o compila llama.cpp. Son las herramientas estándar y más populares del ecosistema. Como vimos en nuestra guía de hardware y modelos para LLMs locales, correr modelos en local requiere conocer bien los límites de tu máquina.

    Sin embargo, la arquitectura de Apple Silicon es particular. Y tratar a tu Mac como si fuera una máquina x86 genérica con una tarjeta de video tradicional es desperdiciar la potencia de su memoria unificada.

    En pruebas reales con agentes y tareas de larga duración, oMLX y llama.cpp demuestran comportamientos radicalmente opuestos.

    Hoy te quiero enseñar la comparativa de rendimiento definitiva de oMLX vs llama.cpp para que sepas exactamente cuál elegir según las necesidades específicas de tus agentes de IA.


    Latencia inicial vs. Velocidad sostenida

    El error más común al medir la velocidad de una IA local es fijarse únicamente en la velocidad de generación final (tokens por segundo). En entornos reales, el rendimiento se divide en dos métricas críticas:

    1. TTFT (Time to First Token / Tiempo hasta el primer token): Es el tiempo que tarda la GPU en procesar el prompt de entrada antes de empezar a responder.
    2. Generación Sostenida: La velocidad pura de escritura una vez que el modelo ha empezado a hablar.

    En las comparativas oficiales utilizando hardware idéntico y el modelo Qwen3.5-9B, los resultados arrojan una conclusión nítida:

    • llama.cpp (Ollama / LM Studio): Es el rey indiscutible de la baja latencia (TTFT). Empieza a responder prácticamente al instante. Es la mejor opción para interfaces de chat interactivas donde el usuario espera una reacción visual inmediata.
    • oMLX (Framework MLX): Aunque tarda unos segundos más en pre-procesar el prompt inicial (fase de prefill), su velocidad de generación sostenida es masiva, llegando a multiplicar por hasta 4.14x el rendimiento bajo concurrencia gracias a su arquitectura de continuous batching y caché SSD.

    La batalla por la Memoria: ¿GGUF o MLX?

    El formato de los pesos que descargas determina cómo el servidor exprime tu hardware:

    llama.cpp y el ecosistema GGUF

    El formato GGUF está sumamente optimizado para ejecutarse en memorias ajustadas (como portátiles de 16GB de RAM). Puedes correr modelos cuantizados ligeros consumiendo muy pocos recursos del sistema, lo que te permite mantener otras aplicaciones abiertas sin comprometer el rendimiento general de tu Mac.

    oMLX y los directorios MLX

    oMLX trabaja exclusivamente con formatos nativos de MLX. Este formato espera que el modelo cargue directamente sobre la memoria unificada Metal de Apple Silicon. Para exprimirlo en tareas pesadas de código o multi-agente, el sweet spot recomendado de hardware sube a 64GB de RAM unificada o superior.

    Si cuentas con ese hardware, oMLX utiliza su pila de caché de dos niveles (PagedSSDCacheManager y PagedCacheManager) para mover las partes frías del contexto de tu IDE al SSD de tu Mac, permitiendo restaurar prefijos de forma inmediata sin re-computar y ahorrando gigabytes de memoria real.


    Cuándo elegir cada motor para tus Agentes

    La decisión arquitectónica óptima es sencilla si analizas el caso de uso:

    Elige oMLX si:

    • Estás desarrollando con agentes autónomos multitarea en segundo plano (como Hermes Agent) que envían múltiples peticiones paralelas gracias al continuous batching.
    • Trabajas con contextos de código muy largos donde la reutilización de prefijos de caché de oMLX evita tener que procesar todo el repositorio en cada llamada.
    • Tienes un Mac de desarrollo de gama alta (32GB, 64GB o Studio/Mini dedicados).

    Elige llama.cpp (Ollama) si:

    • Tu flujo de trabajo es puramente interactivo (un chat directo uno a uno donde el TTFT es prioritario).
    • Corres la IA en movilidad en un portátil con recursos de RAM ajustados (16GB).
    • Necesitas portabilidad multiplataforma (escribir scripts que deban correr idénticos tanto en macOS como en Windows o servidores Linux).

    Esta comparativa y análisis de optimización de infraestructura local es la que ponemos en práctica en el curso de Construye con IA para calibrar entornos y que detallamos a nivel de código de bajo nivel en el nuevo curso de Hermes Agent.


    Conclusión: El software correcto para el silicio correcto

    No limites el rendimiento de tu hardware Apple Silicon por usar loaders de propósito general. Entender cuándo delegar tus tareas a llama.cpp para interactividad rápida o a oMLX para tareas pesadas de automatización autónoma te dará una ventaja competitiva masiva en velocidad y coste de computación local.

    Si quieres debatir con otros desarrolladores senior sobre benchmarks de modelos locales y optimización de cachés, te espero en Dominicode Labs.


    Preguntas Frecuentes (FAQ)

    ¿Cuál es el sweet spot de hardware para usar oMLX en Mac?

    Aunque puede funcionar en máquinas con 16GB de memoria unificada utilizando modelos pequeños de 7B u 8B parámetros bien cuantizados, oMLX destaca especialmente en configuraciones de 64GB de RAM o más. En estas capacidades, permite cargar múltiples modelos en paralelo y utilizar cachés de contexto masivas sin swap de disco ralentizado.

    ¿Se pueden convertir modelos de formato Hugging Face a MLX de forma sencilla?

    Sí. La librería oficial de Apple mlx-lm incluye una utilidad de consola muy sencilla. Ejecutando python -m mlx_lm.convert --hf-path autor/modelo -q puedes descargar, convertir y cuantizar cualquier modelo compatible directamente desde Hugging Face a tu máquina en formato nativo para oMLX.

    ¿Cómo ayuda el continuous batching de oMLX a los agentes de IA?

    Si ejecutas sub-agentes paralelos (por ejemplo, un agente de código y un agente de testing al mismo tiempo), llama.cpp clásico procesará sus peticiones de forma secuencial (encolándolas). oMLX las agrupa de forma dinámica en la GPU de Apple, procesando ambas respuestas simultáneamente sin duplicar la latencia.

    ¿Ollama soporta el formato nativo de MLX?

    No. Ollama está basado en llama.cpp y su arquitectura interna gira en torno a la carga y ejecución de archivos en formato GGUF. Para correr modelos en formato MLX utilizando aceleración nativa pura de Apple, debes utilizar servidores específicos del ecosistema como oMLX o el servidor propio de mlx-lm.


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

  • GPT-5.6 vía API: guía práctica para developers

    GPT-5.6 vía API: guía práctica para developers

    Actualizas el string del modelo en tu código. gpt-5.5 pasa a gpt-5.6. Compilas, despliegas, todo responde igual… hasta que llega la factura de OpenAI y el costo por output es seis veces más alto que la semana pasada.

    No rompiste nada. Elegiste mal el modelo.

    GPT-5.6 no es un modelo — es una familia de tres, con nombres que no explican nada hasta que entiendes el sistema detrás. Si vienes de GPT-5.5 o evalúas moverte desde Claude, migrar sin leer la letra pequeña te puede salir caro. Literalmente.

    Esta es la guía que me hubiera gustado tener el día del lanzamiento: qué es cada variante, cómo llamarlas desde la API con TypeScript, qué cambia con Programmatic Tool Calling, y si vale la pena tocar tu stack si ya construyes con Claude Code.

    Qué es realmente GPT-5.6 (y por qué el nombre importa)

    OpenAI cambió el sistema de versionado con este lanzamiento. El número —5.6— identifica la generación. El nombre —Sol, Terra o Luna— identifica el nivel de capacidad, y cada nivel puede avanzar a su propio ritmo sin esperar un salto de versión completo.

    Tres variantes, disponibles desde el 9 de julio de 2026 en ChatGPT, Codex y la API:

    • Sol — el modelo insignia. Máxima capacidad de razonamiento.
    • Terra — el punto medio. Buen rendimiento a mejor precio.
    • Luna — el económico. Para volumen alto y tareas simples.

    Los tres comparten ventana de contexto: 1.05M tokens de entrada, 128K tokens de salida como máximo. La diferencia no está en cuánto texto aguantan — está en cuánto “piensan” antes de responder.

    Cómo llamar a GPT-5.6 desde la API

    Cada variante tiene su propio slug: gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna. Si usas el alias gpt-5.6 a secas, apunta a Sol por defecto.

    import OpenAI from "openai";
    
    const client = new OpenAI();
    
    const response = await client.responses.create({
      model: "gpt-5.6-terra",
      reasoning: { effort: "medium" },
      input: "Resume los cambios de este PR en 3 bullets técnicos.",
    });
    
    console.log(response.output_text);
    

    Nota el endpoint: responses.create, no chat.completions.create. GPT-5.6 usa la Responses API para razonamiento y tool-calling. Chat Completions sigue funcionando, pero no tiene acceso a las funciones nuevas de esta generación.

    El parámetro que de verdad mueve la aguja del costo es reasoning.effort. Acepta none, low, medium, high, xhigh y max, con medium por defecto. Si vienes de GPT-5.5, la recomendación de OpenAI es probar un nivel por debajo del que ya usabas. GPT-5.6 suele mantener la calidad con menos tokens de razonamiento, y eso es directamente menos costo por request.

    Hay un segundo parámetro, reasoning.mode, que acepta "pro" para forzar que el modelo trabaje más antes de devolver una única respuesta final. Resérvalo para tareas donde una respuesta mediocre te cuesta más que los tokens extra. Piensa en debugging de un incidente en producción, no en un endpoint de autocompletado.

    Programmatic Tool Calling: la función que cambia cómo diseñas agentes

    Esto es lo más relevante si ya construyes agentes con function calling. Antes, cada llamada a una tool implicaba un round-trip completo: el modelo pide la tool, tú la ejecutas, le devuelves el resultado, el modelo decide el siguiente paso. En un workflow con cinco o seis tools, eso son cinco o seis idas y vueltas completas al modelo.

    Programmatic Tool Calling elimina la mayoría de esos round-trips.

    Con Programmatic Tool Calling, GPT-5.6 escribe JavaScript que se ejecuta en un sandbox V8 aislado —sin acceso a red— y coordina varias llamadas a tools dentro de un mismo turno:

    // prStatusSchema y coverageSchema son schemas de Zod definidos aparte, omitidos aquí por brevedad
    const response = await client.responses.create({
      model: "gpt-5.6-sol",
      tools: [
        { type: "function", name: "get_pr_status", parameters: prStatusSchema },
        { type: "function", name: "get_test_coverage", parameters: coverageSchema },
      ],
      input: "Revisa el PR #482 y dime si está listo para mergear.",
    });
    

    Internamente, el modelo puede generar algo como esto y ejecutarlo sin volver a consultarte:

    const [status, coverage] = await Promise.all([
      tools.get_pr_status({ pr: 482 }),
      tools.get_test_coverage({ pr: 482 }),
    ]);
    

    Sin round-trips intermedios. OpenAI reporta reducciones de consumo de tokens de entre 38% y 63.5% en workflows de tools con clientes tempranos, según la documentación oficial de Programmatic Tool Calling. Si tu agente encadena varias tools de forma predecible —no necesitas el juicio del modelo entre cada paso— esta es la razón concreta para migrar a la Responses API si aún no lo has hecho.

    Ojo: esta función solo existe en la Responses API. Chat Completions no la soporta.

    Valida la salida con Zod, no confíes en el string

    Cuando le pides a GPT-5.6 —o a cualquier LLM— que devuelva JSON, vas a recibir respuestas que casi cumplen tu schema. Ese “casi” es el problema en producción.

    import { z } from "zod";
    
    const AnalisisDiffSchema = z.object({
      resumen: z.string(),
      cambiosBreaking: z.boolean(),
      archivosAfectados: z.array(z.string()),
    });
    
    const response = await client.responses.create({
      model: "gpt-5.6-terra",
      input: `Analiza este diff y responde en JSON: ${diff}`,
    });
    
    const analisis = AnalisisDiffSchema.parse(JSON.parse(response.output_text));
    

    Si el modelo devuelve un campo de más, uno de menos, o un tipo equivocado, parse lanza el error ahí mismo — no seis pasos después, cuando ya rompiste el pipeline de otro sistema. Es exactamente el patrón que trabajo en el curso de Zod para TypeScript: la validación no es opcional cuando la fuente de tus datos es un modelo probabilístico.

    Precios: Sol, Terra, Luna, y dónde entra Claude

    Modelo Input Output Contexto
    GPT-5.6 Sol $5 $30 1.05M / 128K salida
    GPT-5.6 Terra $2.50 $15 1.05M / 128K salida
    GPT-5.6 Luna $1 $6 1.05M / 128K salida
    Claude Sonnet 5 (hasta 31 ago 2026) $2 $10 1M / 128K salida
    Claude Sonnet 5 (desde 1 sep 2026) $3 $15 1M / 128K salida

    Con estos números, Terra es el punto de comparación real contra Claude Sonnet 5 — ambos apuntan al mismo caso de uso: producción, buen razonamiento, sin pagar precio de flagship. Sol solo se justifica cuando el problema es genuinamente difícil: razonamiento largo, agentes con muchos pasos, código complejo donde una respuesta mediocre cuesta más en debugging que en tokens.

    Una advertencia sobre comparar precios “por token” entre proveedores: no son manzanas con manzanas. Claude Sonnet 5 estrenó un tokenizer nuevo que genera hasta 30% más tokens para el mismo texto que su versión anterior, según la documentación oficial de Claude. El precio por millón de tokens no te dice el costo real de tu prompt — para eso necesitas correr tus prompts reales contra ambos modelos y medir.

    Cuándo quedarte en Claude Code y cuándo meter GPT-5.6 en tu stack

    Si ya tienes armado tu stack de IA agéntica alrededor de Claude Code, no hay ninguna urgencia de migrar todo. El ecosistema de agentes, skills y MCP que ya tienes montado no se traslada gratis a otro proveedor — cambiar de modelo no es cambiar un string, es revalidar todo el comportamiento agéntico que dependía de ese modelo específico.

    Donde sí tiene sentido meter GPT-5.6 en tu stack:

    • Tareas de alto volumen y baja complejidad. Usa Luna. Es una fracción del costo de Sol o de Claude Sonnet 5 para clasificación, extracción o resúmenes cortos.
    • Workflows con muchas tool calls predecibles. Programmatic Tool Calling puede recortar tu factura de forma directa, sin tocar la lógica de negocio.
    • Comparar output real en tu caso de uso. Nada reemplaza correr el mismo prompt contra Terra y contra Claude Sonnet 5 con tus datos reales, no con benchmarks genéricos.

    Este tipo de decisión —qué modelo, para qué tarea, con qué presupuesto— es exactamente el criterio que trabajamos en el curso Construye con IA: no se trata de casarte con un proveedor, se trata de construir producto sin quemar presupuesto en la elección equivocada.

    Qué hacer hoy

    Sin escribir una línea de producto nuevo, puedes:

    1. Correr tu endpoint más caro contra gpt-5.6-terra con reasoning.effort: "low" y comparar costo y calidad contra tu modelo actual.
    2. Si tu agente encadena tres o más tools por turno, prueba Programmatic Tool Calling en un flujo de staging y mide la reducción real de tokens.
    3. Añade un schema de Zod a cualquier endpoint que hoy confíe en el JSON crudo de un LLM.

    Ninguno de estos tres pasos te compromete a nada. Son experimentos de una tarde que te dan datos reales en lugar de benchmarks de marketing.

    Si quieres ver estos patrones aplicados en proyectos completos —no solo snippets sueltos— en Dominicode Labs están el código y las decisiones de arquitectura detrás de cada integración.

    Preguntas frecuentes

    ¿Qué significa que GPT-5.6 tenga tres variantes (Sol, Terra, Luna)?

    OpenAI separó el número de versión del nivel de capacidad. El 5.6 es la generación; Sol, Terra y Luna son niveles de capacidad que pueden evolucionar en su propio calendario, sin esperar a un salto de versión completo.

    ¿Qué variante de GPT-5.6 debo usar por defecto en producción?

    Depende del caso de uso. Terra es el punto de equilibrio para la mayoría de aplicaciones de producción. Sol solo se justifica en tareas de razonamiento largo o agentes con muchos pasos. Luna sirve para volumen alto y tareas simples, donde el costo por token importa más que el techo de capacidad.

    ¿GPT-5.6 es más barato o más caro que Claude Sonnet 5?

    Depende de la variante. Terra ($2.50 input / $15 output por millón de tokens) queda cerca del precio estándar de Claude Sonnet 5 ($3 / $15 desde el 1 de septiembre de 2026). Sol es notablemente más caro. Luna es la opción más económica de las dos familias. Compara con tus prompts reales, no solo con la tabla de precios — los tokenizers no son iguales entre proveedores.

    ¿Necesito reescribir mi código si vengo de GPT-5.5?

    En parte. El endpoint de la Responses API se mantiene, pero OpenAI recomienda tratar la migración como un ajuste de reasoning.effort, no solo un cambio de string en el nombre del modelo. Probar un nivel de esfuerzo por debajo del que usabas suele mantener la calidad con menos costo.

    ¿Programmatic Tool Calling funciona con Chat Completions?

    No. Es una función exclusiva de la Responses API. Si tu integración sigue en Chat Completions, no tienes acceso a esta función ni a otras capacidades nuevas de la generación GPT-5.6.


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

  • Cómo correr LLMs locales en 2026: Guía de hardware y modelos

    Cómo correr LLMs locales en 2026: Guía de hardware y modelos

    El mes pasado vi la factura de API de OpenAI de un desarrollador independiente que estaba probando un agente de traducción automática de bases de datos. Había consumido $842 USD en un solo fin de semana debido a un bucle infinito de prompts que devoró el contexto de su modelo repetidamente.

    Casi le da algo.

    La experimentación con agentes de IA es el futuro, pero depender ciegamente de APIs en la nube puede ser una ruina financiera para desarrolladores independientes o empresas con políticas estrictas de privacidad.

    Hoy te quiero explicar cómo configurar tu entorno para correr LLMs locales en 2026, analizando qué hardware necesitas realmente y qué modelos de código abierto superan a las opciones comerciales para desarrollo local.


    Por qué el desarrollo local es el estándar en 2026

    Hasta hace poco, correr un modelo en tu propio ordenador era una experiencia frustrante: los modelos pequeños de 7B parámetros eran lentos, "alucinaban" demasiado y carecían de capacidades de razonamiento para escribir código complejo.

    En 2026, la situación ha cambiado radicalmente por tres factores:

    1. Eficiencia en la cuantización: Gracias a formatos avanzados de compresión (como GGUF y EXL2), un modelo de 8B o 14B parámetros mantiene el 98% de su precisión consumiendo la mitad de VRAM.
    2. Capacidad de razonamiento nativa: Modelos como Llama 3.3, Qwen 2.5 Coder y la serie DeepSeek R1 en local ofrecen razonamiento avanzado sin salir de tu máquina.
    3. Privacidad absoluta: Tus datos de código, logs de clientes y bases de datos nunca viajan por internet.

    Correr modelos locales es la mejor forma de testear tus agentes y automatizaciones antes de desplegarlos a producción en la nube.


    El Hardware que necesitas (VRAM es el único rey)

    El error más común al planificar un entorno local de IA es invertir en procesadores rápidos (CPU) o grandes cantidades de memoria RAM convencional. Para la IA, la velocidad del procesamiento y la latencia dependen de la VRAM (Memoria de Vídeo) de tu tarjeta gráfica.

    Aquí tienes la matriz de hardware recomendada según tu presupuesto y objetivos en 2026:

    Nivel Hardware Mínimo Capacidad de Modelos
    Básico (Estudiante) GPU de 8GB VRAM (RTX 4060) o Mac M-Series (16GB RAM) Llama 3.2 3B / Qwen 2.5 Coder 7B (Cuantizados)
    Sweet Spot (Developer) GPU de 16GB VRAM (RTX 4080 / 4070Ti) o Mac M-Series (36GB RAM) Llama 3.1 8B / Qwen 2.5 Coder 14B (Precisión Completa)
    Avanzado (Enterprise) 2x GPU de 24GB VRAM (RTX 3090/4090) o Mac Studio (64GB+ RAM) Llama 3.3 70B / DeepSeek R1 32B (Razonamiento Completo)

    Si eres usuario de Mac, la memoria unificada de Apple Silicon funciona como VRAM. Un Mac Mini o Macbook Pro con 36GB o 64GB de RAM unificada es una de las soluciones más eficientes y silenciosas para correr agentes locales.


    Los mejores modelos locales para Developers en 2026

    Si tu objetivo principal es escribir código, configurar bases de datos o crear agentes DevOps, no uses modelos genéricos. Estos son los reyes del código abierto en 2026:

    • Qwen 2.5 Coder (7B y 14B): Es el rey indiscutible para autocompletado y edición en IDEs como Cursor o VS Code. Supera a muchos modelos propietarios en sintaxis de TypeScript, Python y Rust.
    • Llama 3.1 (8B) / Llama 3.3 (70B): La opción de Meta es la más estable para agentes conversacionales que requieren memoria semántica persistente o integrarse con herramientas externas.
    • DeepSeek R1 (Versiones destiladas de 8B o 14B): Excelente para resolución de bugs complejos y optimización de algoritmos que requieren pasos de pensamiento lógico antes de emitir una respuesta.

    Setup de Arranque Rápido con Ollama

    La forma más sencilla de empezar hoy es utilizar Ollama, una herramienta que gestiona los modelos locales en segundo plano y expone una API compatible con OpenAI para que puedas conectarla a cualquier aplicación.

    1. Descarga Ollama de su sitio oficial.
    2. Ejecuta en tu terminal el modelo deseado:
      ollama run qwen2.5-coder:7b
      
    3. Conecta tus agentes o herramientas de desarrollo apuntando la API Base a: http://localhost:11434/v1.

    Este es exactamente el flujo de base local que enseñamos a configurar y optimizar en nuestro curso de Construye con IA para evitar costes recurrentes de API durante el desarrollo de productos.


    Conclusión: Controla tus costes de desarrollo

    Depender exclusivamente de la nube no solo te hace vulnerable a caídas de red y cambios de precios de API, sino que limita tu velocidad de experimentación. Al aprender a correr LLMs locales, desbloqueas pruebas infinitas y seguras, las cuales son ideales para testear el bucle agéntico o agentic loop sin costes de API.

    Si quieres debatir sobre configuraciones de hardware personalizadas, benchmarks de modelos en local y cómo conectar estos LLMs a tus pipelines de producción, te espero en Dominicode Labs.


    Preguntas Frecuentes (FAQ)

    ¿Se pueden correr LLMs locales en 2026 sin tarjeta gráfica (GPU)?

    Sí, herramientas como Ollama y Llama.cpp admiten ejecución en CPU utilizando la memoria RAM del sistema. Sin embargo, la velocidad de generación (tokens por segundo) será extremadamente lenta en comparación con una GPU, lo que los hace poco prácticos para flujos de desarrollo ágiles.

    ¿Qué es la cuantización de un modelo de IA?

    Es un proceso de compresión matemática que reduce la precisión de los pesos del modelo (por ejemplo, de 16 bits a 4 u 8 bits). Esto reduce drásticamente el uso de VRAM y memoria, permitiendo correr modelos grandes en tarjetas gráficas de gama media con una pérdida de precisión casi imperceptible.

    ¿Ollama es compatible con herramientas como Cursor o VS Code?

    Sí, Ollama expone un servidor local compatible con la especificación de API de OpenAI. Puedes configurar tu editor de código o framework de agentes favorito para que use la URL http://localhost:11434 como proveedor personalizado y consuma tus modelos locales de forma directa.

    ¿Qué modelo local es mejor para desarrollo de software en 2026?

    Para autocompletado y redacción de código rápido, Qwen 2.5 Coder (en sus variantes de 7B o 14B) ofrece el mejor rendimiento en relación al consumo de recursos. Para tareas complejas de depuración o lógica pesada, las variantes cuantizadas de DeepSeek R1 son la opción recomendada.


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

  • NestJS + Vercel AI SDK: backend streaming IA en producción

    NestJS + Vercel AI SDK: backend streaming IA en producción

    ANTHROPIC_API_KEY=sk-ant-xxxxxxxx

    
    En `app.module.ts`, registra `ConfigModule`:
    
    ```typescript
    // src/app.module.ts
    import { Module } from '@nestjs/common';
    import { ConfigModule } from '@nestjs/config';
    import { AiModule } from './ai/ai.module';
    
    @Module({
      imports: [
        ConfigModule.forRoot({ isGlobal: true }),
        AiModule,
      ],
    })
    export class AppModule {}
    

    isGlobal: true significa que ConfigService está disponible en todos los módulos sin importarlo individualmente. Práctico.


    La estructura del AiModule

    Antes de escribir código, la estructura:

    src/
      ai/
        ai.module.ts
        ai.controller.ts
        ai.service.ts
        dto/
          chat.dto.ts
    

    Cuatro archivos. Eso es todo lo que necesita un endpoint de streaming limpio.


    Paso 1: El DTO de validación

    El primer punto de defensa es el DTO. Define el contrato del request:

    // src/ai/dto/chat.dto.ts
    import { IsArray, IsIn, IsString, ValidateNested, ArrayMinSize } from 'class-validator';
    import { Type } from 'class-transformer';
    
    export class ChatMessageDto {
      @IsIn(['user', 'assistant', 'system'])
      role: 'user' | 'assistant' | 'system';
    
      @IsString()
      content: string;
    }
    
    export class ChatRequestDto {
      @IsArray()
      @ArrayMinSize(1)
      @ValidateNested({ each: true })
      @Type(() => ChatMessageDto)
      messages: ChatMessageDto[];
    }
    

    @ValidateNested({ each: true }) valida cada elemento del array individualmente. Si el frontend manda un mensaje con role: 'hacker' o sin content, el request rebota antes de tocar el servicio.

    Para que ValidationPipe funcione globalmente, añádelo en main.ts:

    // src/main.ts
    import { NestFactory } from '@nestjs/core';
    import { ValidationPipe } from '@nestjs/common';
    import { AppModule } from './app.module';
    
    async function bootstrap() {
      const app = NestFactory.create(AppModule);
    
      app.useGlobalPipes(new ValidationPipe({
        transform: true,
        whitelist: true,    // elimina propiedades no declaradas en el DTO
        forbidNonWhitelisted: true,
      }));
    
      // CORS para el frontend Angular en desarrollo
      app.enableCors({
        origin: process.env.FRONTEND_URL ?? 'http://localhost:4200',
        methods: ['POST', 'OPTIONS'],
      });
    
      await app.listen(process.env.PORT ?? 3000);
    }
    
    bootstrap();
    

    whitelist: true es especialmente importante aquí: elimina cualquier campo del body que no esté declarado en el DTO. Si alguien intenta inyectar propiedades extra en el request, NestJS las ignora antes de que lleguen al servicio.


    Paso 2: El AiService

    El servicio encapsula toda la lógica de llamada al modelo. El controlador no sabe qué modelo usamos ni cómo se configura — solo llama al servicio y recibe el stream.

    // src/ai/ai.service.ts
    import { Injectable } from '@nestjs/common';
    import { ConfigService } from '@nestjs/config';
    import { streamText, CoreMessage } from 'ai';
    import { createAnthropic } from '@ai-sdk/anthropic';
    
    @Injectable()
    export class AiService {
      private readonly anthropic;
    
      constructor(private readonly config: ConfigService) {
        this.anthropic = createAnthropic({
          apiKey: this.config.getOrThrow<string>('ANTHROPIC_API_KEY'),
        });
      }
    
      streamChat(messages: CoreMessage[]) {
        return streamText({
          model: this.anthropic('claude-sonnet-4-6'),
          system: `Eres un asistente técnico especializado en desarrollo de software.
    Responde en español de forma concisa y directa.
    Si el usuario pregunta sobre código, incluye ejemplos concretos.`,
          messages,
          maxTokens: 1024,
        });
      }
    }
    

    Dos decisiones importantes aquí:

    createAnthropic({ apiKey }) en el constructor — el cliente de Anthropic se crea una sola vez cuando NestJS instancia el servicio. No se recrea en cada petición. Eso evita overhead innecesario.

    config.getOrThrow<string>('ANTHROPIC_API_KEY') — si la variable de entorno no existe, la app falla en el arranque con un error claro en lugar de fallar silenciosamente en el primer request. Fail fast.

    maxTokens: 1024 es un límite defensivo. Sin él, un usuario puede hacer una pregunta que genere una respuesta de 8.000 tokens, multiplicando el costo por 8. Ajusta según tu caso de uso.


    Paso 3: El AiController con streaming

    El controlador es donde ocurre la magia del streaming. La clave está en cómo NestJS maneja la respuesta HTTP nativa:

    // src/ai/ai.controller.ts
    import {
      Controller,
      Post,
      Body,
      Res,
      HttpCode,
      HttpStatus,
    } from '@nestjs/common';
    import { Response } from 'express';
    import { AiService } from './ai.service';
    import { ChatRequestDto } from './dto/chat.dto';
    import { CoreMessage } from 'ai';
    
    @Controller('api')
    export class AiController {
      constructor(private readonly aiService: AiService) {}
    
      @Post('chat')
      @HttpCode(HttpStatus.OK)
      async chat(
        @Body() body: ChatRequestDto,
        @Res() res: Response,
      ): Promise<void> {
        const messages = body.messages as CoreMessage[];
    
        const result = this.aiService.streamChat(messages);
    
        // toUIMessageStreamResponse() genera una Response Web estándar
        // con el protocolo SSE del AI SDK
        const streamResponse = result.toUIMessageStreamResponse();
    
        // Propagamos los headers del AI SDK a la respuesta de Express
        streamResponse.headers.forEach((value, key) => {
          res.setHeader(key, value);
        });
    
        res.status(streamResponse.status);
    
        // Volcamos el body del ReadableStream a la respuesta de Express
        if (streamResponse.body) {
          const reader = streamResponse.body.getReader();
    
          const pump = async () => {
            while (true) {
              const { done, value } = await reader.read();
              if (done) {
                res.end();
                break;
              }
              res.write(value);
            }
          };
    
          pump().catch((err) => {
            console.error('[AiController] Error en stream:', err);
            if (!res.headersSent) {
              res.status(500).json({ error: 'Error interno del stream' });
            } else {
              res.end();
            }
          });
        } else {
          res.status(500).json({ error: 'No se pudo iniciar el stream' });
        }
      }
    }
    

    ¿Por qué este patrón de pump manual en lugar de pipe()?

    toUIMessageStreamResponse() devuelve una Response Web estándar (la del spec WHATWG), no un stream de Node.js. Express trabaja con streams de Node.js. El pump manual convierte uno en el otro sin dependencias adicionales. Es verboso pero explícito — sabes exactamente qué hace cada línea.

    El bloque catch en el pump gestiona dos escenarios: si el error ocurre antes de enviar headers, devuelve un 500 con JSON. Si ocurre después (cuando el stream ya está activo), llama a res.end() para cerrar la conexión limpiamente. Sin este manejo, el cliente se quedaría esperando indefinidamente.


    Paso 4: El AiModule

    El módulo agrupa las tres piezas:

    // src/ai/ai.module.ts
    import { Module } from '@nestjs/common';
    import { AiController } from './ai.controller';
    import { AiService } from './ai.service';
    
    @Module({
      controllers: [AiController],
      providers: [AiService],
      exports: [AiService], // por si otros módulos necesitan AiService
    })
    export class AiModule {}
    

    Exportar AiService es una decisión de diseño: si en el futuro un módulo de AgentsModule o DocumentModule necesita llamar al modelo, importan AiModule y tienen el servicio disponible sin duplicar configuración.


    Rate limiting: el paso que nadie incluye

    Sin rate limiting, un solo usuario puede vaciar tu cuota de Anthropic en minutos. NestJS tiene @nestjs/throttler para esto:

    npm install @nestjs/throttler
    

    Configúralo en AppModule:

    // src/app.module.ts
    import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
    import { APP_GUARD } from '@nestjs/core';
    
    @Module({
      imports: [
        ConfigModule.forRoot({ isGlobal: true }),
        ThrottlerModule.forRoot([{
          name: 'short',
          ttl: 60_000,   // 1 minuto en ms
          limit: 10,     // máximo 10 requests por minuto por IP
        }]),
        AiModule,
      ],
      providers: [
        {
          provide: APP_GUARD,
          useClass: ThrottlerGuard,
        },
      ],
    })
    export class AppModule {}
    

    10 requests por minuto por IP es un límite conservador para un chat. En producción, ajusta según el plan de Anthropic que tengas y el perfil de uso esperado. Si tus usuarios son developers que mandan snippets de código largos, 10 puede ser demasiado restrictivo. Si es un chat de soporte con usuarios anónimos, puede ser demasiado permisivo.

    ThrottlerGuard como APP_GUARD aplica el límite a todos los endpoints automáticamente. Si quieres excluir algunos endpoints del límite, usa el decorador @SkipThrottle() en el controlador correspondiente.


    Conectar con el frontend Angular

    Este backend está diseñado para ser el complemento del post Angular v22 + Vercel AI SDK: streaming de IA en tu app en 20 minutos.

    El frontend Angular usa fetch nativo con ReadableStream. El cambio que necesitas en el componente Angular es mínimo: actualizar la URL del endpoint del servidor Bun del post anterior (típicamente en el puerto 4000) a http://localhost:3000/api/chat de este servidor NestJS. El contrato del API no cambia — misma ruta, mismo formato de mensajes.

    La diferencia está en el protocolo de stream. El servidor Bun del post anterior usa toTextStreamResponse(), que devuelve texto plano. Este NestJS usa toUIMessageStreamResponse(), que usa el protocolo SSE estructurado del AI SDK. Para consumir este protocolo desde Angular sin la librería useChat de React, el componente Angular necesita parsear los chunks SSE en lugar de concatenarlos directamente.

    Si ya tienes el frontend del post anterior y quieres migrar a este backend sin tocar el componente, cambia en AiService.streamChat() el retorno a toTextStreamResponse():

    // AiService — variante compatible con el componente Angular del post anterior
    streamChat(messages: CoreMessage[]) {
      return streamText({
        model: this.anthropic('claude-sonnet-4-6'),
        system: 'Eres un asistente técnico...',
        messages,
        maxTokens: 1024,
      });
      // En el controlador usar toTextStreamResponse() en vez de toUIMessageStreamResponse()
    }
    

    Y en el controlador, sustituye result.toUIMessageStreamResponse() por result.toTextStreamResponse(). El componente Angular del post anterior funciona sin cambios.

    La versión con toUIMessageStreamResponse() es la recomendada para proyectos nuevos porque soporta tool calls, metadatos de uso de tokens, y datos personalizados dentro del mismo stream — funcionalidades que toTextStreamResponse() no puede transmitir.

    Característica toUIMessageStreamResponse() toTextStreamResponse()
    Protocolo AI SDK SSE estructurado Texto plano
    Tool calls
    Metadatos de tokens
    Compatible con useChat
    Parsing manual en cliente Necesario sin useChat No necesario
    Cuándo usarlo Proyectos nuevos Compatibilidad con cliente simple

    Manejo de errores: más allá del try/catch

    El error handling que ya tenemos en el pump del controlador cubre los fallos en el stream activo. Pero hay errores que ocurren antes del stream — cuando la API de Anthropic devuelve un 429 (rate limit) o un 500:

    // src/ai/ai.controller.ts — versión con manejo de errores completo
    import { APICallError } from 'ai';
    
    @Post('chat')
    @HttpCode(HttpStatus.OK)
    async chat(
      @Body() body: ChatRequestDto,
      @Res() res: Response,
    ): Promise<void> {
      try {
        const messages = body.messages as CoreMessage[];
        const result = this.aiService.streamChat(messages);
        const streamResponse = result.toUIMessageStreamResponse();
    
        streamResponse.headers.forEach((value, key) => {
          res.setHeader(key, value);
        });
        res.status(streamResponse.status);
    
        if (streamResponse.body) {
          const reader = streamResponse.body.getReader();
    
          const pump = async () => {
            while (true) {
              const { done, value } = await reader.read();
              if (done) { res.end(); break; }
              res.write(value);
            }
          };
    
          await pump();
        }
      } catch (error) {
        if (APICallError.isInstance(error)) {
          // Error de la API del LLM (429, 500, etc.)
          console.error('[AiController] Error API LLM:', error.message, error.statusCode);
    
          if (!res.headersSent) {
            const statusCode = error.statusCode === 429 ? 429 : 502;
            res.status(statusCode).json({
              error: error.statusCode === 429
                ? 'Demasiadas peticiones al modelo. Inténtalo en unos segundos.'
                : 'Error al conectar con el modelo de IA.',
            });
          } else {
            res.end();
          }
        } else {
          console.error('[AiController] Error inesperado:', error);
          if (!res.headersSent) {
            res.status(500).json({ error: 'Error interno del servidor.' });
          } else {
            res.end();
          }
        }
      }
    }
    

    APICallError.isInstance(error) es el type guard del AI SDK para distinguir errores de la API del LLM de errores genéricos. Útil para devolver mensajes de error específicos al cliente sin exponer detalles internos.


    Ejecutar el servidor

    # Desarrollo con hot reload
    npm run start:dev
    
    # Producción
    npm run build && npm run start:prod
    

    El servidor levanta en http://localhost:3000. Prueba el endpoint:

    curl -X POST http://localhost:3000/api/chat \
      -H "Content-Type: application/json" \
      -d '{"messages": [{"role": "user", "content": "Qué es NestJS en una frase"}]}' \
      --no-buffer
    

    Verás los chunks SSE llegar en tiempo real en la terminal. Eso confirma que el streaming funciona.


    El AiModule en producción: qué añadir después

    Lo que hemos construido es una base sólida. En un entorno de producción real, los siguientes pasos son:

    1. Autenticación. Añadir un AuthGuard de JWT al endpoint chat para que solo usuarios autenticados consuman tokens. Sin esto, cualquiera con la URL puede vaciar tu cuota.

    2. Logging estructurado. Usar @nestjs/winston o Pino para loguear cada request con userId, messageCount, y tokensUsed. El AI SDK expone usage en el stream — puedes capturarlo en el onFinish callback de streamText.

    3. Persistencia del historial. El backend actual es stateless — el historial viene del cliente en cada request. En producción con usuarios autenticados, guarda el historial en base de datos y envía solo el conversationId desde el frontend. El servidor reconstruye el historial antes de llamar al modelo.

    4. Selección de modelo por request. Si tu app da a los usuarios la opción de elegir entre Claude Sonnet y Claude Haiku (más barato), añade un campo model al DTO y pásalo al servicio. La abstracción del AI SDK hace que el cambio sea trivial.

    Si quieres profundizar en este tipo de decisiones de arquitectura — cómo estructurar un producto completo con IA desde la idea hasta producción — en el curso Construye con IA: de la idea al producto con Claude Code lo vemos con proyectos reales, no con demos de laboratorio.


    FAQ

    ¿Puedo usar este módulo con Fastify en lugar de Express?

    Sí, pero el pump manual del controlador cambia. Fastify usa Reply en lugar de Response de Express, y el método para escribir chunks es reply.raw.write(). El @Res() res: Response del controlador funcionará si configuras passThrough: true en el decorador: @Res({ passThrough: false }). La lógica del pump en sí no cambia — solo los métodos de la respuesta.

    ¿El rate limiting con ThrottlerGuard funciona bien detrás de un proxy o load balancer?

    Por defecto, ThrottlerGuard usa la IP del request. Si tu app está detrás de un proxy (Nginx, Cloudflare, etc.), la IP será siempre la del proxy. Configura ThrottlerModule con throttlers y usa ThrottlerGuard extendido que lea X-Forwarded-For. Alternativamente, delega el rate limiting al proxy — Nginx tiene limit_req_zone para esto.

    ¿Cómo evito que el stream consuma tokens si el cliente desconecta?

    streamText del AI SDK no cancela automáticamente la petición a Anthropic cuando el cliente cierra la conexión HTTP. Para implementar cancelación, pasa un AbortSignal a streamText:

    streamChat(messages: CoreMessage[], signal?: AbortSignal) {
      return streamText({
        model: this.anthropic('claude-sonnet-4-6'),
        messages,
        abortSignal: signal,
      });
    }
    

    En el controlador, escucha el evento close de la respuesta y llama a abortController.abort(). Esto cancela la llamada a la API antes de que el modelo termine de generar.

    ¿Puedo usar @ai-sdk/openai o @ai-sdk/google en lugar de Anthropic?

    Sí. Cambia createAnthropic por createOpenAI o createGoogleGenerativeAI en AiService y actualiza el nombre del modelo. El resto del módulo — controlador, DTO, rate limiting, manejo de errores — no cambia. Esa es exactamente la ventaja de usar el AI SDK como capa de abstracción: cambias de proveedor en un sitio.

    ¿CoreMessage[] es compatible con el formato de mensajes que manda el componente Angular del post anterior?

    CoreMessage del AI SDK acepta objetos con role ('user', 'assistant', 'system') y content (string). El ChatMessage del componente Angular del post anterior tiene exactamente esa forma. El cast body.messages as CoreMessage[] funciona directamente — no necesitas transformar nada.


    Cierre

    Un backend de streaming de IA no es complicado. Lo que sí es complicado es hacerlo bien desde el principio: que valide los inputs, que no queme tokens cuando el cliente desconecta, que no se caiga cuando Anthropic devuelve un 429, que tenga un límite razonable de peticiones por IP.

    NestJS más el Vercel AI SDK resuelven ese conjunto de problemas con una arquitectura que ya conoces si llevas tiempo en el ecosistema TypeScript. No hay magia — hay módulos, servicios, inyección de dependencias, y un stream que fluye limpio de principio a fin.

    El AiModule que has construido hoy es reutilizable. Impórtalo en cualquier NestJS existente, ajusta el system prompt y el modelo, y tienes un endpoint de IA en producción en menos de una hora.

    Si quieres llevarlo más lejos — tool calls, agentes con memoria, pipelines de documentos — en Dominicode Labs tenemos los proyectos completos con los patrones que usamos en producción, incluyendo ejemplos de NestJS con AI SDK con autenticación, persistencia y cancelación de streams.


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

  • Prompt Caching en Claude: reduce tu factura de API un 90%

    Prompt Caching en Claude: reduce tu factura de API un 90%

    El mes pasado revisé los gastos de API de un proyecto que lleva seis semanas en producción. Un agente conversacional para análisis de documentos legales. El cliente lo usa unas 40 veces al día.

    La factura: $340 en un mes.

    El system prompt tenía 8.000 tokens. Las definiciones de herramientas, otros 3.000. En cada llamada, esos 11.000 tokens se procesaban desde cero. Cuarenta veces al día. Treinta días al mes.

    Activé prompt caching. La siguiente factura: $38.

    No cambié la lógica del agente. No modifiqué los prompts. Solo añadí tres líneas de configuración.

    Eso es lo que hace el prompt caching de Claude. Y la mayoría de developers que trabajan con la API de Anthropic aún no lo tienen activado.


    Qué es el prompt caching y cómo funciona

    Cuando haces una llamada a la API de Claude, pagas por cada token que el modelo procesa. System prompt, herramientas, historial de conversación, contexto de documentos: todo se cobra como tokens de entrada.

    El problema es que en la mayoría de aplicaciones reales, una parte enorme de esos tokens es idéntica en cada llamada. Tu system prompt no cambia. Las definiciones de tus herramientas no cambian. El contexto de un documento que estás analizando no cambia entre preguntas del usuario.

    El prompt caching te permite marcar esas partes estáticas para que Claude las almacene en caché. La documentación oficial de prompt caching cubre todos los modelos y casos edge. La primera vez que se procesa ese contenido, se escribe en caché. En las llamadas posteriores, en lugar de reprocesar esos tokens, Claude los lee desde el caché.

    El coste de un cache write es 1.25x el precio base — ligeramente más caro que una llamada normal. El coste de un cache read es 0.1x el precio base. Es decir, un 90% más barato.

    En un agent loop con 40 llamadas al día, pagas el 1.25x una vez. Las otras 39 veces pagas el 0.1x. La aritmética es brutal a tu favor.

    El TTL del caché

    El caché tiene un TTL (Time To Live) de 5 minutos por defecto. Mientras haya llamadas dentro de esa ventana, el caché se renueva automáticamente sin coste adicional. Si una conversación tiene mensajes frecuentes, el caché se mantiene activo.

    Existe también un TTL de 1 hora, que cuesta 2x el precio base en la escritura. Útil cuando tienes contextos que se reutilizan con menos frecuencia pero son muy costosos de regenerar.

    El mínimo de tokens para activar el caché

    No todo se puede cachear. El sistema exige un mínimo de tokens para crear una entrada de caché. Para claude-sonnet-4-6 y claude-opus-4-8, el mínimo es 1.024 tokens. Para claude-haiku-4-5, el umbral sube a 4.096 tokens — cuatro veces más alto, relevante si usas Haiku con prompts cortos. Si tu system prompt tiene menos tokens que el mínimo de tu modelo, el caché no se activa.

    En proyectos donde el system prompt es corto, la estrategia correcta es incluir el contexto del dominio directamente en el system prompt hasta superar ese umbral, o cachear las definiciones de herramientas junto con el sistema.


    Cómo habilitarlo: código TypeScript con el SDK oficial

    Aquí está el patrón que uso en producción. Nada de magia — tres cambios concretos en tu código.

    Habilitación básica: system prompt con cache_control

    import Anthropic from "@anthropic-ai/sdk";
    
    const client = new Anthropic();
    
    const response = await client.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 1024,
      system: [
        {
          type: "text",
          text: `Eres un asistente especializado en análisis de documentos legales.
          
    Tu rol es:
    - Identificar cláusulas de riesgo en contratos
    - Resumir términos clave de forma clara y precisa
    - Señalar inconsistencias o ambigüedades legales
    - Comparar términos con estándares del sector
    
    [...aquí va el resto del system prompt extenso, con contexto del dominio,
    instrucciones detalladas, ejemplos de formato de respuesta, etc.
    Debe superar los 1.024 tokens para activar el caché...]`,
          cache_control: { type: "ephemeral" }, // <-- esto es todo lo que necesitas
        },
      ],
      messages: [
        {
          role: "user",
          content: "Analiza la cláusula de terminación de este contrato: ...",
        },
      ],
    });
    
    console.log(response.usage);
    

    En la primera llamada, usage mostrará:

    {
      "input_tokens": 45,
      "cache_creation_input_tokens": 1280,
      "cache_read_input_tokens": 0,
      "output_tokens": 312
    }
    

    En la segunda llamada (dentro de los 5 minutos):

    {
      "input_tokens": 45,
      "cache_creation_input_tokens": 0,
      "cache_read_input_tokens": 1280,
      "output_tokens": 289
    }
    

    cache_read_input_tokens tiene el 10% del coste. El system prompt completo se leyó desde caché. Esos 1.280 tokens no se procesaron desde cero.

    Cacheando herramientas y system prompt juntos

    Cuando tienes definiciones de herramientas largas — algo habitual en agentes con MCP o con múltiples funciones — el ahorro se multiplica. Aquí el patrón para cachear ambas cosas:

    import Anthropic from "@anthropic-ai/sdk";
    
    const client = new Anthropic();
    
    // Las definiciones de herramientas son estáticas — candidatas perfectas para caché
    const tools: Anthropic.Tool[] = [
      {
        name: "search_legal_database",
        description: `Busca en la base de datos legal precedentes y jurisprudencia relevante.
        Usa esta herramienta cuando necesites comparar cláusulas con casos anteriores o
        encontrar interpretaciones judiciales de términos específicos. La búsqueda incluye
        bases de datos de España, México, Argentina y Colombia. Devuelve hasta 10 resultados
        ordenados por relevancia con fecha, tribunal y resumen del caso.`,
        input_schema: {
          type: "object" as const,
          properties: {
            query: {
              type: "string",
              description: "Término o frase legal a buscar",
            },
            jurisdiction: {
              type: "string",
              enum: ["ES", "MX", "AR", "CO", "ALL"],
              description: "Jurisdicción a consultar",
            },
            date_range: {
              type: "string",
              description: "Rango de fechas en formato YYYY-YYYY",
            },
          },
          required: ["query"],
        },
      },
      {
        name: "analyze_clause_risk",
        description: `Analiza el nivel de riesgo de una cláusula contractual.
        Evalúa factores como onerosidad excesiva, cláusulas abusivas según legislación
        vigente, asimetría de obligaciones y exposición a penalidades. Devuelve un score
        de riesgo del 1 al 10 con justificación detallada y recomendaciones de negociación.`,
        input_schema: {
          type: "object" as const,
          properties: {
            clause_text: {
              type: "string",
              description: "Texto completo de la cláusula a analizar",
            },
            contract_type: {
              type: "string",
              description: "Tipo de contrato (laboral, mercantil, arrendamiento, etc.)",
            },
          },
          required: ["clause_text"],
        },
      },
      // cache_control al final del array de tools — marca el punto de caché
    ];
    
    // Añadimos cache_control al último tool para cachear todo el bloque
    const toolsWithCache = tools.map((tool, index) =>
      index === tools.length - 1
        ? { ...tool, cache_control: { type: "ephemeral" as const } }
        : tool
    );
    
    const response = await client.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 2048,
      system: [
        {
          type: "text",
          text: "Eres un asistente especializado en análisis legal...",
          cache_control: { type: "ephemeral" }, // system prompt cacheado
        },
      ],
      tools: toolsWithCache, // tools cacheadas
      messages: [
        {
          role: "user",
          content: "¿Cuál es el riesgo de esta cláusula de no competencia?",
        },
      ],
    });
    

    Monitorizar el ahorro en tiempo real

    Esta función te dice exactamente cuánto has ahorrado en cada llamada:

    interface CostMonitor {
      inputTokensCost: number;
      cacheWriteCost: number;
      cacheReadCost: number;
      outputTokensCost: number;
      totalCost: number;
      savings: number;
      savingsPercent: number;
    }
    
    // Precios para claude-sonnet-4-6 por millón de tokens (en dólares)
    const PRICING = {
      input: 3.0,
      cacheWrite: 3.75, // 1.25x
      cacheRead: 0.3,   // 0.1x
      output: 15.0,
    };
    
    function calculateCallCost(usage: Anthropic.Usage): CostMonitor {
      const inputCost = (usage.input_tokens / 1_000_000) * PRICING.input;
      const cacheWriteCost =
        ((usage.cache_creation_input_tokens ?? 0) / 1_000_000) * PRICING.cacheWrite;
      const cacheReadCost =
        ((usage.cache_read_input_tokens ?? 0) / 1_000_000) * PRICING.cacheRead;
      const outputCost = (usage.output_tokens / 1_000_000) * PRICING.output;
    
      const totalCost = inputCost + cacheWriteCost + cacheReadCost + outputCost;
    
      // Coste hipotético sin caché (todos los tokens al precio base)
      const totalInputTokens =
        usage.input_tokens +
        (usage.cache_creation_input_tokens ?? 0) +
        (usage.cache_read_input_tokens ?? 0);
      const costWithoutCache =
        (totalInputTokens / 1_000_000) * PRICING.input + outputCost;
    
      const savings = costWithoutCache - totalCost;
      const savingsPercent =
        costWithoutCache > 0 ? (savings / costWithoutCache) * 100 : 0;
    
      return {
        inputTokensCost: inputCost,
        cacheWriteCost,
        cacheReadCost,
        outputTokensCost: outputCost,
        totalCost,
        savings,
        savingsPercent,
      };
    }
    
    // Uso:
    const monitor = calculateCallCost(response.usage);
    console.log(`Ahorro: $${monitor.savings.toFixed(6)} (${monitor.savingsPercent.toFixed(1)}%)`);
    

    Qué debes cachear y qué no

    Los mejores candidatos para el caché

    System prompts largos. Es el caso más obvio. Si tu system prompt tiene instrucciones de rol, reglas de formato, contexto del dominio y ejemplos, estás mirando fácilmente 2.000-8.000 tokens que se repiten en cada llamada. Cachear el system prompt es lo primero que debes activar.

    Definiciones de herramientas (tools). Especialmente en agentes con MCP o con muchas funciones. Las definiciones de tools incluyen nombres, descripciones detalladas y schemas completos. Pueden sumar 3.000-5.000 tokens fácilmente. Son siempre estáticas dentro de una sesión.

    Contexto de documentos. Si tu aplicación analiza un documento largo (un contrato, una especificación técnica, un PDF), ese documento va en el mensaje del usuario pero cambia muy poco. Puedes cachearlo con cache_control en el bloque del contenido del mensaje.

    Historial de conversación en agent loops. En un loop donde el agente tiene muchos turnos, cachear el historial acumulado evita pagar por reprocesar el contexto completo en cada iteración.

    Qué NO debes cachear

    El turno actual del usuario. Es el error más común. El mensaje que el usuario acaba de escribir cambia en cada llamada — si intentas cachearlo, el caché nunca tendrá un hit porque el contenido es siempre distinto.

    Tokens de extended thinking. Si usas extended thinking con Claude, los tokens del proceso de razonamiento interno no se cachean. Esto es relevante si estás midiendo ahorros en pipelines que usan thinking — los números no escalarán de la misma forma.

    Contenido que cambia con frecuencia. Si tienes un bloque de contexto que se actualiza cada pocos minutos (resultados de una búsqueda en tiempo real, estado de una sesión volátil), no tiene sentido marcarlo para caché porque nunca habrá un hit.

    Bloques demasiado pequeños. Si un bloque tiene menos de 1.024 tokens, el sistema no lo cacheará. No añadas cache_control a fragmentos pequeños — solo añade latencia sin beneficio.


    Comparación de coste: sin caching vs con caching

    Escenario real: un agente con 40 llamadas diarias durante 30 días.

    • System prompt: 5.000 tokens
    • Tools: 3.000 tokens
    • Pregunta del usuario: ~100 tokens (variable)
    • Respuesta del modelo: ~400 tokens (variable)
    • Modelo: claude-sonnet-4-6
    Escenario Coste por llamada Total mensual
    Sin caching (8.100 input + 400 output) $0.0303 $36.36
    Con caching — 1ª llamada del día (cache write 8.000 + 100 input + 400 output) $0.037
    Con caching — llamadas 2–40 (cache read 8.000 + 100 input + 400 output) $0.0084
    Con caching — total diario (1ª + 39 × $0.0084) $0.365/día $10.95

    Ahorro: 70%. Y esto asumiendo que el caché expira cada día. Con conversaciones más densas donde el TTL de 5 minutos se aprovecha bien, el ahorro sube al 85-90%.


    Preguntas frecuentes sobre prompt caching en Claude

    ¿El caché es compartido entre usuarios?
    No. El caché es privado por workspace de Anthropic. Desde febrero de 2026, hay aislamiento completo por workspace. Los datos de un usuario nunca se mezclan con los de otro.

    ¿Qué pasa si cambio el system prompt? ¿Se invalida el caché?
    Sí. El caché funciona por contenido exacto. Si modificas un solo carácter del bloque cacheado, se genera una nueva entrada de caché (cache write) en la siguiente llamada. El caché anterior expira según su TTL sin coste adicional.

    ¿Puedo cachear múltiples bloques en la misma llamada?
    Sí, hasta un máximo de cuatro breakpoints de caché por request. La restricción importante es el orden: los bloques con TTL más largo (1 hora) deben aparecer antes que los de TTL más corto (5 minutos) en la estructura del request.

    ¿El caching funciona con streaming?
    Sí. El prompt caching es compatible con la API de streaming de Claude. Los campos cache_creation_input_tokens y cache_read_input_tokens aparecen en el evento message_start del stream — no en message_delta. Es el primer evento emitido, antes de que lleguen los tokens de respuesta.


    El siguiente nivel: combinar con Claude Code

    Si ya estás explorando agentes más complejos, el prompt caching cambia la ecuación de coste de forma radical. Un agent loop sin caching que hace 10 iteraciones paga los tokens del system prompt y las tools diez veces. Con caching, los paga una vez y lee el resto.

    En Claude Code: Effort, Models, Tools y Context hay una sección completa sobre cómo gestiona Claude Code el contexto en agent loops largos — es el contexto perfecto para entender dónde encaja el caching a nivel de infraestructura.

    Y si quieres construir productos reales sobre la API de Anthropic con esta clase de optimizaciones ya integradas desde el primer sprint, el curso Construye con IA: De la Idea al Producto con Claude cubre el stack completo — desde la arquitectura del agente hasta el control de costes en producción.


    Lo que puedes hacer hoy

    Si tienes una aplicación que usa la API de Claude en producción, abre el código y busca dónde defines el system prompt. Si es una cadena de texto plana, conviértela en un array con cache_control: { type: "ephemeral" }.

    Eso solo. Una línea de cambio. Comprueba la siguiente factura.

    Si además tienes tools largas, aplica el mismo patrón al último elemento del array de herramientas. Tendrás dos puntos de caché activos y el ahorro será inmediato.

    El prompt caching no es una optimización avanzada que requiere rediseñar tu arquitectura. Es una configuración de tres minutos que debería estar activa en cualquier aplicación seria sobre la API de Claude. Si no la tienes, estás pagando de más desde el primer día.


    Bezael Pérez — Fundador de Dominicode. Developer senior con 15+ años construyendo software. Si construyes con IA y quieres profundizar más allá de los tutoriales, en Dominicode Labs estamos trabajando en proyectos reales con la API de Anthropic, arquitecturas de agentes y todo lo que no cabe en un post.

  • Claude Fable 5 vuelve: qué pasó y qué cambia para developers

    Claude Fable 5 vuelve: qué pasó y qué cambia para developers

    El 12 de junio de 2026, Anthropic apagó Claude Fable 5 de golpe.

    Sin aviso previo. Sin fecha de vuelta. Sin explicación técnica completa. El modelo que llevaba apenas tres días disponible desapareció para todos los usuarios del planeta — Europa, Latinoamérica, Asia, todos — porque el gobierno de EE.UU. no podía verificar nacionalidades en tiempo real y decidió cortar el acceso global en lugar de arriesgarse.

    Ese mismo día, developers de medio mundo abrieron Claude.ai y encontraron un modelo degradado. Los que habían empezado a construir pipelines con Fable 5 tuvieron que pivotar sobre la marcha. Y los que llevábamos años viendo cómo la IA maduraba como industria recibimos un recordatorio brutal: cuando un modelo tiene capacidades que un Estado considera amenaza para la seguridad nacional, el interruptor lo tiene el Estado, no Anthropic.

    Hoy, 1 de julio de 2026, Claude Fable 5 vuelve. Y la historia de cómo llegamos hasta aquí dice más sobre el futuro de la IA que cualquier benchmark.


    Lo que pasó: el jailbreak que lo cambió todo

    Investigadores de Amazon descubrieron una técnica que permitía a Fable 5 identificar vulnerabilidades en software y, en al menos un caso documentado, demostrar cómo explotarlas. El gobierno de EE.UU. reaccionó con rapidez: el mismo día 12 de junio, el Departamento de Comercio aplicó controles de exportación de emergencia que afectaron tanto a Fable 5 como a Mythos 5.

    La tensión entre ambas partes fue pública. El gobierno argumentó que el problema podría haberse corregido antes de la suspensión. Anthropic respondió que la técnica era más estrecha y específica de lo que la orden de emergencia implicaba — no una vulnerabilidad sistémica, sino un vector concreto que requerirían semanas de investigación para reproducir.

    El debate sobre la severidad real del jailbreak sigue abierto. El resultado fue inequívoco: controles de exportación de emergencia, suspensión global, y Anthropic sin poder verificar la nacionalidad de sus usuarios en tiempo real.

    No había otra salida. Apagaron todo.


    Fable 5 y Mythos 5: la diferencia que importa

    Aquí hay un matiz que mucha cobertura mediática perdió.

    Mythos 5 es la denominación interna de los modelos con capacidades cibernéticas más avanzadas que Anthropic ha construido jamás — superiores a cualquier otro modelo del mercado en ese dominio. Tras la suspensión, Anthropic decidió que Mythos 5 solo estará disponible para socios del Proyecto Glasswing, un programa de ciberseguridad defensiva con acceso controlado y supervisión directa.

    Fable 5 es diferente. Es el modelo de propósito general que se lanza hoy con los salvaguardas más fuertes que Anthropic ha implementado en ningún modelo de su historia. Anthropic afirma explícitamente que Fable 5 "no proporciona capacidades ofensivas únicas" — es decir, no hace nada que un atacante sofisticado no pudiera hacer con las herramientas que ya existen.

    Fable 5 Mythos 5
    Propósito General (razonamiento, código, escritura) Ciberseguridad avanzada
    Acceso Público (planes de pago) Solo Proyecto Glasswing
    API pública ✅ Sí ❌ No
    Capacidades ofensivas No únicas respecto a herramientas existentes Superiores a cualquier otro modelo del mercado

    La distinción es importante para cualquier developer que esté construyendo con la API. No estás usando Mythos 5. Estás usando Fable 5, que ha pasado por una revisión de seguridad que ningún modelo anterior había tenido.


    Qué cambió en Claude Fable 5: los nuevos salvaguardas

    Anthropic no volvió con el mismo modelo. Volvió con un clasificador de seguridad reentrenado específicamente para detectar y bloquear la técnica descrita en el reporte de Amazon.

    Según el anuncio oficial de Anthropic, el nuevo clasificador bloquea el comportamiento problemático en más del 99% de los casos. Cuando se activa, la solicitud no falla en silencio — se redirige automáticamente a Claude Opus 4.8. El usuario recibe respuesta, pero sin las capacidades que generaron el problema.

    El mecanismo de defensa tiene tres capas:

    1. El entrenamiento base del modelo, que ya rechaza asistencia con solicitudes peligrosas.
    2. Un clasificador específico para el patrón de jailbreak identificado por Amazon.
    3. Un margen de seguridad ampliado — Anthropic subió el umbral de bloqueo de forma deliberada, asumiendo más falsos positivos para reducir el riesgo de usos maliciosos.

    Ese tercer punto es el que más impacta a developers en producción. Más falsos positivos significa que algunas solicitudes legítimas relacionadas con ciberseguridad, análisis de código o auditoría de vulnerabilidades van a llegar a Opus 4.8 en lugar de Fable 5. No es un bug. Es una decisión consciente de arquitectura de seguridad.

    El Departamento de Comercio de EE.UU. verificó los salvaguardas y los calificó de "extraordinariamente fuertes". El 30 de junio levantó los controles de exportación. El 1 de julio, Fable 5 vuelve.


    Disponibilidad desde hoy: lo que necesitas saber

    La reactivación es global desde el 1 de julio en Claude.ai, Claude Platform, Claude Code y Claude Cowork.

    AWS, Google Cloud y Microsoft Foundry se reactivarán "lo antes posible" — sin fecha concreta confirmada.

    Hay un período de transición con límites temporales:

    • Hasta el 7 de julio: planes Pro, Max, Team y empresas seleccionadas tienen acceso a Fable 5 con hasta el 50% de sus límites de uso semanal habituales.
    • Después del 7 de julio: disponible mediante créditos de uso, sin restricción porcentual.

    Si estás en el plan gratuito, no hay cambios respecto a antes de la suspensión. Fable 5 era y sigue siendo acceso de pago.

    Para los que construimos con la API de Anthropic, el modelo vuelve a estar disponible desde hoy. Si tenías pipelines configurados con Fable 5 antes del 12 de junio, probablemente ya están activos de nuevo. Verifica tu dashboard y el comportamiento del clasificador con tus casos de uso específicos — especialmente si tienes prompts relacionados con análisis de código o seguridad.


    El nuevo marco de evaluación de jailbreaks

    Lo más interesante de lo que Anthropic publicó esta semana no son los salvaguardas. Es el marco que proponen como estándar industrial para evaluar la severidad de un jailbreak.

    Cuatro criterios:

    1. Ganancia de capacidad. ¿Cuánto supera lo que ya existe? Un jailbreak que replica lo que hace una herramienta de código abierto pesa menos que uno que desbloquea algo genuinamente nuevo.

    2. Amplitud. ¿Cuántas tareas ofensivas distintas habilita? Un jailbreak muy específico (un tipo de ataque, un vector) no es lo mismo que uno que abre la puerta a toda una clase de capacidades.

    3. Facilidad de armamento. ¿Cuánto esfuerzo humano experto requiere convertir el output en un ataque real? Hay una diferencia enorme entre "el modelo identifica una vulnerabilidad" y "el modelo produce un exploit listo para ejecutar".

    4. Descubribilidad. ¿Cómo de fácil es que un actor malicioso llegue a esta técnica? Un jailbreak que requiere semanas de ingeniería de prompts por parte de investigadores avanzados no tiene el mismo riesgo que uno que circula en un foro público.

    Este marco no es solo teoría. Anthropic lo propone como base para que gobiernos, empresas y laboratorios de IA puedan hablar de jailbreaks con criterios objetivos en lugar de reacciones políticas de emergencia.

    Si trabajas en seguridad o builds productos con IA, este marco te va a ser útil.


    Lo que esto significa para developers que construyen con IA

    Hace tres semanas, el modelo más capaz del mercado desapareció sin fecha de vuelta. Hoy está de vuelta con salvaguardas que ningún modelo anterior había tenido, respaldado por verificación gubernamental y un nuevo marco de evaluación que puede convertirse en estándar.

    ¿Qué cambia para nosotros?

    Primero, la confirmación de algo que debíamos asumir pero que muchos ignoraban: los modelos más capaces van a estar regulados. No es una posibilidad futura. Es el presente. El mismo día que Fable 5 volvió, Mythos 5 quedó restringido a socios controlados del Proyecto Glasswing. La IA de alto impacto va a tener fricción institucional. Cuanto antes lo integremos en nuestra planificación de producto, mejor.

    Segundo, la arquitectura de fallback importa más de lo que pensamos. Si tu producto dependía de Fable 5 el 12 de junio, tuviste un problema durante diecinueve días. Los mejores sistemas tienen fallback a modelos alternativos — no porque anticipen este escenario exacto, sino porque construyen con redundancia desde el principio.

    Tercero, y esto es lo más importante: la madurez del sector se mide en cómo responde a los errores, no en si los comete. Anthropic tardó diecinueve días en volver. En esas tres semanas entrenaron un nuevo clasificador, pasaron una auditoría gubernamental, propusieron un marco de evaluación de jailbreaks que puede convertirse en estándar, y redefinieron el acceso a Mythos 5. Eso no es una crisis mal gestionada. Es una empresa que aprendió en tiempo real bajo presión máxima.

    Nosotros podemos hacer lo mismo en nuestros productos.

    En el curso Construye con IA hablo de esto en profundidad: cómo construir sistemas que no colapsen cuando el modelo subyacente cambia, se actualiza o desaparece temporalmente. La resiliencia arquitectural no es un añadido. Es la condición de base para cualquier producto serio con IA.


    Preguntas frecuentes sobre Claude Fable 5

    ¿Claude Fable 5 está disponible hoy para todos los usuarios?

    Desde el 1 de julio de 2026, Fable 5 está disponible en Claude.ai, Claude Platform, Claude Code y Claude Cowork para usuarios en todos los países. AWS, Google Cloud y Microsoft Foundry se reactivarán próximamente. El acceso a Fable 5 requiere un plan de pago (Pro, Max, Team o Enterprise).

    ¿Qué es el jailbreak que causó la suspensión de Claude Fable 5?

    Investigadores de Amazon descubrieron una técnica de prompting que permitía a Fable 5 identificar vulnerabilidades en software y, en al menos un caso, demostrar cómo explotarlas. El gobierno de EE.UU. aplicó controles de exportación de emergencia el 12 de junio de 2026, lo que llevó a Anthropic a suspender el acceso global porque no podía verificar la nacionalidad de sus usuarios en tiempo real.

    ¿Cuál es la diferencia entre Claude Fable 5 y Mythos 5?

    Fable 5 es el modelo de propósito general disponible desde hoy para el público. Mythos 5 es la denominación de los modelos con capacidades cibernéticas avanzadas — superiores a cualquier otro modelo del mercado — restringido exclusivamente a socios del Proyecto Glasswing para ciberseguridad defensiva. No es accesible a través de la API pública.

    ¿Cómo afectan los nuevos salvaguardas al uso de Fable 5 en desarrollo de software?

    El nuevo clasificador bloquea el patrón de jailbreak en más del 99% de los casos, redirigiendo esas solicitudes a Claude Opus 4.8. Anthropic aumentó deliberadamente el margen de seguridad, lo que genera más falsos positivos en tareas de análisis de código, auditoría de seguridad o detección de vulnerabilidades. Si tu caso de uso incluye estas áreas, testea tu pipeline con Fable 5 para verificar el comportamiento del clasificador.

    ¿Qué límites de uso tiene Claude Fable 5 tras la vuelta?

    Hasta el 7 de julio de 2026, los planes Pro, Max, Team y empresas Enterprise seleccionadas tienen acceso a Fable 5 con hasta el 50% de sus límites de uso semanal habituales. Después del 7 de julio, el modelo estará disponible mediante créditos de uso sin restricción porcentual.

    ¿Puede volver a ocurrir una suspensión similar con otros modelos de Anthropic?

    Sí. Los controles de exportación son un instrumento legal que el gobierno de EE.UU. puede aplicar a cualquier modelo con capacidades que considere una amenaza. La colaboración reforzada entre Anthropic y el gobierno reduce la probabilidad de una suspensión de emergencia, pero no la elimina. Cualquier arquitectura de producto con IA debe contemplar escenarios de indisponibilidad del modelo principal.


    Si quieres estar al día de cómo estos eventos impactan a los developers que construyen con IA, en Dominicode Labs analizamos en tiempo real las decisiones de los grandes laboratorios y sus implicaciones para producción. Y en el canal de YouTube seguiré cubriendo la evolución de Fable 5 en las próximas semanas.


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

  • Claude Sonnet 5: el modelo que trabaja solo mientras tú duermes

    Claude Sonnet 5: el modelo que trabaja solo mientras tú duermes

    Me pasó hace unos meses revisando el output de un agente que había dejado corriendo toda la noche.

    Esperaba encontrar la tarea a medias. Un formulario sin completar. Alguna herramienta mal llamada. Lo habitual con los modelos de la generación anterior: empezaban bien, pero a mitad de camino se perdían, pedían confirmación o simplemente paraban.

    En cambio, encontré el trabajo terminado. Del principio al fin. Sin intervención.

    Ese momento cambia algo en tu cabeza como developer. No es que la IA sea "mejor". Es que ya no necesita que estés mirando.

    Claude Sonnet 5 es exactamente esa promesa hecha modelo. Anthropic lo lanzó el 30 de junio de 2026 y lo describe como "el modelo Sonnet más agéntico hasta la fecha". No es marketing vacío — la diferencia en tareas autónomas y multi-paso es medible y, para quien construye con IA, es relevante desde el primer día.


    Por qué Sonnet 5 es distinto a todo lo anterior

    Hasta ahora, la frontera estaba clara: si querías un agente que realmente terminase el trabajo, necesitabas Opus. Sonnet era el punto medio — rápido, accesible, suficientemente bueno para tareas simples. Pero en flujos complejos con múltiples pasos, herramientas y decisiones encadenadas, Sonnet se quedaba corto.

    Claude Sonnet 5 rompe esa frontera.

    Anthropic no ha simplemente subido los parámetros. Han optimizado específicamente para comportamiento agéntico: planificación de tareas, uso coordinado de herramientas (navegadores, terminales, APIs), y lo más relevante — la capacidad de verificar su propio resultado sin que se lo pidas.

    Eso último importa más de lo que parece. Un modelo que ejecuta código y luego comprueba si el output es el esperado, sin que tú se lo digas, está un paso más cerca de un colaborador que de una herramienta.


    Las capacidades agénticas en detalle

    Hay tres áreas donde el cambio es palpable:

    Tareas multi-paso sin interrupciones. Modelos anteriores tendían a pedir confirmación o detenerse cuando encontraban ambigüedad. Sonnet 5 mantiene el hilo. Algunos partners de Anthropic reportan que "terminó el trabajo de principio a fin sin intervención" — algo que antes era territorio exclusivo de Opus 4.

    Uso de herramientas coordinado. Puede combinar búsqueda web, ejecución de código y llamadas a APIs en la misma tarea sin perder el contexto de lo que estaba haciendo. No es nuevo que los modelos puedan usar herramientas — lo nuevo es que lo hacen con coherencia a lo largo de cadenas largas de razonamiento.

    Auto-verificación del resultado. Si ejecuta una query de base de datos o genera un archivo, puede evaluar si el resultado tiene sentido antes de dártelo. Esto reduce drásticamente la necesidad de loops de revisión en tus agentes.

    Si estás construyendo con la API de Claude o con Claude Code, estas tres capacidades cambian el diseño de tus flujos. No necesitas los mismos guardrails de antes. No necesitas los mismos puntos de control manual.

    En el curso de Construye con IA cubrimos exactamente este tipo de arquitectura de agentes — y con Sonnet 5 muchos de esos patrones se simplifican considerablemente.


    Benchmarks: qué dicen los números

    Los benchmarks importan, pero necesitan contexto. Aquí va la comparativa relevante para developers:

    Benchmark Claude Sonnet 5 Claude Sonnet 4.6 Claude Opus 4.8
    BrowseComp Superior Base de comparación Superior
    OSWorld-Verified Superior Base de comparación Superior
    Razonamiento general Muy cercano a Opus Inferior Referencia
    Codificación Notable mejora Base Referencia
    Esfuerzo "extra high" Iguala a Opus 4.8 Referencia

    Evaluaciones cualitativas basadas en el anuncio oficial de Anthropic (30 jun 2026).

    El dato más interesante: a nivel de esfuerzo máximo, Sonnet 5 iguala a Opus 4.8. Esto es arquitectónicamente significativo. Significa que para la mayoría de las tareas que antes justificaban pagar el precio de Opus, ahora puedes usar Sonnet 5 a un coste mucho menor.

    La excepción es ciberseguridad. Anthropic es explícito: Sonnet 5 no fue entrenado deliberadamente para tareas de seguridad ofensiva, y Opus 4.8 sigue siendo superior en ese dominio específico.


    Precios y disponibilidad

    Plan Precio hasta 31 ago 2026 Precio desde 1 sep 2026
    Input tokens $2 / M tokens $3 / M tokens
    Output tokens $10 / M tokens $15 / M tokens

    Anthropic ha aplicado un precio introductorio hasta finales de agosto. Si estás evaluando el switch en la API, este es el momento óptimo para hacerlo.

    Dónde está disponible:

    • Modelo predeterminado en los planes Free y Pro de Claude.ai
    • Disponible en Max, Team, Enterprise
    • Claude Code
    • API (model ID: claude-sonnet-5)

    Si usas Claude.ai directamente, ya lo tienes — es el modelo por defecto desde el lanzamiento.


    El tokenizador actualizado: impacto práctico

    Este punto se menciona poco y puede sorprenderte en producción.

    Sonnet 5 usa un tokenizador actualizado similar al que se introdujo con Opus 4.7. El resultado es que el mismo texto que antes ocupaba X tokens ahora puede ocupar entre 1.0× y 1.35× más tokens.

    ¿Qué significa esto en la práctica?

    Si tienes prompts largos con contexto extenso (documentos, conversaciones, sistemas de RAG), tu consumo de tokens aumentará. Anthropic compensa esto con el precio introductorio, pero necesitas tener este factor en cuenta al proyectar costes para producción.

    Una regla rápida: si venías de Sonnet 4.6 y tienes prompts de más de 5.000 tokens, haz una prueba controlada antes de cambiar el modelo en producción. Mide el consumo real, no lo estimes desde los benchmarks públicos.

    Para esto el post sobre prompt caching en Claude es directamente aplicable — con el nuevo tokenizador, el caching se vuelve aún más relevante para controlar costes.


    Cómo empezar hoy

    En Claude.ai: Ya está activo. Es el modelo por defecto. No necesitas hacer nada.

    En la API:

    El siguiente ejemplo muestra la integración mínima con el SDK oficial @anthropic-ai/sdk para TypeScript. El único cambio respecto a modelos anteriores es el model ID: claude-sonnet-5.

    import Anthropic from "@anthropic-ai/sdk";
    
    const client = new Anthropic();
    
    const response = await client.messages.create({
      model: "claude-sonnet-5",
      max_tokens: 4096,
      messages: [
        {
          role: "user",
          content: "Analiza este código y sugiere mejoras de rendimiento...",
        },
      ],
    });
    

    El cambio de model ID es inmediato. Si tienes un sistema en producción con claude-sonnet-4-6, cambiar a claude-sonnet-5 no requiere modificar nada más en la integración básica.

    Si usas Claude Code, el modelo ya está disponible y puedes seleccionarlo desde la configuración del cliente.


    Qué significa esto para developers que construyen con IA

    Voy a ser directo porque creo que es lo que necesitas saber.

    El lanzamiento de Sonnet 5 no es un update de rendimiento incremental. Es un reposicionamiento del tier medio.

    Hasta ahora, la decisión era: velocidad y coste (Haiku/Sonnet) vs. capacidad y razonamiento complejo (Opus). Con Sonnet 5, esa brecha se cierra de forma significativa. Puedes construir agentes que realmente terminen el trabajo, a un coste que tiene sentido para producción.

    Para quien está construyendo productos con IA — y no solo experimentando — esto cambia la ecuación de build vs. cost. Puedes subir la ambición de tus agentes sin subir proporcionalmente el presupuesto.

    El riesgo que veo es el de siempre: sobreestimar la autonomía del modelo en los primeros días. Sonnet 5 es notablemente mejor en tareas autónomas, pero sigue siendo un modelo de lenguaje. Sigue necesitando specs claras, herramientas bien definidas y tests que verifiquen los outputs.

    En Dominicode Labs estamos ya trabajando con Sonnet 5 en los proyectos de la comunidad — si quieres ver cómo se integra en flujos reales de producción, es donde está pasando.


    Preguntas frecuentes sobre Claude Sonnet 5

    ¿Qué diferencia hay entre Claude Sonnet 5 y Claude Sonnet 4.6?

    Claude Sonnet 5 está optimizado para comportamiento agéntico: puede planificar y ejecutar tareas multi-paso sin interrupciones, verificar sus propios resultados automáticamente y usar herramientas (navegadores, terminales, APIs) de forma coordinada en cadenas largas de razonamiento. Sonnet 4.6 era capaz, pero tendía a detenerse o pedir confirmación en tareas complejas. La mejora en benchmarks como BrowseComp y OSWorld-Verified refleja exactamente esta diferencia en tareas autónomas.

    ¿Es Claude Sonnet 5 tan bueno como Claude Opus 4.8?

    En la mayoría de tareas cotidianas de codificación, razonamiento y trabajo de conocimiento, Sonnet 5 está muy cerca de Opus 4.8 — y a nivel de esfuerzo máximo puede igualarlo. La excepción es el dominio de ciberseguridad, donde Opus 4.8 sigue siendo superior porque fue entrenado deliberadamente para esas tareas. Para el 90% de los casos de uso de desarrollo con IA, Sonnet 5 es suficientemente capaz y mucho más accesible en precio.

    ¿Cómo afecta el nuevo tokenizador a mis costes en la API?

    El tokenizador actualizado puede incrementar el consumo de tokens en un factor de 1.0× a 1.35× respecto a modelos anteriores. Esto es especialmente relevante con contextos largos: documentos, conversaciones extendidas, sistemas RAG. Anthropic compensa esto con el precio introductorio vigente hasta el 31 de agosto de 2026. La recomendación práctica es medir el consumo real con tus prompts de producción antes de estimar costes a escala.

    ¿Puedo usar Claude Sonnet 5 en Claude Code?

    Sí. Claude Sonnet 5 está disponible en Claude Code desde el lanzamiento. Dado que Claude Code es una herramienta diseñada precisamente para tareas agénticas de desarrollo — escribir, ejecutar, verificar código de forma autónoma — la combinación con Sonnet 5 es especialmente potente. Puedes seleccionar el modelo desde la configuración del cliente.

    ¿Qué precio tiene Claude Sonnet 5 y cuándo cambia?

    El precio introductorio vigente hasta el 31 de agosto de 2026 es $2/M tokens de input y $10/M tokens de output. A partir del 1 de septiembre de 2026 pasa a $3/M input y $15/M output. Si estás evaluando la migración en producción, hacerlo antes de septiembre tiene sentido económico.

    ¿Claude Sonnet 5 ya está disponible en el plan gratuito de Claude.ai?

    Sí. Desde el lanzamiento el 30 de junio de 2026, Claude Sonnet 5 es el modelo predeterminado en todos los planes de Claude.ai, incluyendo el gratuito. No necesitas hacer ningún cambio — si abres Claude.ai hoy, ya estás usando Sonnet 5.


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

  • Angular v22 + Vercel AI SDK: streaming de IA con Signals

    Angular v22 + Vercel AI SDK: streaming de IA con Signals

    ANTHROPIC_API_KEY=sk-ant-xxxxxxxx

    
    ---
    
    ## Paso 1: El servidor backend con streamText
    
    Crea un archivo `server/chat.ts` fuera del proyecto Angular (o en un monorepo aparte). Este servidor tiene un solo endpoint: recibe mensajes, llama a Claude, y hace streaming de la respuesta.
    
    ```typescript
    // server/chat.ts
    import { streamText } from 'ai';
    import { anthropic } from '@ai-sdk/anthropic';
    
    const server = Bun.serve({
      port: 3000,
      async fetch(req) {
        // CORS para desarrollo local
        if (req.method === 'OPTIONS') {
          return new Response(null, {
            headers: {
              'Access-Control-Allow-Origin': '*',
              'Access-Control-Allow-Methods': 'POST, OPTIONS',
              'Access-Control-Allow-Headers': 'Content-Type',
            },
          });
        }
    
        if (req.method === 'POST' && new URL(req.url).pathname === '/api/chat') {
          const { messages } = await req.json();
    
          const result = streamText({
            model: anthropic('claude-sonnet-4-6'),
            system: 'Eres un asistente técnico especializado en Angular y desarrollo frontend moderno. Responde en español de forma concisa y directa.',
            messages,
          });
    
          return result.toTextStreamResponse({
            headers: {
              'Access-Control-Allow-Origin': '*',
            },
          });
        }
    
        return new Response('Not found', { status: 404 });
      },
    });
    
    console.log(`Servidor corriendo en http://localhost:${server.port}`);
    

    Arrancar el servidor:

    bun run server/chat.ts
    

    streamText de la AI SDK devuelve un objeto con varios métodos. toTextStreamResponse() genera una Response HTTP estándar con Content-Type: text/plain; charset=utf-8 y Transfer-Encoding: chunked — exactamente lo que necesita el cliente para consumir el stream token a token.


    Paso 2: El modelo de datos

    Antes del componente, define la interfaz de mensaje. Simple:

    // src/app/chat/chat.types.ts
    export interface ChatMessage {
      role: 'user' | 'assistant';
      content: string;
    }
    

    Paso 3: El componente Angular v22 con Signals

    Aquí es donde la magia ocurre. No necesitas HttpClient con responseType: 'text' — eso no soporta streaming incremental. Necesitas fetch nativo con ReadableStream.

    // src/app/chat/chat.component.ts
    import {
      Component,
      signal,
      computed,
      ChangeDetectionStrategy,
    } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { ChatMessage } from './chat.types';
    
    @Component({
      selector: 'app-chat',
      standalone: true,
      imports: [FormsModule],
      changeDetection: ChangeDetectionStrategy.OnPush,
      templateUrl: './chat.component.html',
    })
    export class ChatComponent {
      messages = signal<ChatMessage[]>([]);
      userInput = signal('');
      isStreaming = signal(false);
    
      canSend = computed(
        () => this.userInput().trim().length > 0 && !this.isStreaming()
      );
    
      async sendMessage() {
        const content = this.userInput().trim();
        if (!content || this.isStreaming()) return;
    
        // Añadir mensaje del usuario
        this.messages.update((msgs) => [
          ...msgs,
          { role: 'user', content },
        ]);
        this.userInput.set('');
        this.isStreaming.set(true);
    
        // Capturar mensajes ANTES del placeholder — la API rechaza content vacío como último mensaje
        const messagesToSend = this.messages();
    
        // Placeholder para la respuesta del asistente
        this.messages.update((msgs) => [
          ...msgs,
          { role: 'assistant', content: '' },
        ]);
    
        try {
          const response = await fetch('http://localhost:3000/api/chat', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ messages: messagesToSend }),
          });
    
          if (!response.ok) throw new Error(`HTTP ${response.status}`);
          if (!response.body) throw new Error('No stream body');
    
          const reader = response.body.getReader();
          const decoder = new TextDecoder();
    
          while (true) {
            const { done, value } = await reader.read();
            if (done) break;
    
            const chunk = decoder.decode(value, { stream: true });
    
            // Actualiza el último mensaje (el del asistente) acumulando el chunk
            this.messages.update((msgs) => {
              const updated = [...msgs];
              const last = updated[updated.length - 1];
              updated[updated.length - 1] = {
                ...last,
                content: last.content + chunk,
              };
              return updated;
            });
          }
        } catch (error) {
          console.error('Error en streaming:', error);
          this.messages.update((msgs) => {
            const updated = [...msgs];
            updated[updated.length - 1] = {
              role: 'assistant',
              content: 'Error al conectar con el servidor. Comprueba que el backend está corriendo.',
            };
            return updated;
          });
        } finally {
          this.isStreaming.set(false);
        }
      }
    
      handleEnter(event: KeyboardEvent) {
        if (event.key === 'Enter' && !event.shiftKey) {
          event.preventDefault();
          this.sendMessage();
        }
      }
    }
    

    Tres decisiones clave en este componente:

    messages = signal<ChatMessage[]>([]) — todo el historial de la conversación vive en un signal. Cada vez que llega un chunk, actualizamos el último mensaje del array con update(). Angular detecta el cambio y re-renderiza solo ese elemento.

    ChangeDetectionStrategy.OnPush — esencial para este patrón. Sin esto, Angular ejecutaría la detección de cambios en cada tick mientras el stream está activo. Con OnPush + Signals, Angular solo actualiza cuando el signal cambia — que es exactamente cuando llega un chunk nuevo.

    fetch nativo en lugar de HttpClientHttpClient es poderoso para peticiones normales, pero para streaming necesitas acceso al ReadableStream crudo del Response. fetch te da eso directamente con response.body.getReader().


    Paso 4: El template con el nuevo control flow

    El template aprovecha el control flow de Angular v17+ (@for, @if) y lee los signals directamente — sin async pipe, sin | async, sin subscripciones.

    <!-- src/app/chat/chat.component.html -->
    <div class="chat-container">
      <div class="messages-area">
        @if (messages().length === 0) {
          <p class="empty-state">Escribe un mensaje para empezar.</p>
        }
    
        @for (msg of messages(); track $index) {
          <div class="message" [class]="msg.role">
            <span class="role-label">
              {{ msg.role === 'user' ? 'Tú' : 'Asistente' }}
            </span>
            <p class="message-content">{{ msg.content }}</p>
    
            @if (msg.role === 'assistant' && $last && isStreaming()) {
              <span class="cursor-blink">|</span>
            }
          </div>
        }
      </div>
    
      <div class="input-area">
        <textarea
          [value]="userInput()"
          (input)="userInput.set($any($event.target).value)"
          (keydown)="handleEnter($event)"
          placeholder="Escribe tu mensaje... (Enter para enviar)"
          rows="3"
          [disabled]="isStreaming()"
        ></textarea>
    
        <button
          (click)="sendMessage()"
          [disabled]="!canSend()"
        >
          @if (isStreaming()) {
            Generando...
          } @else {
            Enviar
          }
        </button>
      </div>
    </div>
    

    El cursor parpadeante | aparece solo en el último mensaje del asistente mientras isStreaming() es true. Es un detalle pequeño que hace que la experiencia se sienta viva.


    Paso 5: Estilos mínimos (opcional)

    /* src/app/chat/chat.component.css */
    .chat-container {
      display: flex;
      flex-direction: column;
      height: 100vh;
      max-width: 800px;
      margin: 0 auto;
      padding: 1rem;
      gap: 1rem;
    }
    
    .messages-area {
      flex: 1;
      overflow-y: auto;
      display: flex;
      flex-direction: column;
      gap: 1rem;
      padding: 1rem;
      border: 1px solid #e5e7eb;
      border-radius: 0.5rem;
    }
    
    .message {
      padding: 0.75rem 1rem;
      border-radius: 0.5rem;
      max-width: 80%;
    }
    
    .message.user {
      background: #e90464;
      color: white;
      align-self: flex-end;
    }
    
    .message.assistant {
      background: #f3f4f6;
      color: #111827;
      align-self: flex-start;
    }
    
    .role-label {
      font-size: 0.75rem;
      font-weight: 600;
      opacity: 0.7;
      display: block;
      margin-bottom: 0.25rem;
    }
    
    .cursor-blink {
      animation: blink 1s step-end infinite;
    }
    
    @keyframes blink {
      50% { opacity: 0; }
    }
    
    .input-area {
      display: flex;
      gap: 0.5rem;
    }
    
    textarea {
      flex: 1;
      padding: 0.75rem;
      border: 1px solid #d1d5db;
      border-radius: 0.5rem;
      resize: none;
      font-family: inherit;
    }
    
    button {
      padding: 0.75rem 1.5rem;
      background: #e90464;
      color: white;
      border: none;
      border-radius: 0.5rem;
      cursor: pointer;
      font-weight: 600;
      align-self: flex-end;
    }
    
    button:disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }
    

    El resultado

    Arranca los dos procesos:

    # Terminal 1: backend
    bun run server/chat.ts
    
    # Terminal 2: Angular
    ng serve
    

    Abre http://localhost:4200. Escribe cualquier pregunta técnica. Las palabras aparecen token a token mientras Claude las genera. El botón muestra "Generando…" y el cursor parpadea al final del último mensaje.

    Eso es streaming real, en Angular v22, con Signals, en menos de 20 minutos.


    Por qué este patrón funciona bien en producción

    Si quieres entender cómo se conectan estos patrones con el desarrollo de productos completos con IA, el post sobre cómo crear productos con IA para vender muestra el panorama completo.

    Lo que tienes aquí no es un prototipo. Es un patrón que escala:

    El estado es predecible. Todo vive en messages = signal<ChatMessage[]>([]). No hay subscripciones dispersas, no hay Subject de BehaviorSubject, no hay que recordar hacer unsubscribe. El signal se actualiza, Angular re-renderiza lo necesario, punto.

    El backend es stateless. Cada petición envía el historial completo de mensajes. Así funciona la API de Anthropic — no hay sesión en el servidor, lo que facilita el escalado horizontal.

    ChangeDetectionStrategy.OnPush es obligatorio aquí. Con Zone.js y la detección de cambios por defecto, Angular correría su ciclo de detección constantemente mientras el stream está activo. Con OnPush + Signals, solo actualiza cuando el signal cambia.

    Si quieres llevar esto más allá — añadir herramientas (tool calls), mantener sesiones con localStorage, o integrar el chat dentro de una app Angular más grande con routing y autenticación — el patrón es el mismo. Cambias el modelo en el servidor, añades tools a streamText, y el componente no necesita modificarse.

    Si ya tienes experiencia con Angular y quieres dominar Signals, componentes standalone y el control flow moderno que hemos usado aquí, en el Curso Angular Moderno lo cubrimos desde la arquitectura hasta producción — incluyendo patrones de integración con APIs externas como esta.

    Y si quieres ir más allá del chat básico y construir agentes reales con Claude — con herramientas, contexto persistente, y pipelines de desarrollo AI-first — eso es exactamente lo que enseñamos en Construye con IA: de la idea al producto con Claude Code.


    FAQ

    ¿Necesito Angular Universal (SSR) para que esto funcione?

    No. El streaming ocurre entre el cliente Angular (browser) y el servidor Bun que creamos. Angular SSR es irrelevante para este patrón — el componente de chat vive completamente en el cliente. Si tienes SSR activado, asegúrate de que el componente de chat solo se renderiza en el browser con isPlatformBrowser o usando @defer.

    ¿Puedo usar el mismo enfoque con OpenAI o Google Gemini en lugar de Anthropic?

    Sí. Cambia @ai-sdk/anthropic por @ai-sdk/openai o @ai-sdk/google, y sustituye anthropic('claude-sonnet-4-6') por openai('gpt-4o') o google('gemini-2.5-pro'). El resto del código — el componente Angular, el consumo del stream, los Signals — no cambia. Esa es una de las ventajas del Vercel AI SDK: abstrae el proveedor.

    ¿Qué pasa si el usuario envía el siguiente mensaje mientras el anterior aún está en streaming?

    El botón está deshabilitado mientras isStreaming() es true gracias al computed canSend. El usuario no puede enviar otro mensaje hasta que el stream termine. Si quieres cancelar el stream activo al recibir un nuevo mensaje, puedes guardar el reader como propiedad del componente y llamar a reader.cancel() antes de iniciar la nueva petición.

    ¿Cómo manejo el historial para conversaciones largas?

    La API de Anthropic tiene un límite de tokens por request. Para conversaciones largas, lo más simple es limitar el historial que envías al servidor — por ejemplo, los últimos 20 mensajes. En producción, lo correcto es implementar una ventana deslizante o resumir el historial antiguo con un llamada previa al modelo. Por ahora, con this.messages().slice(-20) en el body del fetch tienes un control básico suficiente para empezar.

    ¿Puedo usar HttpClient en lugar de fetch nativo?

    HttpClient con responseType: 'text' recibe el texto completo cuando la conexión cierra — no es streaming incremental. Para streaming real necesitas acceso al ReadableStream crudo de la Response, que solo fetch te proporciona directamente. Podrías implementar un interceptor custom o un HttpBackend alternativo, pero la complejidad no vale la pena. fetch nativo es la solución correcta aquí.


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