· 18 min de lectura

La esfera de Claude, apagada y con la batería vacía, manda el trabajo a un panel de veredictos. De ahí salen líneas hacia siete modelos: OpenAI en verde con una marca de aprobado y una etiqueta de 0,005 $; Kimi, Grok, Qwen y Z.ai en naranja con un signo de aviso; DeepSeek y MiniMax en rojo con una equis.

Si tienes prisa: cuando se me acaba la cuota de Claude, uso GPT-6 Luna por defecto (medio centavo por tarea), GPT-6.1 Sol para lo delicado y espero a Claude para los incidentes. Pero lo que vale la pena leer es por qué, y eso lo cuenta una sola tarea: arreglar un error de fechas. Diez de trece modelos la entregaron mal, y seis de ellos devolvían datos erróneos sin dar ningún error.

La cuota se acaba el jueves

Uso Claude Code a diario, y con un uso intensivo la cuota de la suscripción se acaba antes que la semana. Cuando pasa, quedan dos opciones: esperar a que se reinicie o seguir con otro modelo de pago por uso.

Le pregunté a otra IA qué modelo usar y la respuesta fue fácil: los modelos «Flash» más baratos, con una tabla de precios por millón de tokens. Suena razonable, pero ya había pasado por eso.

Hace unos meses probé MiniMax para lo mismo y no estuvo a la altura. Nada explotó: el problema era que el trabajo parecía terminado y no lo estaba. Ese es el fallo caro, porque te enteras tarde.

Así que esta vez lo medí antes de pagar. Trece modelos, ocho tareas sacadas de mi trabajo, tests que el modelo no veía y menos de diez dólares en total. El resultado cambió lo que tengo configurado, y no por la razón que esperaba.

Qué buscaba de verdad

Para un uso intensivo, una suscripción como las de Claude sale mucho más barata que pagar la API token a token, y ningún pago por uso se le acerca. Así que la pregunta no es «¿con qué sustituyo la suscripción?», sino «¿con qué cubro los días sin cuota, sin bajar la calidad?».

Y un consejo antes de cambiar de modelo: un agente de código relee el contexto entero de la sesión en cada turno, así que una sesión larga gasta la cuota mucho más rápido que varias cortas. La forma más barata de estirarla no es cambiar de modelo, es usar /clear entre tareas.

Cómo lo medí

Usé opencode, un agente de código de terminal parecido a Claude Code, pero de código abierto y que funciona con cualquier proveedor. Así los trece modelos alternativos trabajaban con las mismas herramientas y el mismo bucle; lo único que cambiaba era el modelo. Con Claude hice algo distinto, y lo explico más abajo. Los modelos venían de dos proveedores, OpenRouter y DeepInfra.

Las ocho tareas salen de lo que hago de verdad, con datos sintéticos:

TareaQué pide
t1Script de PowerShell que exporta los permisos de los buzones compartidos de Microsoft 365, con pruebas en Pester
t2Analizar un export del Unified Audit Log para encontrar reglas de buzón sospechosas (reenvíos externos, carpetas ocultas, palabras de pagos), típico de un BEC
t3Shortcode de newsletter para un plugin de WordPress, con nonce y escapado
t4Endpoint de Flask para relanzar un escaneo, siguiendo las convenciones del proyecto
t5Arreglar un error a partir de un traceback
t6Componente de Remotion en TypeScript estricto (un rótulo animado para vídeo)
t7Un escaneo masivo que a veces se cuelga para siempre, sin error ni log
t8Refactorizar un servicio para separar el motor de la API sin cambiar su comportamiento

Las reglas del banco de pruebas:

Claude no corrió en opencode, y hay que decirlo. La referencia es Claude Opus 5.5 en Claude Code, con suscripción: cada tarea la hizo un subagente aislado, con el mismo enunciado, la misma copia del repositorio y los mismos tests ocultos, pero con las herramientas de Claude Code, no las de opencode. Para tener una comparación en igualdad de condiciones, pasé además Claude Opus 5.5 por la API dentro de opencode en cuatro tareas (t1, t4, t5 y t7): aprobó las cuatro, la t5 incluida, a unos 0,19 dólares por tarea.

Y el revisor a ciegas también era Claude. Un subagente de Claude Code con Opus 5.5, el mismo modelo que la referencia. No sabía de quién era cada diff, pero un modelo puede preferir el estilo que él mismo escribiría. Por eso las notas de la revisión las uso solo como apoyo; la conclusión sale de los tests.

Cada intento se lanzaba así:

opencode run --dir ./trabajo -m openrouter/openai/gpt-6-luna --auto --format json "$(cat prompt.md)"

--auto aprueba todo lo que no esté prohibido de forma explícita. Como no me gusta dejar a un modelo ejecutar comandos sin mirar, cada copia llevaba un opencode.json con lo que no podía hacer:

{
  "permission": {
    "edit": "allow",
    "webfetch": "deny",
    "external_directory": "deny",
    "bash": {
      "*": "allow",
      "git push*": "deny",
      "curl *": "deny",
      "Invoke-WebRequest*": "deny",
      "ssh *": "deny",
      "Remove-Item C:\\*": "deny"
    }
  }
}

Una advertencia, que este es un blog de seguridad: esto es un cinturón, no una jaula. Las reglas comparan texto, así que un python -c con urllib se salta el curl * sin problema. Para código que no conoces, usa una máquina virtual o un contenedor sin red.

Resultados

Así quedaron las ocho tareas, con Claude como referencia. El costo es lo que pagué por tarea de media, y la última columna es la tarea t5, que explico en la siguiente sección:

ModeloTests$/tareaMedianat5
Claude Opus 5.5 (Claude Code, referencia)8/8cuota–✅
Claude Opus 5.5 (API en opencode, control)4/40,1990 s✅
GPT-6 Luna8/8 y 7/8 (dos pasadas)0,005103 s✅ las dos veces
GPT-6.1 Sol8/80,077121 s✅
GPT-6 Sol8/80,082187 s✅
Qwen3.8 Max7/80,099282 s⚠️ detalle
Kimi K37/80,195283 s⚠️ detalle
Grok Build7/80,070258 s⚠️ detalle
DeepSeek V4 Pro7/80,044221 s❌ fechas mal
DeepSeek V4.1 Flash7/8 y 7/8 (dos pasadas)0,00463–82 s❌ fechas mal, las dos veces
Jev Router (TypeSafe en OpenRouter)7/8sin registro99 s❌ fechas mal
GLM-5.36/80,09146 s⚠️ detalle
GLM-5.3 Flash6/80,004341 s❌ fechas mal
MiniMax M2.75/80,055120 s❌ fechas mal
MiniMax M33/80,019741 s❌ fechas mal
Gráfica de puntos con 12 modelos ordenados por costo facturado por tarea, en escala logarítmica. Los dos más baratos, DeepSeek V4.1 Flash y GLM-5.3 Flash (0,004 $), devuelven fechas mal. GPT-6 Luna, a 0,005 $, cumple el contrato. Después van MiniMax M3, DeepSeek V4 Pro y MiniMax M2.7, con fechas mal; Grok Build con el mensaje de error mal; GPT-6.1 Sol y GPT-6 Sol, que cumplen, en torno a 0,08 $; y GLM-5.3, Qwen3.8 Max y Kimi K3, el más caro a 0,20 $, con el mensaje de error mal.

Mirando solo la columna de tests parece que casi todos aprueban: 7 de 8 por aquí, 7 de 8 por allá. El 7/8 de casi todos ellos lo explica la misma tarea, la t5.

La excepción es el 7/8 de Luna en su segunda pasada, y como es mi modelo por defecto, lo explico. Falló la t6, el rótulo animado: el enunciado pedía que la opacidad bajara de 1 a 0 durante los últimos 15 fotogramas, y Luna hizo que llegara a 0 ya en el último fotograma visible, así que el rótulo desaparece un fotograma antes de lo que esperaba el test. En un vídeo a 30 fotogramas por segundo es una trigésima de segundo, y el enunciado admite las dos lecturas. Pero el test lo marca, y lo cuento como fallo.

La tarea que lo decidió todo: una fecha

La t5 es corta. Un script normaliza las brechas de datos que devuelven distintos proveedores de inteligencia de amenazas, y al importar los de un proveedor nuevo salta este error:

breaches.py:33: in parse_leak_date
    return date.fromisoformat(s)
E   ValueError: Invalid isoformat string: '2024-03'

El enunciado era el traceback y una frase: «La función tiene que cumplir lo que dice su docstring». Y el docstring dice esto:

def parse_leak_date(value: str | None) -> date | None:
    """Convierte la fecha de una brecha a `date`.

    Formatos que envían los proveedores:
      - ISO completo:      "2024-03-15"            -> 2024-03-15
      - Solo año-mes:      "2024-03"               -> 2024-03-01
      - Europeo:           "15/03/2024"            -> 2024-03-15   (día/mes/año)
      - Mes en inglés:     "March 2024"            -> 2024-03-01
      ...
    Cualquier otro valor lanza ValueError con el valor original en el mensaje.
    """

El error del traceback es fácil: la rama que trata las fechas con guion se come 2024-03 antes de que llegue a la rama de año-mes. Pero hay un segundo error que el traceback no enseña. Lo ve quien lee el contrato y lo compara con el código:

    if "/" in s:
        month, day, year = (int(p) for p in s.split("/"))   # mes/día: formato de EE. UU.
        return date(year, month, day)

El docstring dice día/mes/año y el código lee mes/día/año. Ningún test existente lo cubre.

Los modelos se repartieron en tres grupos.

Los que solo arreglaron el síntoma. DeepSeek V4 Pro, DeepSeek V4.1 Flash, GLM-5.3 Flash, el Jev Router y los dos MiniMax hicieron prácticamente lo mismo: mover dos líneas.

     if "T" in s:
         return datetime.fromisoformat(s.replace("Z", "+00:00")).date()

+    if len(s) == 7 and s[4] == "-":
+        return date(int(s[:4]), int(s[5:]), 1)
+
     if "-" in s:
         return date.fromisoformat(s)

El traceback desaparece y los tests del repositorio pasan. Es un arreglo que cualquiera aprobaría en una revisión rápida. Pero 05/03/2024, el 5 de marzo, ahora sale como 3 de mayo, sin ningún error. Con 15/03/2024 al menos explota, porque no hay mes 15. Con cualquier día del 1 al 12 obtienes una fecha válida y equivocada.

Piensa dónde acaba eso: un informe de brechas con las fechas cambiadas, una línea de tiempo de un incidente desordenada, un filtro de «últimos 30 días» que deja fuera lo que no debería. Nadie se entera hasta que alguien compara con el original.

DeepSeek V4.1 Flash lo repitió igual en la segunda pasada. No fue mala suerte: es como se comporta ante este tipo de tarea.

Los que arreglaron las fechas pero no el mensaje. Qwen3.8 Max, Kimi K3, Grok Build y GLM-5.3 leyeron el docstring y arreglaron el formato europeo. Fallaron en la última línea: ante un valor inválido como 13/13/2024, el error decía month must be in 1..12, not 13 en lugar de incluir el valor original. Es un detalle, y se lo marco como detalle. Pero también estaba escrito en el contrato.

Los que cumplieron el contrato entero. Claude, GPT-6 Sol, GPT-6.1 Sol y GPT-6 Luna. Luna, además, en las dos pasadas. Su arreglo comprueba cada formato con una expresión regular exacta en vez de buscar un carácter suelto, lee día/mes/año y convierte cualquier fallo en el ValueError con el valor original.

Tres columnas para parse_leak_date("05/03/2024"), que significa 5 de marzo de 2024. Solo el síntoma: devuelve 2024-05-03 sin error, seis modelos. Fechas bien, mensaje no: devuelve 2024-03-05 pero el error de 13/13/2024 no cita el valor, cuatro modelos. Contrato entero: 2024-03-05 y el error cita el valor original, tres modelos más Claude.

Lo que más me sorprendió es que el precio no predijo nada. Kimi K3 fue el más caro de la prueba, a 0,20 dólares por tarea, y se dejó el detalle del mensaje. GPT-6 Luna costó 0,005 dólares, casi cuarenta veces menos, y cumplió todo. La tabla de precios que me dio la otra IA ordenaba los modelos por lo único que no importaba.

Lo que los tests no ven

Los tests ocultos atrapan mucho, pero no todo. Por eso hubo revisión a ciegas.

La condición de carrera de t4. El endpoint tenía que devolver 409 si ya había un escaneo en curso. Casi todos lo hicieron así: leer el estado, comprobarlo y escribirlo. Si llegan dos peticiones a la vez, las dos leen «libre» y se lanzan dos escaneos. Ni el enunciado ni los tests lo mencionaban. Solo Claude y los dos GPT Sol protegieron la comprobación y el cambio de estado con un candado, para que no pudieran ocurrir a la vez. GPT-6.1 Sol añadió incluso un test que lanza dos peticiones en paralelo. Luna, no. Es la razón por la que no le dejo a Luna nada con concurrencia.

Más no es mejor. En t7, el escaneo que se colgaba, los seis de la revisión a ciegas encontraron la causa: el hilo solo capturaba los errores de DNS, cualquier otro error lo mataba y la cola esperaba para siempre. Claude lo arregló convirtiendo un módulo de 43 líneas en uno de 128, con su propio bucle de sondeo y tiempos de espera por dominio. El revisor ciego lo llamó «desproporcionado» y le dio un 1 sobre 5 en fidelidad al encargo. GPT-6 Sol lo resolvió con un cambio de unas 40 líneas, tests incluidos, y sacó la nota máxima. Luna, que llegó después de la revisión a ciegas, también dio con la solución completa en su primera pasada; esto lo comprobé yo leyendo su diff: capturar cualquier excepción, garantizar con un finally que cada dominio acaba en resultados o en errores, y validar el número de hilos.

Las notas globales de la revisión a ciegas de la primera ronda, sobre 5:

Modelot4t7t8
GPT-6 Sol554
Qwen3.8 Max345
Kimi K3344
Grok Build333
DeepSeek V4 Pro223
Claude Opus 5.52*2*3*

*Las tres soluciones de Claude borraban el opencode.json y añadían una copia de los tests ocultos. Ninguna de las dos cosas era del modelo: Claude corrió en Claude Code, donde ese archivo no se usa, y la copia de los tests la dejó mi proceso de corrección. El revisor no podía saberlo y las penalizó, con razón: tal cual, no se podían fusionar. Mirando solo la corrección del código, le dio 5, 4 y 5.

Rarezas que conviene saber

GLM-5.3 pensó 32 000 tokens y no escribió nada. En la primera tarea razonó hasta el límite de salida, no creó ningún archivo y costó 0,08 dólares. Lo repetí con otra configuración de razonamiento y pasó exactamente lo mismo. Si un modelo puede gastarse el presupuesto entero pensando, conviene saberlo antes de dejarlo solo.

MiniMax quedó último, y eso valida la prueba. MiniMax M3 aprobó 3 de 8 y agotó los 20 minutos en varias tareas; M2.7 aprobó 5 de 8. Es justo lo que viví usándolo. Si mi banco de pruebas hubiera puntuado bien al modelo que en la práctica me falló, el que estaría mal sería el banco. Que lo reprodujera me dio confianza en el resto de resultados.

El enrutador no dice qué hizo. El Jev Router, el enrutador de TypeSafe en OpenRouter, elige un modelo para cada petición. Falló la t5 como los modelos baratos, y en el registro de opencode no aparece qué modelo eligió ni cuánto costó: marca 0 dólares en las ocho tareas. Así no sabes quién hizo cada cosa ni lo que pagas por tarea. Enrutar me sigue pareciendo buena idea, pero con mis criterios y entre modelos que ya medí, no a ciegas.

Lo que uso ahora

Una escalera de tres escalones:

EscalónModeloPara qué$/tarea
1 (por defecto)GPT-6 LunaCasi todo: scripts, componentes, plugins, refactors con tests~0,005
2 (preciso)GPT-6.1 Sol, con GPT-6 Sol de reservaBugs, lógica de negocio, parseo de datos, concurrencia, código sin tests~0,08
3Esperar a ClaudeIncidentes en tenants, producción, arquitectura, seguridadcuota

Para saber en qué escalón va una tarea me hago una sola pregunta: ¿puedo comprobar objetivamente que quedó bien? Si hay tests o una verificación clara, escalón 1. Si no, escalón 2 como mínimo. Y para un incidente real no me fío de dos tareas complejas aprobadas: ahí espero a Claude o lo pago por API.

En opencode lo tengo como dos agentes, y se cambia de uno a otro con Tab. Este es el archivo, en ~/.config/opencode/opencode.jsonc, sin claves: las lee de la variable de entorno OPENROUTER_API_KEY.

{
  "$schema": "https://opencode.ai/config.json",
  "enabled_providers": ["openrouter"],
  "provider": {
    "openrouter": {
      "whitelist": ["openai/gpt-6-luna", "openai/gpt-6.1-sol", "openai/gpt-6-sol"],
      "models": {
        // GPT-6.1 Sol es muy nuevo y puede no estar aún en el catálogo de opencode
        "openai/gpt-6.1-sol": {
          "name": "GPT-6.1 Sol",
          "tool_call": true,
          "reasoning": true,
          "cost": { "input": 2, "output": 10, "cache_read": 0.1 },
          "limit": { "context": 1050000, "output": 128000 }
        }
      }
    }
  },
  "model": "openrouter/openai/gpt-6-luna",
  "small_model": "openrouter/openai/gpt-6-luna",
  "agent": {
    "build":   { "model": "openrouter/openai/gpt-6-luna" },
    "plan":    { "model": "openrouter/openai/gpt-6.1-sol" },
    "preciso": {
      "mode": "primary",
      "model": "openrouter/openai/gpt-6.1-sol",
      "description": "Bugs, lógica de negocio, parseo de datos, concurrencia y código sin tests."
    }
  }
}

Si usas OpenRouter, ponle un límite de gasto a la clave. Es una casilla en el panel, y un agente en modo automático con un bucle mal parado puede gastar mucho.

Dos detalles de precio

Para el uso diario, una suscripción sigue siendo lo más barato. Lo que cambia es que cubrir los días sin cuota con Luna cuesta centavos por tarea. Dos detalles de la página de precios de OpenAI que conviene conocer antes de usar estos modelos en sesiones largas:

Haz tu propia prueba en una tarde

Mis resultados sirven para mis tareas. Si tu trabajo es otro, la prueba se monta rápido:

  1. Elige 5 a 8 tareas reales de las que haces cada semana, con datos inventados. Que cubran tus lenguajes y al menos una sea difícil.
  2. Escribe los tests antes y escóndelos. Valídalos con tu propia solución. El modelo no los ve hasta que termina.
  3. Incluye una tarea de contrato: un error con traceback en el que el arreglo obvio deja otro fallo que solo se ve leyendo la documentación. Es la que más separa a unos modelos de otros, y te dice si el modelo lee o solo reacciona.
  4. Una copia limpia y un modo automático por intento, con límite de tiempo y comandos peligrosos prohibidos.
  5. Repite al menos una vez con los modelos que te interesan. Una sola pasada no distingue la suerte de la costumbre.
  6. Compara el costo con la factura real, no con lo que dice la herramienta. A mí opencode me calculó de más en cuatro modelos, en GLM-5.3 Flash el doble de lo facturado, porque usaba un precio de catálogo y no el del proveedor que de verdad atendía.
  7. Incluye un modelo que ya conoces de la práctica. Si tu prueba no reproduce lo que viviste con él, la prueba está mal.

Lo que no medí

Fuentes

Seguir leyendo en IT Rafa

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *