Módulo 3: Medir costo y tokens por run
Módulo 3: Medir costo y tokens por run
Descripción
El Módulo 2 resolvió un problema real: un run que fallaba con RuntimeError dejaba de perder toda su información. traced_run registra cada paso del loop —cada tool_use, cada tool_result, el cierre del run— en el instante exacto en que ocurre, con un trace_id determinista que correlaciona todo, incluso cuando el run entero se cae. Al cerrar ese módulo, tenías observability/run_logger.py completo y un RUN_LOG.jsonl real en tu disco, capaz de reconstruir la historia completa de cualquier run.
Pero hay una pregunta que ese archivo, tal como quedó, no puede responder. Corre de nuevo el mismo run limpio del Módulo 2 —"Reserva Focus pro 3h para Ana", sin ningún error— y mira, con atención, qué campos trae cada línea:
import logging
import run_logger as rl
import reservo_agent as ra
rl.logger.setLevel(logging.INFO)
script_a = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "list_rooms", "input": {}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_02", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 3}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_03", "name": "book_room",
"input": {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Reservé Focus pro 3h para Ana. Confirmación #1."}]},
]
with rl.traced_run("Reserva Focus pro 3h para Ana", 1) as trace_id:
final, history = ra.run_reservo_agent("Reserva Focus pro 3h para Ana", script_a)
Qué esperar:
{"seq": 1, "trace_id": "run-8487582448eb", "event": "run_started", "question": "Reserva Focus pro 3h para Ana", "tool_errors": 0, "error": ""}
{"seq": 2, "trace_id": "run-8487582448eb", "event": "tool_use", "step": 1, "tool": "list_rooms", "is_error": false, "content": ""}
{"seq": 4, "trace_id": "run-8487582448eb", "event": "tool_result", "step": 1, "tool": "list_rooms", "is_error": false, "content": "[{\"room\": \"Focus\", \"rate_cents\": 2500}, {\"room\": \"Studio\", \"rate_cents\": 4000}, {\"room\": \"Boardroom\", \"rate_cents\": 8000}]"}
{"seq": 6, "trace_id": "run-8487582448eb", "event": "tool_use", "step": 2, "tool": "get_quote", "is_error": false, "content": ""}
{"seq": 8, "trace_id": "run-8487582448eb", "event": "tool_result", "step": 2, "tool": "get_quote", "is_error": false, "content": "{\"price_cents\": 6000}"}
{"seq": 10, "trace_id": "run-8487582448eb", "event": "tool_use", "step": 3, "tool": "book_room", "is_error": false, "content": ""}
{"seq": 12, "trace_id": "run-8487582448eb", "event": "tool_result", "step": 3, "tool": "book_room", "is_error": false, "content": "{\"booking_id\": 1, \"confirmed\": true}"}
{"seq": 14, "trace_id": "run-8487582448eb", "event": "run_finished", "question": "Reserva Focus pro 3h para Ana", "tool_errors": 0, "error": ""}
Ocho líneas, completas, correlacionadas por run-8487582448eb. Sabes con certeza cuántos pasos dio el run, qué tool llamó cada uno, si algo falló. Y sin embargo, ninguna de las ocho líneas —ni una sola clave de ningún objeto JSON— dice cuánto costó este run. RunEvent tiene seq, trace_id, event, question, tool_errors, error. ToolCallEvent tiene seq, trace_id, event, step, tool, is_error, content. En ningún lado hay un campo tokens, ni cost_cents, ni nada que se le parezca. El Módulo 2 resolvió, por completo, la pregunta "¿qué pasó en este run?" — y dejó absolutamente intacta la pregunta "¿cuánto costó que pasara?". Ese es, con precisión, el problema de este módulo.
Conexión con el módulo
Este módulo no reemplaza nada del Módulo 2 — lo usa. Cada lección que sigue reusa traced_run y el trace_id determinista tal como quedaron, y construye, al lado, un artefacto nuevo: observability/cost_calculator.py. El puente es literal: un CostReport de este módulo se identifica por el mismo trace_id que ya viste en RUN_LOG.jsonl — la misma correlación de siempre, aplicada ahora al costo.
Regla dura de esta guía (heredada, con una precisión nueva)
La regla dura de los Módulos 1 y 2 sigue exactamente igual: la decisión del modelo no se ejecuta. Este módulo agrega una sola precisión, porque el tema ahora es dinero, y equivocarse con dinero —aunque sea estimado— es peor que no calcularlo:
- El costo se estima, y la aritmética del costo SÍ se ejecuta, de verdad. La conversión de texto a tokens usa, sin cambios, la convención honesta que ya viste nombrada en los dos módulos anteriores:
len(texto) // 4, rotulada siempre como estimación de orden de magnitud — nunca como el conteo exacto de un tokenizer real. Este módulo es donde esa convención, mencionada hasta ahora de pasada, se vuelve una función real, probada, y usada para calcular dinero. - El pricing de
claude-sonnet-5es una constante fija y citada. $3.00 por cada millón de tokens de entrada, $15.00 por cada millón de tokens de salida — el precio de lista, verificado en la documentación oficial de Claude. Este módulo fija esa constante una sola vez, en la lección 04, y el resto de la guía la reusa sin volver a citarla. - La llamada al LLM sigue siendo concepto. Nunca se llama a la API de Claude para medir un costo real — el costo se calcula, siempre, con la fórmula fija sobre tokens estimados.
- Nada de aleatoriedad ni de reloj real. Sin
random, sindatetime.now(), sinuuid4(), sintime.time(). Lostrace_idsiguen siendo los deterministas del Módulo 2.
Guárdate esta frase, porque la vas a usar en cada lección que sigue: el Módulo 2 te dice QUÉ pasó en un run; este módulo te dice CUÁNTO costó que pasara — con la misma correlación, el mismo trace_id, sin volver a construir nada del loop.
Dónde estamos en el ecosistema
Agentes en producción — operar el agente de Reservo
├── Módulo 1: Por qué operar es distinto de construir
├── Módulo 2: Logging estructurado y trazado de un run
├── Módulo 3: Medir costo y tokens por run ← ESTÁS AQUÍ
│ → tokens como unidad de costo, len(texto)//4, el pricing
│ fijo de claude-sonnet-5, centavos por run, escalado a miles
│ de runs, el costo como señal operacional
├── Módulo 4: Medir latencia con honestidad
├── Módulo 5: Evals de regresión como gate de producción
├── Módulo 6: Fallos a escala — backoff, circuit breakers y rate limits
├── Módulo 7: Versionado y rollout seguro
└── Módulo 8: Proyecto — el agente de Reservo en producción
Este es el segundo de los siete módulos que construyen las cuatro disciplinas del Módulo 1: observar → medir → gatear → endurecer+versionar. El Módulo 2 construyó la capa de observar. Este módulo es la primera mitad de la capa de medir —la segunda mitad, la latencia, es el Módulo 4—. observability/cost_calculator.py, el artefacto que se construye aquí, se reusa sin cambios desde el Módulo 4 en adelante, exactamente como run_logger.py se reusa desde este módulo.
La analogía central de este módulo: el medidor de luz, antes de que llegue la factura
Un electrodoméstico —una plancha, un refrigerador, un cargador— consume electricidad mientras está encendido, y ese consumo se mide en una unidad precisa: kilowatts-hora. Una sola plancha, encendida media hora, consume una fracción de kilowatt-hora tan pequeña que, si miraras solo esa media hora, dirías que "casi no cuesta nada" — y tendrías razón, para ese uso aislado. Pero una empresa de electricidad no factura una plancha; factura un edificio completo, con cientos de electrodomésticos encendidos, todos los días, durante todo el mes. La factura que llega a fin de mes es la suma de miles de esas fracciones diminutas — y esa suma sí es una cifra que un negocio necesita presupuestar con cuidado.
Un run del agente de Reservo es exactamente esa plancha encendida media hora: consume tokens —la unidad de costo de un modelo de lenguaje, tan real como el kilowatt-hora de un electrodoméstico—, y esa fracción de centavo, mirada un run a la vez, parece insignificante. Pero Reservo no atiende un usuario al día — atiende miles, cada uno generando su propio consumo de tokens. Medir el costo por run, antes de que llegue la "factura" del mes completo, es exactamly lo que este módulo enseña a hacer: leer el medidor de cada plancha individual, para poder presupuestar la factura del edificio entero antes de que llegue —no después, cuando ya es demasiado tarde para actuar sobre ella.
El caso que sigue acompañando la guía: Reservo, sin reconstruir nada
Las cuatro tools son las mismas de siempre —list_rooms(), get_quote(room, tier, hours), book_room(room, tier, hours, member), cancel_booking(id)—, y las dos anclas de precio siguen intactas: Focus basic 3h = 7500 centavos, Focus pro 3h = 6000 centavos. Este módulo no declara ninguna tool nueva, no toca reservo_tools.py, reservo_contracts.py, reservo_robust.py ni reservo_agent.py, y tampoco toca observability/run_logger.py del Módulo 2 — lo importa, tal como quedó, y construye al lado un artefacto nuevo:
- Lección 03 construye
estimate_tokens(text), la función que aplicalen(texto) // 4de verdad, con sus límites confirmados sobre textos reales de Reservo. - Lección 04 fija la constante de pricing de
claude-sonnet-5—citada, con su nota honesta sobre el precio promocional— y las dos constantes en centavos por millón de tokens. - Lección 05 combina ambas piezas en
estimate_cost_centsycost_for_run, ejecutadas sobre un run real de Reservo, con desglose de costo por cada tool call dentro del run. - Lección 06 agrega la agregación por lote y el escalado a miles de runs — la parte de la analogía donde la fracción diminuta se convierte en una factura real.
- Lección 07 trata el costo como una señal operacional más, junto a la tasa de error y de fallo por herramienta del Módulo 1, y traza la frontera con
cost-optimization-caching-guide—la guía que enseña a reducir el costo, no a medirlo. - Lección 08 cierra el módulo con
observability/cost_calculator.pycompleto, ejecutado sobre un lote de runs contrace_id, en un reporte de costo integral.
Como en cada módulo de esta guía: identificadores y código en inglés; prosa y comentarios, en español; dinero, siempre en centavos int.
Prerequisitos
Conocimiento requerido:
- ✅ Haber completado el Módulo 2 de esta guía, en especial las lecciones 04 (
make_trace_id,open_run) y 05-06 (traced_runcompleto, conToolCallEvent). Este módulo importarun_loggersin volver a explicar ninguna de sus piezas. - ✅ Haber completado (o conocer bien)
agent-fundamentals-and-tool-calling-guideM8:run_reservo_agent, y el formato dehistory(turnosuser/assistant, bloquestool_use/tool_result/text). - ✅ Python: funciones,
dataclasses, comprensión básica de aritmética entera (//, el operador de división entera que ya usaste para el descuentoprode Reservo,* 80 // 100).
Recomendado:
- ✅ Haber visto, alguna vez, una factura de un servicio de API cobrado por uso y haberte preguntado "¿de dónde sale exactamente este número?" — esa pregunta es, con precisión, lo que este módulo responde para el agente de Reservo.
NO requerido:
- ❌ No necesitas una API key ni conexión a internet: la decisión del modelo sigue siendo concepto, y todo el cálculo de costo de este módulo corre 100% local, con aritmética entera pura.
- ❌ No necesitas instalar
tiktokenni ningún tokenizer real. Esta guía usa, deliberadamente, la estimaciónlen(texto) // 4— la lección 03 explica con precisión por qué, y qué se pierde al hacerlo. - ❌ No necesitas saber nada de facturación real de proveedores de LLM más allá de la tabla de precios que la lección 04 fija como constante — no hay contratos, tiers de descuento por volumen ni nada de eso en el alcance de este módulo.
Entorno:
- ✅ Python 3.14.0 con su librería estándar (
dataclasses,json,itertools,statistics). Nada que instalar. - ✅ El directorio
observability/del Módulo 2, conrun_logger.pyya construido y funcionando.
Roadmap del módulo
Lección 01 — Introducción al módulo (esta)
El límite exacto que deja el Módulo 2 —RUN_LOG.jsonl completo, sin ningún campo de costo—, la analogía del medidor de luz, y el mapa de las ocho lecciones.
Lección 02 — Los tokens son la unidad de costo
Qué es un token, por qué un proveedor de LLM cobra por token y no por llamada ni por segundo, y la diferencia entre contar caracteres, palabras y tokens sobre el mismo texto.
Lección 03 — Estimando tokens con len // 4
estimate_tokens(text), ejecutada sobre textos reales de Reservo, con sus límites confirmados: por qué es una estimación de orden de magnitud, nunca un conteo exacto, y en qué casos se equivoca más.
Lección 04 — El pricing de claude-sonnet-5
La constante fija y citada: $3.00/$15.00 por millón de tokens de entrada/salida, precio de lista — con la nota honesta sobre el precio promocional, y por qué el output cuesta cinco veces más que el input.
Lección 05 — Costo por run, en centavos
estimate_cost_cents y cost_for_run, ejecutadas sobre un run real de Reservo correlacionado por su trace_id, con el desglose de costo de cada tool call dentro del run.
Lección 06 — Escalando el costo a miles de runs
De un run que cuesta una fracción de centavo a la proyección de 1.000, 10.000 y 100.000 runs — y el error real de sumar centavos ya redondeados en vez de sumar tokens primero.
Lección 07 — El costo como señal operacional
El costo junto a la tasa de error y de fallo por herramienta del Módulo 1: cómo detectar un run anómalamente caro, y la frontera con cost-optimization-caching-guide.
Lección 08 — Mini-proyecto: un reporte de costo para los runs de Reservo
observability/cost_calculator.py completo, ejecutado sobre un lote de runs de Reservo trazados, con un reporte de costo integral: por run, por lote, y escalado.
Mapa de progresión
Lección 01 (esta) → El límite del Módulo 2, la analogía del medidor
Lección 02 → Qué es un token, por qué se cobra por token
Lección 03 → estimate_tokens(text) = len(text) // 4, ejecutada
Lección 04 → El pricing fijo de claude-sonnet-5
Lección 05 → Costo por run, con desglose por tool call
Lección 06 → De centavos por run a la factura de miles de runs
Lección 07 → El costo como señal, la frontera con "reducir costo"
Lección 08 → Mini-proyecto: el reporte de costo completo
Dificultad: ⭐⭐ ──────────────────▶ ⭐⭐⭐
Qué lograrás en este módulo
Al completar las 8 lecciones, podrás:
- Explicar por qué un proveedor de LLM cobra por token, y distinguir un token de un carácter y de una palabra sobre el mismo texto.
- Estimar tokens con
len(texto) // 4, y explicar con precisión por qué es una estimación de orden de magnitud —nunca un conteo exacto— y en qué casos se aleja más de la realidad. - Citar el pricing de
claude-sonnet-5($3.00/$15.00 por millón de tokens de entrada/salida, precio de lista) y explicar por qué el costo de salida pesa más que el de entrada. - Calcular el costo de un run en centavos, con desglose por cada tool call, correlacionado por su
trace_id. - Escalar el costo de un run a un lote de miles, y evitar el error de sumar cifras ya redondeadas en vez de sumar tokens antes de redondear.
- Usar el costo como una señal operacional, detectar un run anómalamente caro, y trazar la frontera con la guía que enseña a reducir costo, no a medirlo.
El antes y después
ANTES del módulo:
→ "el costo de un run es un detalle de facturación, no algo que
se calcule con código"
→ "si un run cuesta una fracción de centavo, no vale la pena
medirlo"
→ "el precio de un modelo es un número que busco cuando lo
necesito, no algo que fijo en el código"
→ "todos los tokens cuestan lo mismo, sean de entrada o de salida"
DESPUÉS del módulo:
→ el costo por run es una señal operacional de primera clase,
calculada con la misma aritmética entera que el resto del
sistema
→ una fracción de centavo, multiplicada por miles de runs, es
una cifra real de presupuesto -- medirla por run es leerla
ANTES de que llegue la factura
→ el pricing es una constante fija, citada una vez, reusada en
toda la guía
→ el token de salida cuesta cinco veces más que el de entrada --
una asimetría con consecuencias de diseño reales
Trampas a evitar al cursar este módulo
1. "Este módulo va a llamar a la API de Claude para medir el costo real"
No. La regla dura de esta guía —heredada de agent-fundamentals, context-engineering y los dos módulos anteriores de esta misma guía— prohíbe la red en toda su ingeniería. El costo se estima: tokens con len(texto) // 4, precio fijo, aritmética entera. La lección 03 es explícita sobre qué se pierde al no usar un tokenizer real, y por qué esa pérdida es un costo aceptable para esta guía.
2. "Si cost_cents da 0, algo está roto"
No — es, con frecuencia, la respuesta correcta y honesta para un run de este tamaño. La lección 05 lo confirma ejecutado: un run típico de Reservo, con unas pocas docenas de tokens de entrada y salida, cuesta una fracción de centavo tan pequeña que la división entera la redondea a 0. Eso no es un error del código — es la realidad de la aritmética entera con int, y la lección 06 muestra exactamente por qué ese 0 sigue siendo información real, no ruido.
3. "El costo de entrada y el de salida son, más o menos, lo mismo"
La lección 04 lo confirma con números: el precio de salida de claude-sonnet-5 es cinco veces el de entrada ($15.00 contra $3.00 por millón de tokens). Esa asimetría no es un detalle — cambia qué parte de un run vale la pena vigilar de cerca.
4. "Medir el costo de un run es el primer paso para bajarlo"
No en esta guía. Este módulo se detiene, con precisión, en "cuánto costó este run y por qué" — nunca en "cómo bajarlo". Prompt caching, selección de modelo por costo, batching: eso es cost-optimization-caching-guide, nombrada con precisión en la lección 07. Confundir medir con optimizar es el error de frontera más común de todo este módulo.
5. "Con el costo ya resuelto, el Módulo 4 no tiene nada nuevo que agregar"
El costo y la latencia son dos señales operacionales distintas, calculadas sobre datos distintos, con distinta aritmética. Este módulo nunca mide cuánto tardó un run —eso es, con precisión, el trabajo del Módulo 4, que llega después y que no usa ningún cronómetro real, exactamente por la misma razón honesta que esta guía ya adelantó en el Módulo 1.
Cómo trabajar este módulo
- Corre cada ejemplo tú mismo, con la calculadora al lado. La aritmética de este módulo es simple —multiplicaciones y divisiones enteras—, pero confirmar a mano que
estimate_cost_centsda lo que esperas es la mejor forma de que la fórmula se te quede grabada. - No confundas "estimado" con "inventado". Cuando una lección dice que el costo está estimado, no significa que el número sea arbitrario — significa que depende de una aproximación declarada (
len(texto) // 4) y un precio fijo, nunca de una llamada real a la API. - El mini-proyecto (lección 08) es la síntesis. Ahí vas a correr
observability/cost_calculator.pycompleto sobre un lote real de runs de Reservo, correlacionados por sutrace_id, con un reporte de costo integral.
Tiempo estimado:
Lección 01 (esta) → 20 min lectura
Lección 02 → 20 min lectura
Lección 03 → 25 min + correr el ejemplo
Lección 04 → 20 min + correr el ejemplo
Lección 05 → 30 min + correr el ejemplo
Lección 06 → 30 min + correr el ejemplo
Lección 07 → 25 min + correr el ejemplo
Lección 08 → 35 min + armar el mini-proyecto completo
Total: ~3.5 horas
Evidencia de éxito
Antes de avanzar al Módulo 4 (Medir latencia con honestidad), deberías poder:
- ✅ Explicar qué es un token y por qué es la unidad de costo de un modelo de lenguaje, no el carácter ni la palabra.
- ✅ Calcular
estimate_tokens(text)a mano sobre un texto corto, y confirmarlo con código. - ✅ Citar el pricing de
claude-sonnet-5de memoria: $3.00/$15.00 por millón de tokens de entrada/salida, precio de lista. - ✅ Calcular el costo en centavos de un run real de Reservo, con su desglose por tool call.
- ✅ Escalar el costo de un run a 1.000, 10.000 y 100.000 runs, sumando tokens antes de redondear.
- ✅ Trazar la frontera entre medir costo (esta guía) y reducir costo (
cost-optimization-caching-guide).
Resumen
- Este módulo mide el costo por run del agente de Reservo, sobre la misma correlación por
trace_idque el Módulo 2 ya construyó — sin volver a tocar el loop ni el logger. - Confirmamos, ejecutado, que
RUN_LOG.jsonldel Módulo 2 —completo, correlacionado, con ocho líneas para un run limpio— no tiene ningún campo de costo. Ese es, con precisión, el límite que este módulo cierra. - La analogía central es el medidor de luz: un run cuesta una fracción de centavo, insignificante por sí solo, pero multiplicado por miles de usuarios es una factura real — medirlo por run es leer el medidor antes de que llegue esa factura.
- Las piezas del módulo:
estimate_tokens(L03), el pricing fijo (L04),estimate_cost_cents+cost_for_runcon desglose (L05), el escalado a miles de runs (L06), el costo como señal operacional (L07), y el reporte completo del mini-proyecto (L08).
Siguiente lección: 02 — Los tokens son la unidad de costo. Antes de escribir la fórmula, respondemos la pregunta de fondo: ¿qué es exactamente un token, y por qué un proveedor de LLM cobra por esa unidad y no por otra?
Recursos adicionales
- Anthropic — Pricing — La fuente del precio de lista de
claude-sonnet-5que la lección 04 fija como constante. - Anthropic — Token counting — Cómo cuenta tokens la API de Claude de verdad, la referencia contra la que la lección 03 confronta la estimación
len // 4de esta guía. - Python —
dataclasses—CostReportyStepCost, las estructuras que este módulo construye desde la lección 05. - Anthropic — Building effective agents — Sobre por qué medir el costo de un sistema agentic es una disciplina operacional propia, no un detalle de facturación.
- Python 3.14 — What's New — La versión con la que se ejecuta toda la aritmética de este módulo.