Módulo 3: Medir costo y tokens por run

Escalando el costo a miles de runs

Descripción

Cada CostReport que calculaste en la lección 05 terminó en lo mismo: cost_cents = 0. No es un error — es la respuesta honesta para un run de este tamaño, la misma conclusión a la que ya llegó el Módulo 1 con run_and_observe. Un run de Reservo mueve, típicamente, entre cincuenta y ciento veinte tokens en total; al precio de lista de claude-sonnet-5, eso es una fracción de centavo tan pequeña que la aritmética entera la redondea, sin remedio, a cero.

Esta lección retoma la analogía del medidor de luz de la lección 01: una plancha encendida media hora "casi no cuesta nada", mirada sola — pero un edificio con cientos de planchas, todos los días, sí genera una factura real. Esta lección hace ese mismo salto con el agente de Reservo: de un run que cuesta 0 centavos a la proyección de lo que costarían 1.000, 10.000 y 100.000 runs del mismo tipo — y confirma, con números reales, por qué la única forma correcta de hacer ese salto es sumar tokens antes de redondear, nunca sumar cifras de centavos que ya fueron redondeadas por separado.

Conexión con el módulo

Esta lección agrega la agregación por lote a observability/cost_calculator.py: una función que suma los tokens de varios CostReport y aplica estimate_cost_cents una sola vez sobre el total, más el patrón de proyección a escala que reusa la misma fórmula de la lección 05 sin ningún cambio.


Agregando un lote real: cuatro runs, un total honesto

Antes de proyectar nada hacia el futuro, agrega el costo de un lote de runs que sí corrieron, de verdad, en esta lección. Cuatro tareas distintas de Reservo, cada una con su propio trace_id:

import logging
import reservo_agent as ra
import run_logger as rl

rl.logger.setLevel(logging.CRITICAL)  # silenciamos el detalle de traced_run para este resumen

script_a = [  # Ana: Focus pro 3h, con un tier inválido corregido en el camino
    {"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": "premium", "hours": 3}}]},
    {"stop_reason": "tool_use", "content": [
        {"type": "tool_use", "id": "toolu_03", "name": "get_quote",
         "input": {"room": "Focus", "tier": "pro", "hours": 3}}]},
    {"stop_reason": "tool_use", "content": [
        {"type": "tool_use", "id": "toolu_04", "name": "book_room",
         "input": {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana"}}]},
    {"stop_reason": "end_turn", "content": [
        {"type": "text", "text": "Reservé Focus pro por 3 horas para Ana. Total $60.00. Confirmación #1."}]},
]
script_b = [  # Sofía: Boardroom pro 1h, limpio
    {"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": "Boardroom", "tier": "pro", "hours": 1}}]},
    {"stop_reason": "tool_use", "content": [
        {"type": "tool_use", "id": "toolu_03", "name": "book_room",
         "input": {"room": "Boardroom", "tier": "pro", "hours": 1, "member": "Sofía"}}]},
    {"stop_reason": "end_turn", "content": [
        {"type": "text", "text": "Reservé Boardroom pro por 1 hora para Sofía. Total $64.00. Confirmación #2."}]},
]
script_d = [  # Diego: reserva y cancela, dos pasos
    {"stop_reason": "tool_use", "content": [
        {"type": "tool_use", "id": "toolu_01", "name": "book_room",
         "input": {"room": "Studio", "tier": "basic", "hours": 1, "member": "Diego"}}]},
    {"stop_reason": "tool_use", "content": [
        {"type": "tool_use", "id": "toolu_02", "name": "cancel_booking", "input": {"id": 4}}]},
    {"stop_reason": "end_turn", "content": [
        {"type": "text", "text": "Reservé y luego cancelé Studio basic 1h para Diego."}]},
]
# Carla: compara las seis combinaciones de sala/tier antes de decidir -- el run más largo del lote.
rooms, tiers = ["Focus", "Studio", "Boardroom"], ["basic", "pro"]
script_compare = [{"stop_reason": "tool_use", "content": [
    {"type": "tool_use", "id": "toolu_00", "name": "list_rooms", "input": {}}]}]
for i, (room, tier) in enumerate([(r, t) for r in rooms for t in tiers], start=1):
    script_compare.append({"stop_reason": "tool_use", "content": [
        {"type": "tool_use", "id": f"toolu_{i:02d}", "name": "get_quote",
         "input": {"room": room, "tier": tier, "hours": 2}}]})
script_compare.append({"stop_reason": "tool_use", "content": [
    {"type": "tool_use", "id": "toolu_99", "name": "book_room",
     "input": {"room": "Boardroom", "tier": "pro", "hours": 2, "member": "Carla"}}]})
script_compare.append({"stop_reason": "end_turn", "content": [
    {"type": "text", "text": "Comparé las seis combinaciones de sala y tier. Reservé Boardroom pro por 2 horas para Carla. Total $128.00. Confirmación #1."}]})

tasks = [
    ("Reserva Focus pro 3h para Ana", script_a),
    ("Reserva Boardroom pro 1h para Sofía", script_b),
    ("Reserva y cancela Studio basic 1h para Diego", script_d),
    ("Compara todas las salas antes de reservar la mejor opción para Carla", script_compare),
]

reports = []
for i, (question, script) in enumerate(tasks, start=1):
    with rl.traced_run(question, i) as trace_id:
        final, history = ra.run_reservo_agent(question, script)
    reports.append(cost_for_run(trace_id, question, history))

print(f"{'trace_id':<18} {'in':>4} {'out':>4} {'total':>6} {'cost_cents':>11}  pregunta")
for r in reports:
    total = r.input_tokens + r.output_tokens
    print(f"{r.trace_id:<18} {r.input_tokens:>4} {r.output_tokens:>4} {total:>6} {r.cost_cents:>11}  {r.question!r}")

Qué esperar:

trace_id             in  out  total  cost_cents  pregunta
run-8487582448eb     64   56    120           0  'Reserva Focus pro 3h para Ana'
run-ae6ff85cf0b0     53   49    102           0  'Reserva Boardroom pro 1h para Sofía'
run-2c27934d8a39     27   31     58           0  'Reserva y cancela Studio basic 1h para Diego'
run-cecde864aa84     88  118    206           0  'Compara todas las salas antes de reservar la mejor opción para Carla'

Cuatro runs, cuatro trace_id distintos, y cuatro veces cost_cents = 0 — cada uno, individualmente, honesto. Ahora agrega el lote completo:

def aggregate_reports(reports):
    """Suma los tokens de TODOS los reports primero, y aplica
    estimate_cost_cents UNA SOLA VEZ sobre el total -- nunca suma
    cost_cents individuales, ya redondeados."""
    total_input = sum(r.input_tokens for r in reports)
    total_output = sum(r.output_tokens for r in reports)
    return total_input, total_output, estimate_cost_cents(total_input, total_output)


total_in, total_out, total_cost = aggregate_reports(reports)
print("input_tokens totales :", total_in)
print("output_tokens totales:", total_out)
print("costo total del lote :", total_cost, "centavos")

Qué esperar:

input_tokens totales : 232
output_tokens totales: 254
costo total del lote : 0 centavos

Todavía 0 — cuatro runs siguen siendo muy pocos tokens acumulados (232 + 254 = 486) para cruzar el umbral donde la aritmética entera empieza a producir algo distinto de cero (recuerda la lección 04: hacen falta números de miles o millones de tokens para que // 1_000_000 deje de aplastarlo todo a 0). Este resultado sigue siendo correcto y honesto — el salto real viene a continuación.


De cuatro runs a cien mil: la proyección

El agente de Reservo, en producción, no atiende cuatro tareas por día — atiende miles. Toma el run más representativo del lote —el de Ana, 64 tokens de entrada y 56 de salida— y proyecta su costo si ese mismo patrón se repitiera 1.000, 10.000 y 100.000 veces:

per_run_input, per_run_output = 64, 56  # el run de Ana, del ejemplo trabajado

print(f"{'runs':>9} {'input_tokens':>13} {'output_tokens':>14} {'cost_cents':>11} {'dólares':>10}")
for n in (1, 10, 100, 1_000, 10_000, 100_000):
    input_tokens = per_run_input * n
    output_tokens = per_run_output * n
    cost = estimate_cost_cents(input_tokens, output_tokens)
    print(f"{n:>9} {input_tokens:>13} {output_tokens:>14} {cost:>11} {f'${cost/100:.2f}':>10}")

Qué esperar:

     runs  input_tokens  output_tokens  cost_cents    dólares
        1            64             56           0      $0.00
       10           640            560           1      $0.01
      100          6400           5600          10      $0.10
     1000         64000          56000         103      $1.03
    10000        640000         560000        1032     $10.32
   100000       6400000        5600000       10320    $103.20

Ahí está el salto completo de la analogía: un run cuesta $0.00 según esta aritmética —una plancha encendida media hora—, pero 100.000 runs del mismo tipo cuestan $103.20 — la factura del edificio completo, a fin de mes. Fíjate también en la fila de 10 runs: es la primera donde el costo deja de ser 0 (1 centavo) — el punto exacto donde la fracción diminuta de un run individual empieza a acumularse en algo que la aritmética entera ya puede representar.


El error real: sumar centavos ya redondeados, en vez de tokens

Esta es la razón por la que la lección 03 insistió, con ejemplos pequeños, en la diferencia entre sumar fragmentos redondeados y concatenar antes de redondear. Aquí esa diferencia deja de ser una curiosidad y se convierte en un error de presupuesto real. Compara las dos formas de proyectar el costo de 1.000, 10.000 y 100.000 runs del tipo de Ana:

per_run_cost = estimate_cost_cents(per_run_input, per_run_output)
print("costo de UN run, redondeado:", per_run_cost, "centavos")
print()
print(f"{'runs':>9} {'naive (cost_cents * n)':>24} {'correcto (tokens * n primero)':>32}")
for n in (1_000, 10_000, 100_000):
    naive_total = per_run_cost * n
    correct_total = estimate_cost_cents(per_run_input * n, per_run_output * n)
    print(f"{n:>9} {naive_total:>24} {correct_total:>32}")

Qué esperar:

costo de UN run, redondeado: 0 centavos

     runs   naive (cost_cents * n)   correcto (tokens * n primero)
     1000                        0                              103
    10000                        0                             1032
   100000                        0                            10320

El método "naive" —tomar el costo ya redondeado de un run (0 centavos) y multiplicarlo por la cantidad de runs— predice, en los tres casos, que el costo total es $0.00, sin importar cuántos runs se proyecten. Es aritméticamente correcto (0 * n siempre da 0), pero es una respuesta falsa sobre el presupuesto real: el método correcto —multiplicar los tokens por n primero, y aplicar estimate_cost_cents una sola vez sobre ese total— muestra que 100.000 runs de este tipo sí cuestan $103.20, una cifra que cualquier presupuesto real necesita conocer. La causa exacta es la misma que ya viste en la lección 03: cada redondeo hacia abajo de un run individual descarta una fracción de centavo que, aislada, parece irrelevante, pero que la multiplicación por miles vuelve significativa. Redondear antes de escalar pierde esa información para siempre; redondear después de escalar la conserva.


Errores comunes

  1. Reportar "este run cuesta $0.00, así que no importa" sin proyectar a escala. El ejemplo de esta lección lo desmiente directamente: $0.00 por run individual y $103.20 por cada 100.000 runs son la misma información, vista a dos escalas distintas — ninguna de las dos es "la verdad" sin la otra.

  2. Multiplicar el cost_cents ya calculado de un CostReport por la cantidad de runs esperada. Es, con precisión, el error "naive" de esta lección. La forma correcta es multiplicar input_tokens/output_tokens por n, y aplicar estimate_cost_cents una sola vez sobre esos totales escalados.

  3. Asumir que todos los runs de un lote real tienen exactamente los mismos tokens que el run usado para proyectar. El ejemplo trabajado usó el run de Ana (120 tokens totales) como representativo, pero el lote real tenía runs desde 58 hasta 206 tokens. Una proyección seria usa el promedio del lote —o, mejor, agrega los tokens reales del lote completo y escala ese total— en vez de un solo run elegido a mano.

  4. Confundir "agregar un lote" (aggregate_reports, sobre runs que sí ocurrieron) con "proyectar a escala" (multiplicar tokens de un run representativo por n). Son dos operaciones relacionadas pero distintas: la primera suma tokens reales de runs que ya corrieron; la segunda multiplica tokens de un run (real o promedio) por una cantidad hipotética de repeticiones futuras. Mezclarlas sin aclarar cuál se está haciendo es una fuente fácil de confusión al leer un reporte.

  5. Pensar que este escalado ya es "predecir el costo de producción". Es una proyección aritmética simple, útil para tener una cifra de orden de magnitud —de nuevo, la misma honestidad de la lección 03—, no un modelo de tráfico real. La cantidad real de runs por día, la mezcla real de tareas simples y complejas, y la variabilidad real de cada conversación son preguntas de negocio que esta guía no intenta responder — el Módulo 8 va a mostrar cómo este mismo cálculo se aplica sobre datos reales de un lote más grande, sin pretender que sustituye una proyección de negocio completa.


Ejercicios

Ejercicio 1: Proyecta el costo del run de Sofía a 1.000 runs (Fácil)

Usando los tokens del run de Sofía (53 de entrada, 49 de salida, del ejemplo trabajado de esta lección), calcula el costo proyectado de 1.000 runs de ese mismo tipo.

Ver solución
print(estimate_cost_cents(53 * 1_000, 49 * 1_000))

Salida esperada:

89

Explicación: 89 centavos ($0.89) por 1.000 runs del tipo de Sofía — menos que los 103 centavos del run de Ana a la misma escala (lección, ejemplo trabajado), consistente con que el run de Sofía usa menos tokens en total (102 contra 120).

Ejercicio 2: Confirma que el método naive nunca detecta el costo de Diego, ni a 50.000 runs (Medio)

El run de Diego (27 tokens de entrada, 31 de salida) es el más barato del lote. Calcula su costo individual redondeado, y compara el método naive contra el correcto para 50.000 runs de ese tipo.

Ver solución
per_run_cost_diego = estimate_cost_cents(27, 31)
naive_50k = per_run_cost_diego * 50_000
correct_50k = estimate_cost_cents(27 * 50_000, 31 * 50_000)

print("costo de UN run de Diego (redondeado):", per_run_cost_diego, "centavos")
print("naive a 50.000 runs   :", naive_50k, "centavos")
print("correcto a 50.000 runs:", correct_50k, "centavos =", f"${correct_50k/100:.2f}")

Salida esperada:

costo de UN run de Diego (redondeado): 0 centavos
naive a 50.000 runs   : 0 centavos
correcto a 50.000 runs: 2730 centavos = $27.30

Explicación: el método naive vuelve a fallar por completo —0 * 50.000 = 0—, mientras el método correcto revela $27.30 reales a esa escala. El run de Diego, siendo el más barato del lote, sigue produciendo una cifra de presupuesto real una vez multiplicado por decenas de miles — la lección central de este ejercicio es que ningún run, por barato que parezca de forma aislada, puede descartarse del presupuesto sin proyectarlo primero.

Ejercicio 3: Encuentra a partir de qué cantidad de runs de Diego el costo correcto deja de ser 0 (Difícil)

Usando los tokens del run de Diego (27/31), escribe un bucle que encuentre el primer valor de n (empezando en 1) para el cual estimate_cost_cents(27 * n, 31 * n) deja de ser 0. Confirma tu resultado calculando el costo en n - 1 y en n.

Ver solución
n = 1
while estimate_cost_cents(27 * n, 31 * n) == 0:
    n += 1

print("primer n con costo > 0:", n)
print(f"costo en n={n - 1}:", estimate_cost_cents(27 * (n - 1), 31 * (n - 1)), "centavos")
print(f"costo en n={n}    :", estimate_cost_cents(27 * n, 31 * n), "centavos")

Salida esperada:

primer n con costo > 0: 19
costo en n=18: 0 centavos
costo en n=19: 1 centavos

Explicación: hacen falta 19 repeticiones del run de Diego —el más barato del lote— para que la aritmética entera deje de aplastar el costo a 0. Esto confirma, con un número concreto, por qué proyectar a "miles de runs" (lección 06) y no a "un puñado de runs" es necesario para que el costo se vuelva visible: por debajo de ese umbral, cualquier reporte de costo —individual o de un lote pequeño— seguirá mostrando honestamente $0.00, sin que eso signifique que el sistema no tiene costo real.


Resumen y siguiente paso

  • Agregamos un lote real de cuatro runs de Reservo con aggregate_reports: sumar tokens primero, aplicar estimate_cost_cents una sola vez — 232 tokens de entrada, 254 de salida, 0 centavos, un resultado todavía honesto a esta escala.
  • Proyectamos el costo del run de Ana a 1.000, 10.000 y 100.000 runs: de $0.00 a $103.20 — la analogía del medidor de luz, confirmada con aritmética real.
  • Confirmamos, ejecutado, el error central de este escalado: multiplicar un cost_cents ya redondeado por la cantidad de runs siempre predice $0.00, sin importar la escala; multiplicar los tokens primero y redondear al final revela el costo real.
  • El Ejercicio 3 cuantificó el umbral exacto: hacen falta 19 repeticiones del run más barato del lote para que la aritmética entera deje de mostrar 0 — la razón de fondo por la que esta guía habla de "miles de runs", no de un puñado.

Siguiente lección: 07 — El costo como señal operacional. Con la agregación y el escalado ya resueltos, tratamos el costo como una señal más, 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 exacta con la guía que enseña a reducirlo.


Recursos adicionales

  1. Python — sum() — La función usada en aggregate_reports para sumar tokens de varios CostReport antes de aplicar el pricing.
  2. Python — operadores aritméticos (//, *) — La base exacta de por qué sumar antes de redondear da un resultado distinto —y más correcto— que redondear antes de sumar.
  3. Anthropic — Pricing — La fuente del precio de lista de claude-sonnet-5, reusado sin cambios desde la lección 04 en cada cálculo de esta lección.
  4. Anthropic — Building effective agents — Sobre por qué proyectar el costo de un sistema agentic a escala de producción, no solo medirlo por run, es parte de operarlo con responsabilidad.
  5. Python 3.14 — What's New — La versión con la que se ejecutó cada línea de código de esta lección.