Módulo 3: Medir costo y tokens por run

Estimando tokens con `len // 4`

Descripción

La lección anterior dejó clara la unidad —el token—, y por qué es la que un modelo de lenguaje factura. Esta lección construye la primera pieza real de observability/cost_calculator.py: estimate_tokens(text), la función que esta guía entera va a usar, sin excepción, para convertir un fragmento de texto en una cantidad de tokens. La fórmula es deliberadamente simple —len(texto) // 4— y el objetivo de esta lección no es solo mostrarla, sino justificarla con precisión: de dónde sale ese 4, qué tan cerca está de un tokenizer real, y en qué casos se equivoca de forma sistemática.

Esta es, con toda honestidad, una aproximación. Nunca reemplaza a un tokenizer real como el que usa claude-sonnet-5 internamente. Cada lección de esta guía que use estimate_tokens lo va a rotular como una estimación de orden de magnitud — suficiente para razonar sobre el costo relativo de distintos runs, insuficiente para una factura exacta.

Conexión con el módulo

Esta lección entrega la primera función real de observability/cost_calculator.py: estimate_tokens. La lección 04 agrega el pricing; la lección 05 combina ambas en estimate_cost_cents y cost_for_run. Todo lo que sigue en este módulo —y, según el DISEÑO de esta guía, en los módulos que vienen después— reusa estimate_tokens sin cambiarla.


De dónde sale el 4: una aproximación conocida, no un número inventado

La convención len(texto) // 4 no nace en esta guía — es una regla general, ampliamente citada en la documentación de proveedores de modelos de lenguaje, para textos en inglés: en promedio, un token equivale, aproximadamente, a cuatro caracteres. No es una ley física ni una garantía —la lección 02 ya lo confirmó con el ejemplo del tool_result en JSON, con una proporción de caracteres por token distinta a la de la prosa—, es un promedio útil cuando no tienes acceso al tokenizer real y necesitas una cifra de orden de magnitud, rápida de calcular, sin dependencias externas.

Esta guía hereda esa convención de agent-fundamentals-and-tool-calling-guide y de context-engineering-guide —las mismas guías que ya la usaron para razonar sobre presupuestos de contexto—, y la convierte, aquí, en la primera vez que se usa para calcular dinero de verdad.

def estimate_tokens(text):
    """Estimación de ORDEN DE MAGNITUD: len(texto) // 4. NUNCA un conteo
    exacto de un tokenizer real (como el que usa claude-sonnet-5
    internamente) -- una aproximación declarada, usada en toda esta guía
    porque no hay acceso a un tokenizer real sin llamar a la API."""
    return len(text) // 4

División entera (//), no división de punto flotante — el mismo estilo aritmético que ya usaste para el descuento pro de Reservo (base * 80 // 100) y que vas a usar en cada cálculo de costo del resto de esta guía. El resultado siempre es un int, nunca un decimal.


Ejemplo trabajado: estimate_tokens sobre texto real de Reservo

Antes de calcular ningún costo, confirma la función sobre una variedad de fragmentos reales, del más corto al más largo:

import json
import reservo_tools as rt

samples = {
    "vacío": "",
    "un carácter": "x",
    "tres caracteres": "xyz",
    "cuatro caracteres": "wxyz",
    "pregunta corta": "Reserva Focus pro 3h para Ana",
    "get_quote result": json.dumps({"price_cents": 6000}),
    "list_rooms result": json.dumps(rt.list_rooms()),
    "respuesta final": "Reservé Focus pro por 3 horas para Ana. Total $60.00. Confirmación #1.",
}
for label, text in samples.items():
    print(f"{label:<20} len={len(text):>4}  estimate_tokens={estimate_tokens(text):>4}")

Qué esperar:

vacío                len=   0  estimate_tokens=   0
un carácter          len=   1  estimate_tokens=   0
tres caracteres      len=   3  estimate_tokens=   0
cuatro caracteres    len=   4  estimate_tokens=   1
pregunta corta       len=  29  estimate_tokens=   7
get_quote result     len=  21  estimate_tokens=   5
list_rooms result    len= 122  estimate_tokens=  30
respuesta final      len=  70  estimate_tokens=  17

Detente en las primeras cuatro filas, porque revelan el límite más importante de esta convención: cualquier texto de menos de cuatro caracteres se estima en 0 tokens, sin excepción. "x", "xyz" —tres caracteres, casi un token real en cualquier tokenizer que exista— se redondean, con la división entera, exactamente a 0. Esto no es un bug: es la consecuencia directa y esperada de //, y la razón por la que esta guía nunca llama a esta estimación "exacta". Recién con "wxyz" —cuatro caracteres— el estimado deja de ser 0.


Confirmando el límite con texto real de Reservo: nombres cortos

El caso de "x" puede parecer artificial. No lo es — Reservo trabaja con nombres de personas todo el tiempo, y varios de los nombres que ya viste en esta guía ("Ana", "Luis") tienen menos de cuatro caracteres:

nombres = ["Ana", "Luis", "Nina", "Rui", "Omar", "Carla", "Sofía"]
for nombre in nombres:
    print(f"{nombre!r:<10} len={len(nombre)}  estimate_tokens={estimate_tokens(nombre)}")

Qué esperar:

'Ana'      len=3  estimate_tokens=0
'Luis'     len=4  estimate_tokens=1
'Nina'     len=4  estimate_tokens=1
'Rui'      len=3  estimate_tokens=0
'Omar'     len=4  estimate_tokens=1
'Carla'    len=5  estimate_tokens=1
'Sofía'    len=5  estimate_tokens=1

"Ana" y "Rui" —dos de los nombres reales que aparecen en el guion canónico de esta guía— se estiman en 0 tokens cada uno, cuando cualquier tokenizer real los contaría como, al menos, un token. Esta es la razón exacta por la que esta guía nunca calcula el costo de un fragmento diminuto aislado —como un solo nombre— y siempre lo hace sobre el texto acumulado de un run completo, donde ese error de redondeo se vuelve proporcionalmente menos importante. La lección 06 va a confirmar, con números reales, por qué sumar el texto antes de aplicar // 4 es mejor que aplicar // 4 a cada fragmento por separado y sumar después.


Qué se pierde exactamente frente a un tokenizer real

Vale la pena ser preciso sobre las dos formas en que esta estimación se aleja de un conteo real, porque cada lección que sigue de esta guía hereda ambas limitaciones sin volver a mencionarlas:

  1. Es ciega al contenido, solo mira la longitud. El Ejercicio 3 de la lección 02 ya lo confirmó: un texto sin sentido y una frase real de la misma longitud producen el mismo estimado. Un tokenizer real distingue palabras conocidas de secuencias raras; len(texto) // 4 no distingue nada.
  2. Es ciega al idioma. La regla de "cuatro caracteres por token" se calibró, históricamente, sobre texto en inglés. El español —con tildes, con palabras que suelen ser más largas que sus equivalentes en inglés— no necesariamente sigue la misma proporción en un tokenizer real. Esta guía sigue usando // 4 de forma uniforme, en español e inglés, precisamente porque es una estimación de orden de magnitud, no una calibrada por idioma — y lo dice, aquí, con todas sus letras.

Ninguna de las dos limitaciones invalida el uso que esta guía le da a la función: razonar sobre el costo relativo de distintos runs (¿cuál costó más?, ¿por qué?), y calcular un orden de magnitud del costo total, sin necesitar una llamada real a la API solo para contar tokens.


Errores comunes

  1. Llamar a esta función "el conteo de tokens" sin el calificativo "estimado". Cada mención de estimate_tokens en esta guía —en el código, en la prosa— existe, precisamente, para que nunca se lea como un conteo exacto. El nombre de la función ya lo dice: estimate_, no count_.

  2. Aplicar la estimación a un fragmento diminuto y confiar en el resultado. Como confirmó el ejemplo de los nombres, "Ana" y "Rui" se estiman en 0 tokens — un resultado técnicamente correcto según la fórmula, pero engañoso si se lee como "este nombre no cuesta nada". El costo real de un run nunca se calcula sobre un fragmento aislado tan corto — siempre sobre el texto acumulado, como muestra la lección 05.

  3. Usar división de punto flotante (/) en vez de división entera (//). len(texto) / 4 da un float (por ejemplo, 7.25) — un número de tokens fraccionario no tiene sentido, porque un token es una unidad discreta. Esta guía usa // en cada lugar donde se estima una cantidad de tokens, sin excepción, la misma disciplina de aritmética entera que ya viste en el pricing de Reservo.

  4. Pensar que instalar tiktoken o un tokenizer real "arreglaría" esta guía. No es el objetivo. La regla dura del DISEÑO de esta guía prohíbe cualquier dependencia externa para esto, precisamente porque el punto de esta guía no es contar tokens con precisión perfecta —eso es un problema ya resuelto por librerías existentes—, sino aprender a razonar sobre costo, con una aproximación honesta y reproducible sin instalar nada.

  5. Sumar estimados de fragmentos individuales cuando se puede concatenar el texto primero. El ejemplo de los nombres cortos ya insinuó el problema: cada fragmento diminuto pierde información al redondear hacia abajo por separado. La lección 06 lo demuestra con números concretos, sobre un caso real de escalado.


Ejercicios

Ejercicio 1: Estima los tokens de tres textos de Reservo, a mano primero (Fácil)

Antes de ejecutar nada, calcula a mano el estimado de tokens de: (a) "Cancela la reserva 5" (21 caracteres), (b) "{\"cancelled\": true}" (19 caracteres), (c) "Studio" (6 caracteres). Después, confirma con estimate_tokens.

Ver solución

(a) 21 // 4 = 5. (b) 19 // 4 = 4. (c) 6 // 4 = 1.

for text in ["Cancela la reserva 5", '{"cancelled": true}', "Studio"]:
    print(f"{text!r:<25} len={len(text):>3}  estimate_tokens={estimate_tokens(text)}")

Salida esperada:

'Cancela la reserva 5'    len= 21  estimate_tokens=5
'{"cancelled": true}'     len= 19  estimate_tokens=4
'Studio'                  len=  6  estimate_tokens=1

Explicación: los tres cálculos a mano coinciden exactamente con la ejecución — la fórmula es determinista y no tiene ninguna sorpresa una vez que conoces la longitud exacta del texto.

Ejercicio 2: Encuentra el texto más largo que sigue estimándose en 0 tokens (Medio)

Sin ejecutar nada primero: ¿cuál es la longitud máxima, en caracteres, que un texto puede tener y seguir estimándose en 0 tokens con len(texto) // 4? Confirma tu respuesta probando esa longitud exacta y la longitud inmediatamente superior.

Ver solución

0 tokens significa len(texto) // 4 == 0, lo que ocurre para cualquier longitud de 0 a 3 caracteres —4 // 4 ya da 1—. La longitud máxima que sigue en 0 es 3.

print("3 caracteres:", estimate_tokens("abc"))     # 3 // 4 = 0
print("4 caracteres:", estimate_tokens("abcd"))     # 4 // 4 = 1

Salida esperada:

3 caracteres: 0
4 caracteres: 1

Explicación: la división entera por 4 produce un cambio de resultado exactamente cada 4 caracteres — 0-3 da 0, 4-7 da 1, 8-11 da 2, y así sucesivamente. Cualquier nombre de tres letras o menos —como "Ana" o "Rui", ya confirmados arriba— cae en el primer bloque, el único que se estima en 0.

Ejercicio 3: Cuantifica la pérdida de sumar por fragmento en vez de concatenar (Difícil)

Toma esta lista de ocho nombres de miembros de Reservo: ["Ana", "Sofia", "Diego", "Luis", "Carla", "Marta", "Nina", "Omar"]. Calcula el estimado de tokens de dos formas: (a) aplicando estimate_tokens a cada nombre por separado y sumando los ocho resultados, (b) concatenando los ocho nombres en un solo string y aplicando estimate_tokens una sola vez. Compara ambos resultados y explica la diferencia.

Ver solución
fragments = ["Ana", "Sofia", "Diego", "Luis", "Carla", "Marta", "Nina", "Omar"]

per_fragment_sum = sum(estimate_tokens(f) for f in fragments)
concatenated = estimate_tokens("".join(fragments))

print("longitudes individuales:", [len(f) for f in fragments])
print("longitud total          :", sum(len(f) for f in fragments))
print("(a) suma por fragmento  :", per_fragment_sum)
print("(b) concatenado una vez :", concatenated)

Salida esperada:

longitudes individuales: [3, 5, 5, 4, 5, 5, 4, 4]
longitud total          : 35
(a) suma por fragmento  : 7
(b) concatenado una vez : 8

Explicación: la forma (a) subestima —7 contra 8— porque cada fragmento redondea hacia abajo por separado, y esos restos perdidos ("Ana" pierde 3 caracteres enteros por caer bajo el umbral del Ejercicio 2, y cada uno de los demás fragmentos pierde entre 0 y 3 caracteres de resto) nunca se recuperan. La forma (b) concatena primero, así que solo hay una operación de redondeo al final, sobre la longitud total —35 // 4 = 8—, perdiendo como máximo 3 caracteres en total, no 3 por cada uno de los ocho fragmentos. Esta diferencia, pequeña aquí, es exactamente el mecanismo que la lección 06 va a mostrar a una escala donde sí importa: sumar tokens (o texto) antes de redondear, nunca sumar valores ya redondeados.


Resumen y siguiente paso

  • Construimos estimate_tokens(text): len(texto) // 4, la primera función real de observability/cost_calculator.py, heredada de la convención ya establecida en agent-fundamentals y context-engineering.
  • Confirmamos, ejecutado, sobre ocho fragmentos reales de Reservo, que es una estimación de orden de magnitud — nunca un conteo exacto—, y que textos de tres caracteres o menos (incluidos nombres reales como "Ana" y "Rui") se estiman siempre en 0 tokens.
  • Nombramos, con precisión, las dos limitaciones que esta guía acepta a propósito: es ciega al contenido, y no está calibrada por idioma.
  • Confirmamos, con números reales, que sumar estimados de fragmentos pequeños por separado pierde más precisión que concatenar el texto y estimar una sola vez — el argumento que la lección 06 retoma a escala de miles de runs.

Siguiente lección: 04 — El pricing de claude-sonnet-5. Con estimate_tokens ya construida y probada, fijamos la segunda pieza que hace falta para calcular dinero: el precio, en centavos, de cada millón de tokens de entrada y de salida.


Recursos adicionales

  1. Anthropic — Token counting — Cómo cuenta tokens la API de Claude de verdad; el conteo exacto contra el que esta lección confronta su propia estimación.
  2. Python — operadores aritméticos (//) — La división entera, la base exacta de estimate_tokens y de cada cálculo de costo del resto de esta guía.
  3. Python — len() — La función que mide la longitud de cualquier string en Python, el único insumo real de esta estimación.
  4. Anthropic — Building effective agents — Sobre por qué razonar sobre el costo relativo de un sistema agentic, incluso con una estimación aproximada, es más valioso que no medirlo en absoluto.
  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.