Cuando un clasificador cambia tu modelo sin avisar: por qué hacen falta cabeceras con el modelo servido
Un fallback silencioso a un modelo más barato no deja ni error ni línea de log. Cómo leer los datos del modelo servido en las transcripciones de Claude Code y por qué los proveedores deberían exponerlos en las respuestas.
Cuando Anthropic publicó el 7 de agosto que relajaba el clasificador de seguridad biológica de su modelo insignia, la mayoría de los comentarios lo archivaron en el viejo debate sobre el exceso de rechazos, un tema que parecía agotado. Pero la palabra más interesante del anuncio es fallback, no rechazo.
Una sustitución sin señal
Según el anuncio, un clasificador que se activa no detiene la petición. La redirige a Opus 5, un modelo potente que carece de la capacidad biológica de Fable 5. No se bloquea ni se filtra nada, y no aparece ningún aviso. Alguien pide el modelo top, un clasificador decide que el prompt se parece a biología y responde otro modelo.
Los modelos no cuestan lo mismo. En la lista de precios pública, Fable 5 cobra $10 por millón de tokens de entrada y $50 por millón de salida, mientras que Opus 5 cobra $5 y $25, la mitad en ambos sentidos. El post no dice cómo se factura una petición redirigida y eso no se ha probado, así que la facturación queda fuera de las afirmaciones. Lo que está claro es una sustitución con un destino documentado y una diferencia de precio publicada.
Las especificaciones importan más que el precio. En la documentación, la comparación queda así:
- Posicionamiento en la documentación: Fable 5 apunta a agentes que funcionan durante mucho tiempo, y Opus 5 a programación agéntica compleja y cargas de negocio.
- Precio por millón de tokens, entrada y salida: $10 y $50 frente a $5 y $25.
- Ventana de contexto: 1M en ambos.
- Salida máxima: 128K en ambos.
- Fecha de corte fiable del conocimiento: enero de 2026 en Fable 5, mayo de 2026 en Opus 5.
El tamaño de contexto coincide, el techo de salida coincide y el conocimiento del modelo sustituto es cuatro meses más reciente. Su propia documentación incluso lo recomienda en primer lugar para programación agéntica exigente. El anuncio deja claro el coste del cambio, la capacidad biológica, y esa es una pérdida que ningún diff de código revelaría.
Así que la sustitución no agota un presupuesto de contexto, no choca con un límite de tokens y en una tarea de programación puede que ni siquiera parezca peor. Eso no es un atenuante. Es justo lo que hace invisible el cambio.
Leer las cifras con cuidado
La cifra principal es una caída de alrededor del 85% en los fallbacks relacionados con biología en todas las superficies de producto de Anthropic: un solo número, limitado a biología y agregado sobre todo.
Las cifras por superficie miden otra cosa y aparecen en una nota al pie sobre todos los fallbacks, sean de biología o no. Se prevé que el total de fallbacks baje cerca de dos tercios en Claude.ai (67%), algo más de la mitad en Cowork (55%), un 17% en Claude Code y solo un 7% en Claude Platform. No son porciones del 85, y mezclar ambas cosas da una frase limpia, citable y falsa.
Además son reducciones, no tasas. Un recorte del 7% en Platform no dice nada de la frecuencia absoluta allí, y puede que los fallbacks siempre hayan sido poco comunes. Lo que sí muestra la dispersión es dónde tuvo efecto el reajuste: el chat de consumo se movió unas diez veces más que la plataforma para desarrolladores, y la programación quedó en medio con un 17%.
Un clasificador cuya relajación mueve una caja de chat diez veces más que una API claramente no se calibró con tráfico de API. Cualquiera que ejecute una flota de subagentes estaba aguas abajo de una barrera ajustada para una persona que escribe una pregunta en un campo de texto.
Por justicia hace falta una aclaración, y es importante para la confianza: no se filtró nada. Anthropic publicó los detalles ella misma al lanzar la corrección, con una cifra incluida. La queja no es de ocultación. Es sobre la forma del mecanismo.
Por qué una degradación es peor que un rechazo
Un rechazo es una buena forma de fallar. Tiene tipo, lleva un código de estado, aparece en un dashboard, dispara una alerta, sigue una ruta de reintento y es, a la vista, culpa de otro.
Una degradación no ofrece ninguno de esos ganchos. No llega ningún error, ningún código de estado, ninguna entrada de log. Lo que vuelve es una respuesta válida y bien formada que es un poco peor: un parche al que le falta un caso límite, un test que se da por inestable, un resumen sin la salvedad que importaba.
Un resultado flojo de un subagente no suele llevar a nadie a culpar al proveedor. Se culpa al prompt, porque es la parte bajo control directo y la que más a menudo tiene la culpa. Así que se reformula, los resultados mejoran y la teoría parece confirmada. Pero una nueva redacción también le llega al clasificador como una entrada distinta. El remedio habitual para una mala respuesta altera lo que pudo provocar el cambio, de modo que el arreglo parece funcionar y el diagnóstico equivocado se queda. Si una sola edición arregla tanto un prompt vago como uno que activó un filtro, nadie puede decir qué problema había.
Meses de resultados mediocres de subagentes pueden haberse achacado al prompting cuando parte era una sustitución, y no hay forma de saber cuál, porque nadie registró qué modelo respondió.
Los datos siempre estuvieron ahí
No faltaba nada. El objeto de respuesta de la Messages API tiene un campo model junto a id, content, stop_reason, stop_details y usage. Cada respuesta devuelta ha nombrado el modelo que la produjo. Casi nadie lee ese campo, porque parece un eco de la petición.
El SDK todavía lo presenta como un eco de la petición: el comentario de documentación de model en el tipo de respuesta lo describe como el modelo que completará el prompt. Tiempo futuro, en un campo dentro de una respuesta ya terminada. Esa redacción fue correcta durante años y ya no lo es. Qué modelo atendió una llamada se decide en tiempo de ejecución, y el único registro fiable de ello es la respuesta que la mayoría del código tira.
Quienes usan Claude Code ya lo tienen en disco. Cada turno del asistente se añade a una transcripción JSONL bajo ~/.claude/projects/, con el objeto de mensaje completo de la API. El one-liner obvio cuenta cadenas de modelo:
grep -ho '"model":"[^"]*"' *.jsonl | sort | uniq -c | sort -rn
Contar sobre las doce transcripciones más recientes de una carpeta de proyecto dio estos totales: claude-opus-5 respondió 4,691 turnos, claude-opus-4-8 se ocupó de 1,231, seis turnos estaban marcados como sintéticos y un último grupo de 133 líneas decía solo sonnet, haiku u opus.
Esas 133 no son modelos servidos. Están en message.content.input.model, que guarda los parámetros de lanzamiento de subagentes: el modelo pedido para un subagente, registrado dentro de una llamada a herramienta. El modelo servido vive en message.model. El nombre de la clave es el mismo, la ruta es distinta y el significado es el contrario. Seleccionar por ruta hace desaparecer la cola:
jq -r 'select(.type=="assistant") | .message.model' *.jsonl | sort | uniq -c | sort -rn
El primer intento se equivocó, y la forma del error es la lección. Un mismo archivo guarda el modelo pedido y el servido bajo una clave idéntica, y la ruta es lo único que los distingue. Un grep simple da un recuento, pero no la distinción que importa.
Los dos ids completos se eligieron a propósito, en sesiones que abarcan un cambio de versión, así que esos totales no muestran ninguna sustitución del clasificador; en estos logs no ha aparecido ninguna. Lo que hay es una herramienta de medición, no un hallazgo. Los registros también incluyen isSidechain, que marca el tráfico de subagentes, y un requestId, así que comparar el modelo pedido y el servido para cada subagente es trabajo de una tarde sobre archivos que ya están en disco. Una fontanería tan simple no debería tener que escribirla el usuario.
El harness ya trata los cambios de modelo como eventos
Claude Code lleva meses, versión tras versión, tratando el fallback como un concepto de primera clase. La versión 2.1.166 introdujo el ajuste fallbackModel, que enumera hasta tres modelos para probar en orden cuando el principal está saturado o caído. En 2.1.76 esos avisos pasaron a verse siempre, ya no escondidos en el modo verbose, y ganaron nombres legibles. La versión 2.1.183 imprime un aviso en stderr en modo print si el modelo pedido está obsoleto o se ha actualizado en silencio. En 2.1.233, un id de modelo desconocido produce una línea de modelo no reconocido en stderr. La compilación más reciente añade los eventos de hook PreModelSwitch y PostModelSwitch para que el código pueda vetar, confirmar o anotar un cambio.
Cinco decisiones apuntan a una conclusión: un cambio de modelo es algo de lo que el operador debe enterarse.
Todo eso rige los cambios que hace el propio harness. Un cambio hecho aguas arriba por un clasificador llega sin aviso, sin evento de hook, sin salida en stderr y sin error tipado. Un equipo que decidió que una degradación por disponibilidad merece un banner permanente lanza una degradación por seguridad sin nada. La mala fe es una explicación improbable. Encaja mejor una costura: el harness informa de sus propias acciones, el modelo de las suyas, y la sustitución ocurre en el hueco entre ambos.
El mismo patrón en otros sitios
El 19 de agosto OpenAI presentó en preview Private Safety Processing, una capa de monitorización que examina juntas las entradas y salidas de varias conversaciones para detectar abusos que solo aparecen entre sesiones. La empresa dice que no conserva ningún dato del cliente y que solo genera una señal de alcance limitado que describe el tipo de actividad. Responde a la retención de 30 días que Anthropic anunció en julio para los modelos afectados, una política que inquietó a las empresas que manejan material sensible.
Llamarlo falso sería injusto. Simplemente no se puede verificar. Correlacionar comportamiento entre sesiones sin retener nada son afirmaciones en tensión visible, y resolverla exige saber qué contiene la señal y cuánto dura la ventana de correlación, y ninguna de las dos cosas se ha publicado.
La forma coincide con el caso del fallback: una propiedad del servicio que afecta de forma material a los clientes, observable solo por el proveedor y divulgada con la granularidad que el proveedor elija. El comportamiento de seguridad se ha convertido en un terreno de competencia, y la competencia se libra con atributos para los que los clientes no tienen instrumento.
Qué lo arreglaría
Aquí nadie pide marcha atrás. El clasificador de biología puede quedarse, igual que los fallbacks que el post enumera para virología, toxicología y diseño molecular. La petición es pequeña: cuesta una sola cadena.
La sustitución debe ir en el sobre de la respuesta. Cualquiera de estas opciones serviría: una cabecera, un campo extra en usage o un atributo de telemetría que separe el modelo pedido del que respondió. No hay nada raro en ello. Las peticiones de subagentes ya salen con las cabeceras agent-id y parent-agent-id, así que adjuntar datos del modelo a una petición es práctica establecida en una dirección. Mientras tanto, la exportación de OpenTelemetry etiqueta con un atributo model una sola métrica (líneas de código), y ninguna señal muestra en ningún sitio que el modelo que respondió fuera distinto del pedido.
No hace falta revelar el motivo. Basta una categoría, o incluso solo el hecho de que ocurrió, entregado en el momento en que ocurre, dentro de la respuesta.
La regla que se repite sobrevive a este proveedor y a este mes: lo que un proveedor se niega a poner en la respuesta se convierte en un bug del cliente. Acaba en su presupuesto de errores, su revisión de código, su postmortem y su estimación de su propia competencia. El 85% se midió del lado del proveedor de esa línea. Una cabecera pondría la siguiente cifra del lado del cliente.