Headroom con 77.253 estrellas: 300 líneas de log comprimidas a cero y cuatro cifras de ahorro distintas para la misma herramienta

Headroom anuncia un 20% menos de tokens para agentes de código y un 60-95% para JSON. Medido el 22-10-2026, un log de 300 líneas quedó en 35 tokens: '[300 lines compressed to 0...]' y desapareció una línea crítica de negocio. Su ruta JSON sí cumple (300 filas conservadas, 42,2% ahorrado, valores intactos) y el código pasa sin tocar (0%). PyPI dice 50-90%, GitHub 60-95%, la tabla del README 21-57%, la página de limitaciones ~52% y la model card 13%. 77.253 estrellas, 226 vigilantes, 174 releases.

Headroom comprime lo que lee tu agente antes de que el modelo lo vea. La descripción en GitHub lo resume en una línea: «20% fewer tokens for coding agents, 60-95% fewer tokens for JSON, same answers». La página de PyPI dice «Cut costs by 50-90%». La tabla de pruebas del README dice 21%, 57%, 42% y 30%.

Son cuatro afirmaciones distintas en cuatro superficies que controla el mismo equipo. Este artículo mide qué hace la librería con siete tipos de carga útil y después compara cada cifra publicada con todas las demás, porque el propio repositorio contiene la prueba de que sus números de portada no eran reproducibles: «by anyone, including us», según un script que ellos mismos distribuyen.

Las mediciones se tomaron el 22 de octubre de 2026 con headroom-ai 0.41.0 instalado desde PyPI en un entorno limpio de Python 3.14 (pip install headroom-ai, solo extras base). El conteo de tokens usa tiktoken cl100k_base. No se usó ninguna clave de API ni se llamó a ningún modelo: solo se invocó compress(). Los datos del repositorio vienen de la API REST de GitHub y de un clon nuevo de main (3.324 commits). Los números del proveedor son suyos; las cargas de prueba y la comparación son mías.

El resultado que importa: 300 líneas de log se convirtieron en cero

Generé 300 líneas de log de aplicación corrientes —fecha ISO, nivel, worker, id de petición, ruta, estado, duración— y las pasé a compress() como resultado de herramienta, con un mensaje de usuario preguntando «Anything wrong in the logs?».

'[300 lines omitted: 300 INFO]\n[300 lines compressed to 0. Retrieve more: hash=23769fb19ad8a5495f533075]'

De 10.199 tokens a 35. Un ahorro del 99,7%. Todas las líneas desaparecieron.

La vía de escape existe de verdad: el hash= es un puntero CCR, y un agente que sepa llamar a headroom_retrieve puede recuperar el original del almacén local. Pero la carga comprimida no contiene ninguna señal de que se haya descartado algo concreto: informa un recuento y un hash. Un agente que lea «300 INFO, comprimido a 0» y responda a partir de eso está respondiendo desde la nada.

Después cambié exactamente una línea, sustituyendo la línea 211 por otra de la misma forma pero con datos de negocio en lugar de ruido:

2026-10-21T03:14:07 INFO worker-3 request id=447120 path=/v1/orders status=200 dur=88ms tenant=acme-eu plan_downgrade_scheduled=2026-11-01 seats=14

Resultado: de 10.220 tokens a 38. La aguja no sobrevivió. acme-eu no aparece en la salida. Es una línea INFO, no es un error, y el compresor la trató como una de 300 líneas de log intercambiables.

Ahora pongamos una línea FATAL, en la posición 67:

2026-10-21T02:11:09 FATAL worker-3 out of memory while loading shard 7 (heap 3.9GB/4.0GB)

Resultado: de 10.203 tokens a 282, nueve líneas conservadas, y la línea FATAL llega byte por byte, tal como promete la demo animada del README. El README no miente. Está mostrando el caso en el que la red de seguridad atrapa la línea importante.

Lo que hay que mirar es qué separa los dos desenlaces: una palabra clave. La regla de retención no es «conserva lo que importa para la pregunta» —la compresión no depende de la consulta, lo verifiqué ejecutando la misma carga contra tres preguntas distintas y obteniendo una salida byte a byte idéntica—. La regla es «conserva las líneas que casan con un patrón de error, más una ventana de contexto alrededor». Cambia una palabra en una línea y las mismas 300 líneas pasan de 9 conservadas a 0.

No es un fallo en el sentido de una caída. La documentación lo describe abiertamente: se conservan los elementos de error «containing ’error’, ’exception’, ‘failed’, ‘critical’», y aparece bajo Safety guarantees (additive, never dropped). La garantía es sobre una lista de palabras, y ningún compresor puede prometer más que su regla de retención. Lo que la garantía no cuenta es el complemento: todo lo que no casa con la lista, en un log donde no casa nada, se elimina, y la carga lo dice en una línea que un modelo leerá como rutina de mantenimiento.

Qué hicieron realmente las siete cargas

CargaAntes (tokens)Después (tokens)AhorroTransformación
300 líneas de log, sin palabra de error10.1993599,7%router:log:0.00
igual + una línea crítica de negocio10.2203899,6%router:log:0.00
igual + una línea FATAL10.20328297,2%router:log:0.03
array JSON de 300 objetos14.1648.18142,2%router:smart_crusher:0.37
array JSON de 500 números12.0018.01133,2%router:smart_crusher:0.51
120 funciones Python (código)3.3603.3600,0%protected:recent_code
traza de error repetida1.4001.4000,0%protected:error_output
JSON de 2 elementos13130,0%—

Dos de estas filas son la mejor prueba de que el proyecto no es un envoltorio de buenos deseos.

La ruta JSON es conservadora en el sentido correcto. Las 300 filas sobrevivieron, las 500 filas numéricas sobrevivieron y todos los valores que comprobé llegaron sin cambios. Lo que cambió fue la codificación: los objetos JSON pasaron a una línea de cabecera más filas CSV.

[300]{amount:float,created:string,id:string,note:string,status:string}
613.15,2026-10-06T05:00:00Z,ORD-00000,manual,paid
42.46,2026-10-15T18:00:00Z,ORD-00001,retry,paid

Es un ahorro del 42,2% sin pérdida de información: la redundancia son las llaves, las claves repetidas y las comillas. La fila por la que preguntaría un operador, ORD-00042, está ahí. Este es el núcleo honesto del producto, y mi 42,2% queda junto al «~52%» que la propia página de limitaciones da para arrays de diccionarios, y bien dentro de la banda 21-57% del README.

Las compuertas de protección funcionan. El código y las trazas vuelven idénticos, etiquetados protected:recent_code y protected:error_output. La página de limitaciones dice que es intencional y explica por qué: «Code is almost always fetched because the user wants to work with it. Compressing function bodies would remove exactly what they need.»

Lo cual plantea sobre qué se cuenta el «20% fewer tokens for coding agents». En esta prueba, sobre código, la respuesta es 0%. La tabla de pruebas del README lo dice con más precisión que el eslogan: «Code search (100 results) 21%», «Codebase exploration 42%»; esos son resultados JSON de búsqueda y listados de archivos, no archivos de código. El ahorro en un agente de código viene del envoltorio de la salida de herramientas, no del código.

Cuatro cifras de ahorro para una sola herramienta

Cada fila es el mismo proyecto describiéndose a sí mismo.

SuperficieCifraTexto exacto
Página de PyPI50–90%«Cut costs by 50-90%»
Descripción en GitHub20% / 60–95%«20% fewer tokens for coding agents, 60-95% fewer tokens for JSON, same answers»
Tabla del README21–57% (total 41%)178.024 → 104.261 tokens en cuatro escenarios
Tabla de limitaciones~45–52% para JSON de diccionarios/mezclado, ~95–97% para arrays de cadenas/númeroscada fila es «a directional single-sample measurement»
Model card de Hugging Face13% de subpalabras con el umbral por defecto«F1 0.920, must-keep recall 0.989, keep rate 0.866 (13% of subwords removed)»

El eslogan de PyPI no mide nada; es una cadena de marketing que se lee como una medición. El 60–95% de GitHub es el caso de arrays de cadenas y números, que es el caso fácil: rutas deduplicadas, etiquetas, series de métricas. La tabla del README, que es la más defendible de las cinco, nunca pasa del 57% y promedia 41%.

La página de limitaciones va más lejos de lo que suele ir una página de marketing:

«The compression figures below are not from a published, repeatable benchmark suite – we could not trace a prior version of this table to one. Instead of an unsourced number, each row is a directional single-sample measurement taken with compress() against a synthetic payload of that shape.»

Léelo dos veces. La propia página de limitaciones del proyecto afirma que los números de su propia portada no se pudieron rastrear hasta un benchmark. Es una honestidad poco común y merece decirse claro: es un repositorio que audita sus propias afirmaciones y publica la corrección. Casi ningún proyecto con 77.000 estrellas lo hace.

Pero la corrección entró en la documentación mientras los números viejos y más grandes se quedaron en PyPI y en la descripción de GitHub, que son las dos superficies que un lector ve primero.

El problema de procedencia también está documentado en el código. benchmarks/index_proof_table.py abre así:

«docs/content/docs/index.mdx published four precise before/after token counts with no reproducible source. The nearest harness, real_world_agent_benchmark.py, seeded nothing, so its corpus differed on every run and the published figures could not be reproduced by anyone, including us. A number on the front page of the docs that nobody can regenerate is a liability, not evidence.»

Así que construyeron un arnés con semilla. Lo cual me lleva al hallazgo más pequeño de este artículo, y el que más me gusta.

El arnés escrito para separar «de fábrica» de «mejor caso» produce un solo número

El script está escrito para imprimir dos configuraciones: default (protect_recent=4), descrita como «what a coding agent actually gets out of the box», y full corpus (protect_recent=0), descrita como «the honest number for ‘how far can this payload compress’». El comentario explica por qué importa la distinción: «Under the default, protect_recent=4 shields almost every tool result and the measurement says more about the guard than about the compressor.»

El archivo de resultados versionado, benchmarks/results/index_proof_table.txt, contiene las dos ejecuciones:

=== default (protect_recent=4) ===
Code search (100 results)          17,199     13,597       21%
SRE incident debugging             55,957     24,340       57%
Codebase exploration               58,801     33,895       42%
GitHub issue triage                46,067     32,429       30%
TOTAL                             178,024    104,261       41%

=== full corpus (protect_recent=0) ===
Code search (100 results)          17,199     13,597       21%
...
TOTAL                             178,024    104,261       41%

Idénticas, fila por fila. El parámetro que el arnés existía para probar no tiene ningún efecto en estos escenarios, porque protect_recent es una ventana de protección de código y los escenarios son JSON. Así que la distinción para la que se escribió el script no existe en su propia salida, y el README cita el único número que producen ambas configuraciones, sin etiquetarlo como ninguna de las dos.

No es un escándalo. Es lo que pasa cuando una compuerta se documenta como general y se implementa como estrecha, y es una buena ilustración de por qué conviene leer el comentario del arnés antes que la tabla.

El modo por defecto no comprime gran cosa, y el changelog lo dice

El modo más importante de Headroom es el que su nombre no anuncia: --mode cache. Del changelog, una entrada que corrige el contador de ahorro:

«SavingsTracker.record_request() only appended a history point when tokens_saved > 0 (headroom’s own lossy compression). In --mode cache, tokens_saved is near-always 0 by design, since the frozen prefix is byte-replayed rather than compressed to keep the provider’s prompt cache warm.»

Byte-replayed, reenviado byte a byte. En el modo caché por defecto, el prefijo congelado de la conversación no se comprime en absoluto: se reenvía tal cual para que la caché KV del proveedor siga acertando, y el ahorro en ese modo es ahorro de caché, no de compresión. El README dice lo mismo desde el otro lado: «Live-zone compression — only new bytes are compressed (fresh tool output, the latest turn). The frozen prefix stays byte-identical, so the provider cache survives, and history is never dropped.»

Es una apuesta de diseño, y es la correcta: un token de entrada leído de la caché de Anthropic cuesta una décima parte que uno fresco, así que recomprimir el prefijo en cada turno para ahorrar un 40% de tokens perdería dinero. El precio de la apuesta es que la compresión se aplica solo a la salida de herramienta más reciente, una vez, antes de congelarse.

También explica una discrepancia que de otro modo parecería un fallo: la herramienta comprime «todo lo que lee tu agente», pero una sesión larga es sobre todo prefijo congelado, así que la mayoría de los bytes nunca se comprimen. Los bytes que ves en headroom savings son la zona viva.

Probé el invariante del que depende este diseño, porque es lo más difícil de hacer bien: ¿el compresor produce una salida estable para el mismo mensaje entre turnos? Dos ejecuciones con la misma entrada fueron idénticas byte a byte. Una conversación de tres mensajes y esa misma conversación ampliada con un resultado de herramienta nuevo y una pregunta nueva produjeron una compresión idéntica del primer mensaje de herramienta. Lo mismo con tres preguntas distintas contra la misma carga. Ese determinismo no es casual —es la propiedad que hace sobrevivible el caché de prefijo— y con esta evidencia se sostiene.

La evidencia de precisión no cubre el fallo de arriba

El README informa cuatro filas de precisión. python -m headroom.evals suite --tier 1 define nueve benchmarks de nivel 1, con gpt-4o-mini por defecto, y las diferencias importan más que los números.

BenchmarkCómo se ejecutaN¿En la tabla del README?
GSM8Karnés lm-eval a través del proxy100sí (0,870 → 0,870)
TruthfulQAarnés lm-eval a través del proxy100sí (0,530 → 0,560)
MMLUarnés lm-eval2 por materiano
ARC-Challengearnés lm-eval100no
HumanEvalarnés lm-eval164no
SQuAD v2antes/después, accuracy_preservation_rate100sí (97% con 19% de compresión)
BFCLantes/después, coincidencia con la verdad100sí (97% con 32% de compresión)
Tool Outputsantes/después, accuracy_preservation_rate8no
CCR Round-tripida y vuelta byte a byte, umbral 1,050no

GSM8K y TruthfulQA corren con el arnés estándar de lm-eval contra el proxy. Sus prompts son problemas cortos y preguntas factuales. No hay ningún log de 300 líneas, ningún array JSON de 14.000 tokens, y nada que las rutas JSON o de log puedan hacer; es coherente con que GSM8K salga exactamente igual hasta tres decimales (0,870 → 0,870) y con la propia nota del README de que un delta de ±0,03 cae dentro del intervalo de confianza con N=100. Esas dos filas demuestran que una petición sin nada que comprimir no cambia. No pueden demostrar que una salida de herramienta comprimida siga permitiendo una respuesta correcta.

El benchmark que mide justo lo que puede salir mal es el que se llama Tool Outputs, muestrea ocho casos, y no aparece en la tabla del README. La documentación no dice qué comprime ninguna de estas ejecuciones, ni qué modelo las sirve, ni cuáles son las líneas base de SQuAD y BFCL: su columna Baseline es un guion. Si quieres saber si tu agente sigue respondiendo bien sobre un log comprimido, la evidencia publicada del proyecto no lo responde, y su propio arnés, tal como está configurado, no puede.

La model card del compresor de texto es el único lugar donde existe una métrica real de fidelidad, y también el documento más autocrítico del repositorio:

«An earlier version of this card reported metrics on the v2.0 split, where 12.2% of test rows were byte-identical to training rows; those numbers were optimistic by roughly 0.02 F1 and have been replaced.»

y

«The rest of the split is text where almost every word should be kept, which inflates F1 for any model.»

Con el umbral por defecto (0,5) el modelo elimina el 13% de las subpalabras con un must-keep recall de 0,989. Es un compresor de texto modesto y bien documentado. Es también el mismo producto cuya línea de PyPI dice 50–90%.

El repositorio, en números

MétricaValor (22-10-2026)
Estrellas / vigilantes / forks77.253 / 226 / 5.991
Vigilantes como proporción de estrellas0,29%
Issues abiertas528
Creado2026-01-07
Commits / contribuidores / ramas3.324 / 333 / 572
Releases / tags174 / 175
Archivos versionados / bytes2.662 / 73.302.851
Mezcla de lenguajes (GitHub)Python 82,1%, Rust 12,2%, TypeScript 2,2%
Archivos .py / .rs1.712 / 218
CHANGELOG.md599.648 bytes, 2.048 entradas, 34 versiones documentadas
Descargas PyPI (30 días)292.636
Descargas npm (30 días)168.866
Descargas de assets de release (últimas 100)13.123

Dos cosas destacan. Primero, 226 vigilantes frente a 77.253 estrellas es un 0,29%: la proporción más baja que he medido en esta serie, donde repositorios comparables están entre 0,55% y 0,64%. La estrella es un marcador; el vigilante es alguien que pidió que le avisen. Segundo, el proyecto publica 174 releases en nueve meses y medio —una versión cada 1,7 días— dentro de un número de versión 0.x, con un changelog de 2.048 entradas y un núcleo en Rust (12,2% de la mezcla) creciendo al lado de un núcleo en Python. No hay 1.0, no hay LTS y no hay documento de ruta de actualización: el changelog es la ruta de actualización.

La adopción no está en duda: 292.636 descargas de PyPI en 30 días y 168.866 de npm es una herramienta que la gente instala de verdad, y la cobertura es más amplia que la de casi cualquier proyecto tan joven, con 18 integraciones de agentes listadas y un unwrap documentado para cada una.

Qué comprobar antes de instalarlo

Cinco comprobaciones, en el orden que habría detectado todo lo anterior. Todas son locales y no cuestan nada.

  1. Comprime tu peor carga, no la de la demo. Ejecuta compress() sobre una salida de herramienta real de tu agente —no la sintética del README— y lee la salida antes de meterla en un prompt. El modo de fallo es una carga que parece completa.
  2. Cuenta lo que sobrevivió, no lo que se ahorró. Para cada resultado comprimido, verifica que los identificadores que necesitabas están en la salida. En mi prueba de log, acme-eu no estaba y el marcador decía «300 INFO»: un modelo que lea eso informará una ejecución limpia.
  3. Trata un marcador compressed to 0 como alarma, no como resumen. La configuración por defecto inyecta una herramienta de recuperación (inject_tool: True), así que la vía de recuperación existe; solo ayuda a un agente que decida usarla.
  4. Mide el modo en el que vas a correr. headroom savings informa la zona viva. Si estás en el modo caché por defecto, la mayor parte de tu contexto se reenvía byte a byte y nunca aparecerá en ese número.
  5. Haz la comprobación de precisión que tu tráfico merece. La suite publicada es gpt-4o-mini sobre datasets académicos con N=8 en la única fila de salidas de herramienta. Construye un conjunto de regresión de 200 casos de tus propias sesiones, ejecútalo en los dos sentidos y compara las respuestas: ese es el único número que se aplica a tu caso, y la documentación lo dice: «Your own traffic will vary; measure it with client.chat.completions.simulate() rather than treating these as guarantees.»

Preguntas frecuentes

¿La compresión JSON de Headroom tiene pérdidas? En mi prueba con un array de 300 objetos, las 300 filas sobrevivieron con los valores intactos; el ahorro vino de convertir JSON a una codificación de cabecera más CSV (42,2%). Es una recodificación, no un resumen. La ruta con pérdidas en mis pruebas fue la de logs de texto plano, y los casos de arrays de cadenas y números que la documentación describe como «statistical summary» son explícitamente con pérdidas.

¿Por qué 300 líneas de log se convirtieron en cero? El compresor de logs conserva las líneas que casan con un patrón de error más una ventana de contexto, y conserva contexto del principio y del final. Sin ninguna línea que case, no hay ancla de retención y la carga colapsa a un recuento más un hash CCR. Añadir una línea FATAL cambió el desenlace de 0 líneas conservadas a 9.

¿La compresión rompe el caché de prompt? En mis pruebas, no. La salida fue idéntica byte a byte para la misma entrada y estable cuando la conversación creció, que es lo que exige el caché de prefijo. Ojo: el --mode cache por defecto no comprime el prefijo congelado en absoluto; es un intercambio deliberado para mantener calientes las lecturas de caché.

¿Es real el 60–95%? Es real para arrays de cadenas y números, que es el caso que la documentación mide en ~95% y ~97%. Para arrays de diccionarios —«primary use case» en la página de limitaciones— es ~52% allí y 42,2% en mi medición. Los cuatro escenarios del README promedian 41%.

¿Qué instalación necesito para cada ruta? JSON solo requiere el núcleo. La compresión de código necesita headroom-ai[code] (tree-sitter) y protect_analysis_context=False para activarse. Texto/RAG necesita [ml], que carga un modelo ONNX int8 de 274 MB. El proxy necesita [proxy], MCP necesita [mcp]. [all] cubre el núcleo pero no los adaptadores de framework ni el backend [vector].

¿Es seguro dejar que reescriba las salidas de herramienta sin verlas? Las compuertas de protección son reales —el código y las trazas pasaron intactos en todas las pruebas— y el comportamiento fail-open está documentado. El riesgo se concentra en cargas de texto y logs que comprimen mucho precisamente porque casi todo su contenido es redundante, y donde la única línea que no lo era se va con el resto.

La versión corta

Headroom es una herramienta seria con un núcleo honesto y cuatro números distintos en la portada. La ruta JSON cumple lo que afirma la tabla del README —reproduje 42,2% donde ellos dicen 21–57%— y lo hace recodificando en lugar de borrar. La ruta de código está desactivada por compuertas, así que el «20% para agentes de código» es en realidad «20% del JSON que rodea al código». La ruta de logs es donde la ratio de compresión deja de significar nada como métrica: 99,7% ahorrado y el 100% del contenido eliminado, con la diferencia entre «nueve líneas conservadas» y «nada conservado» decidida por si una línea contiene la palabra FATAL.

El proyecto sabe casi todo esto. Su página de limitaciones dice que su propia tabla no se pudo rastrear hasta un benchmark; su arnés dice que los números publicados no los pudo reproducir «by anyone, including us»; su model card corrige su propia evaluación contaminada. Es una cantidad de autocorrección poco frecuente, y es justo por eso que los números que nunca se corrigieron —50–90% en PyPI, 60–95% en GitHub— son los que hay que descontar con más fuerza.

Mide tu propia carga. Cuenta lo que sobrevivió. El porcentaje es el número menos informativo que imprime la herramienta.