Model routing: un router de 300ms que decide si gastas $0.001 o $0.05

Por qué Opus para clasificar mensajes es absurdo, y cómo armé un router con Haiku 4.5 que decide en menos de 300ms qué modelo merece cada tarea.


Anthropic vende 3 modelos: Haiku, Sonnet, Opus. Los precios:

ModeloInputOutputVelocidad
Haiku 4.5$0.80/M$4/Mrápido
Sonnet 4.6$3/M$15/Mmedio
Opus 4.7$15/M$75/Mlento

Opus es ~19× más caro que Haiku. Sonnet es ~4× más caro que Haiku.

La pregunta que casi nadie se hace: ¿cada tarea de mi app necesita Opus?

Spoiler: no. La mayoría son tareas baratas que Haiku resuelve perfecto. Pero la gente usa Opus porque “es el mejor” y luego se queja del costo.

El router

Mi sistema de 11 agentes tiene un router que clasifica cada mensaje y elige el modelo:

TASK_MODELS = {
    "routing":  "claude-haiku-4-5",   # el router en sí
    "simple":   "claude-haiku-4-5",   # FAQ, clasificar, summarize
    "standard": "claude-sonnet-4-6",  # code review, drafts
    "complex":  "claude-opus-4-7",    # estrategia, decisiones
}

Cada agente declara su complejidad:

class GeneralAgent(Agent):
    complexity = "simple"   # → Haiku

class CreativeAgent(Agent):
    complexity = "standard" # → Sonnet

class OptimizerAgent(Agent):
    complexity = "complex"  # → Opus

El router decide a qué agente mandar el mensaje. Y el agente decide su propio modelo.

El router: cómo funciona

Es un Haiku con un system prompt corto y structured output forzado:

SYSTEM = """Eres un router de un equipo de agentes IA.
Dado un mensaje, decide qué agente debe atenderlo.
Responde SOLO con JSON válido: {"agent": "<nombre>", "reason": "<una frase>"}

Agentes:
- "coordinator": tareas multi-paso (busca leads Y envía emails)
- "dev": código, debugging, arquitectura
- "ops": estado del servidor, deploys, métricas
- "sales": buscar leads, CRM
- "finance": invoices Stripe, cobrar, pagos
- "email": leer/resumir emails, drafts
- "creative": copy LinkedIn/Twitter, posts
- "marketing": cotizar, propuestas abstractas
- "security": revisar configs, hardening
- "general": saludos, charla, lo demás
"""

Con structured outputs y JSON schema:

resp = await client.messages.create(
    model="claude-haiku-4-5",
    max_tokens=200,
    system=[{
        "type": "text",
        "text": SYSTEM,
        "cache_control": {"type": "ephemeral", "ttl": "1h"},
    }],
    messages=[{"role": "user", "content": user_message}],
    output_config={
        "format": {"type": "json_schema", "schema": ROUTER_SCHEMA},
    },
)

Latencia típica: 150-300ms. Costo por decisión: <$0.0001 USD.

Sin structured outputs, antes hacía un try/except json.JSONDecodeError con regex de markdown-stripping. Ya no — el output siempre es JSON válido por schema.

Por qué Haiku para el router (y no Sonnet)

La intuición: “el router es la decisión más importante, dale Sonnet para que no se equivoque”.

La realidad: el router tiene una tarea trivial. Clasificar un mensaje en 10 buckets predefinidos no requiere razonamiento de alto nivel. Haiku lo hace perfecto el 99% del tiempo.

Y cuando se equivoca, el cliente (yo) lo redirige con un mensaje explícito (@finance crea un invoice...). Costo del error: 1 mensaje más. Costo de usar Sonnet en cada llamada: 4× más caro × millones de llamadas.

Optimiza el caso común. Acepta el peor caso poco frecuente.

Mi distribución real

Después de 3 meses corriendo:

Routing decisions:  ~140/día (todas Haiku)
Simple (Haiku):     ~80% del tráfico
Standard (Sonnet):  ~15%
Complex (Opus):     ~5%

Costo promedio por llamada: $0.001 USD. Con prompt caching encima, $0.0001.

Si usara Opus para todo: 20-50× más caro, latencia 3-5× más lenta, sin ganancia perceptible para tareas simples.

Cuándo usar el modelo grande

Mi regla:

La heurística: ¿qué tan grave es si el modelo se equivoca? Si “renombrar variable” se equivoca → fix de 5 segundos. Si “decidir mi estrategia de pricing del próximo trimestre” se equivoca → impacto de meses. Paga Opus solo donde el costo del error importa.

El error que NO debes cometer

Vi gente hacer routing con modelos grandes en producción y luego subir a HackerNews quejándose de la factura. El patrón:

# ❌ MAL
response = await client.messages.create(
    model="claude-opus-4-7",  # usar Opus para clasificar
    system="Decide si este mensaje es bug, feature o pregunta.",
    messages=[{"role": "user", "content": msg}],
)

Eso es como pagar consultor senior $500/hora para hacer triage de tickets. Función válida, modelo absolutamente incorrecto.

Combina con prompt caching

El router corre cada mensaje. Su system prompt (~600 tokens) se cachea. 99% de los routes son cache reads → casi gratis.

Más detalles en Bajé mi factura de Claude 90% con prompt caching.

La conclusión

Si tu app de Claude cuesta más de $20/mes y no estás haciendo nada heavy-compute, casi seguro tienes:

  1. Modelo too-big para el 80% de las llamadas
  2. Sin prompt caching activo
  3. Sin estructura de “complejidad declarada” por endpoint/feature

Implementar el router (~50 líneas de código) y declarar complejidades te baja la factura entre 5× y 20×. Sin sacrificar calidad porque la calidad importa solo donde la tarea lo justifica.


Si quieres ver el router en acción, abre oficina.lalu.dev — el modelo mix de la derecha muestra qué porcentaje de llamadas va a cada modelo. En tiempo real.


← todos los posts