Module 3: Measuring Cost and Tokens per Run

Tokens Are the Unit of Cost

Description

Before writing a single cost formula, it's worth answering a question lesson 01 left pending: exactly what unit measures what a Reservo agent run costs? It isn't dollars directly — those are the final result of a calculation — and it isn't "one call" or "one second of compute," even though both ideas seem reasonable at first glance. The real unit, the one claude-sonnet-5 and practically any language-model provider uses, is the token. This lesson explains what it is, why that's the unit that got chosen, and why it isn't the same as a character or a word — even though all three measure, in different ways, the same question: "how much text is here?"

This lesson doesn't run any cost formula yet — that starts in lesson 05. What it does is put the exact vocabulary in place, with examples run over real Reservo text, so lesson 03 can build estimate_tokens on solid ground.

Connection to the module

This lesson doesn't add code to observability/cost_calculator.py — that starts in lesson 03. It's the conceptual foundation for everything that follows: without being clear on what a token is and why it's counted the way it is, lesson 03's len(text) // 4 formula would sound like an arbitrary trick, instead of what it really is — a reasoned approximation of a real unit.


Analogy: you don't bill for the paper, you bill for the ink used

When a print shop quotes a job, it doesn't charge by the number of sheets you handed it to read, nor by the minutes the machine took to run — it charges, precisely, by the amount of ink the job consumed: how much ink goes into printing the document you gave it, and how much ink goes into the document it produces as a result. Two documents with the same number of pages can consume very different amounts of ink — a page full of dense text uses more ink than one with lots of blank space — so "pages" was never an honest unit for what the print shop actually spends.

A language model bills in a similar way. It doesn't charge for "one question" (the equivalent of a sheet), nor for seconds of compute (the equivalent of machine minutes) — it charges for tokens, the unit that does reflect, with reasonable precision, how much real work it took to process what you sent it and to generate what it answered. Two questions of the same length in characters can cost different amounts of tokens, exactly as two pages of the same size can use different amounts of ink.


What a token is, with reasonable precision

A token is the fragment of text a language model processes as a single unit — not always a character, not always a whole word. A real tokenizer (the component that turns text into tokens) splits text into fragments according to patterns learned from an enormous training corpus: a common English word like "the" is usually a single token; a less common word, or a word in another language, might split into two or three fragments; punctuation, numbers, and spaces also count as tokens or parts of them. The result isn't a simple rule like "one word, one token" — it's a learned mapping, specific to each model family, that doesn't exactly match any traditional linguistic unit.

This guide does not have access to a real tokenizer — lesson 03 explains why, and what convention it uses instead — but the concept still matters: when a line in this module says "this run cost 120 tokens," that 120 is neither 120 characters nor 120 words — it's the real unit an LLM provider uses to measure how much text went into and came out of a model call.

Two token streams, counted and billed separately

Every language-model call moves text in two directions, and both are counted — and billed — independently:

  • Input tokens (input_tokens): all the text the model reads before answering — the original question, the system prompt, and — in an agent like Reservo — every tool_result fed back to it along the way. The longer the accumulated conversation, the more input tokens every new turn consumes, because the model re-reads the entire history every time.
  • Output tokens (output_tokens): all the text the model generates — the tool_use it requests (the tool's name and its arguments, as text), and the final natural-language response.

This distinction isn't an accounting detail — lesson 04 is going to show output token price is five times input token price on claude-sonnet-5. An agent that generates long, verbose responses pays for that asymmetry in a way an agent that reads lots of context but replies in few words doesn't pay equally.


Worked example: token, character, and word are not the same unit

To put this beyond any ambiguity, compare the three ways of measuring "how much text is here" over three real fragments of a Reservo run: the user's question, the agent's final response, and a quote's tool_result.

import json
import reservo_tools as rt

def estimate_tokens(text):
    """Adelanto de la lección 03: la convención de esta guía. Por ahora,
    solo la usamos para comparar -- la justificación completa llega en la
    próxima lección."""
    return len(text) // 4


textos = {
    "pregunta del usuario": "Reserva Focus pro 3h para Ana",
    "respuesta final del agente": "Reservé Focus pro por 3 horas para Ana. Total $60.00. Confirmación #1.",
    "tool_result de get_quote": json.dumps({"price_cents": 6000}),
}

print(f"{'fragmento':<28} {'caracteres':>10} {'palabras':>9} {'tokens (estim.)':>16}")
for label, text in textos.items():
    n_chars = len(text)
    n_words = len(text.split())
    n_tokens = estimate_tokens(text)
    print(f"{label:<28} {n_chars:>10} {n_words:>9} {n_tokens:>16}")

What to expect:

fragmento                    caracteres  palabras  tokens (estim.)
pregunta del usuario                 29         6                7
respuesta final del agente           70        12               17
tool_result de get_quote             21         2                5

None of the three columns matches another. The question has 29 characters and 6 words — almost 5 characters per word, a reasonable average for Spanish — and its token estimate (7) matches neither the characters nor the words. The tool_result is the most revealing case: only 2 "words" if you count by spaces ({"price_cents": and 6000} aren't real words, they're fragments of JSON syntax), but 21 characters that produce 5 estimated tokens — a compact JSON, with no spaces between keys and values, packs more meaning per character than a prose sentence does, and that shows up in the token ratio.


Why billing is per token, not per call or per second

It's worth understanding the underlying reason, not just memorizing the unit. A language model processes text autoregressively: it generates its response one token at a time, and every new token requires the model to reconsider all the text it already has ahead of it — the question, the accumulated context, and what it's already generated of the response. A call's real computational cost grows, roughly, with the amount of text involved — both what's read and what's generated — not with how many "calls" were made or how many clock-seconds it took to respond (that depends on external factors, like server load, which don't reflect the model's real work).

Billing per token, then, isn't an arbitrary billing choice — it's the unit that most closely reflects the real computational work behind every call. Two calls of the same "size" in tokens cost, roughly, the same, regardless of whether one took longer to respond because of network congestion or server load at that moment — that variability is, precisely, Module 4's topic (latency), a signal completely distinct from the cost this module measures.


Common mistakes

  1. Thinking "token" and "word" are synonyms. The worked example confirms it: "Reserva Focus pro 3h para Ana" has 6 words but is estimated at 7 tokens — close, but not equal, and the gap grows with texts that have more punctuation, numbers, or syntax (like a tool_result's JSON).

  2. Assuming input and output tokens cost the same. This lesson only mentions it; lesson 04 confirms it with the real price table: claude-sonnet-5's output token costs five times more than its input token. Designing an agent without that asymmetry in mind is a silent cost mistake.

  3. Believing "billing per token" means "billing per character, with a different name." The worked example's tool_result disproves it: 21 characters, but a different tokens-per-character ratio than the prose question. A token isn't a fixed-length unit — it depends on content.

  4. Ignoring that a conversation's entire history gets re-read every turn. In an agent like Reservo, every new tool_result added to history doesn't just get counted once — the model (concept) re-"reads" the entire accumulated history on every following turn, so a multi-step run's input tokens grow with every step, they don't stay constant.

  5. Thinking this lesson already gives you a way to count tokens for real. Not yet — this example's estimate_tokens is an unjustified preview of lesson 03, which is where it's precisely explained why this guide uses len(text) // 4 instead of a real tokenizer, and what's lost by doing it that way.


Exercises

Exercise 1: Compare the three units over a cancellation task (Easy)

Take the text "Cancela la reserva 999" (a user's question canceling a nonexistent booking) and calculate, by hand first and then with code, its character count, word count, and its token estimate with len(text) // 4.

See solution
text = "Cancela la reserva 999"
print("caracteres:", len(text))
print("palabras  :", len(text.split()))
print("tokens (estim.):", len(text) // 4)

Expected output:

caracteres: 22
palabras  : 4
tokens (estim.): 5

Explanation: 22 characters, 4 words (Cancela, la, reserva, 999), and an estimate of 5 tokens — once again, none of the three figures matches another, confirming they're three genuinely distinct units over the same text.

Exercise 2: Confirm the token estimate grows with accumulated text (Medium)

Take list_rooms()'s tool_result (the JSON for the three rooms). Calculate its token estimate once, and then calculate the estimate for that same text repeated three times in a row (simulating, in a simplified way, how context would grow if the same result got re-read several times in a long conversation). Confirm the estimate grows, roughly, in the same proportion as the text.

See solution
import json
import reservo_tools as rt

def estimate_tokens(text):
    return len(text) // 4

one = json.dumps(rt.list_rooms())
three = one + one + one

print("una copia   :", len(one), "caracteres ->", estimate_tokens(one), "tokens")
print("tres copias :", len(three), "caracteres ->", estimate_tokens(three), "tokens")
print("proporción  :", round(estimate_tokens(three) / estimate_tokens(one), 2))

Expected output:

una copia   : 122 caracteres -> 30 tokens
tres copias : 366 caracteres -> 91 tokens
proporción  : 3.03

Explanation: tripling the text almost exactly triples the token estimate (3.03, not exactly 3.00, due to integer division's rounding down at each point) — this guide's estimate is linear in the text's length, a property lesson 03 confirms in more detail and that precisely explains why a run's cost grows with every additional step that adds text to the history.

Exercise 3: Demonstrate len(text) // 4 is blind to content (Hard)

Build two texts of exactly the same length in characters (35): one with real Spanish prose ("Reserva Boardroom pro 1h para Sofia", watch out: count the exact characters) and one with a meaningless character repeated ("x" repeated 35 times). Confirm both produce the same token estimate, and explain in one sentence why this is a real limitation of this guide's convention — not a calculation error.

See solution
def estimate_tokens(text):
    return len(text) // 4

prose = "Reserva Boardroom pro 1h para Sofia"
nonsense = "x" * len(prose)

print("longitud de ambos:", len(prose), "==", len(nonsense))
print("tokens (prosa)      :", estimate_tokens(prose))
print("tokens (sin sentido):", estimate_tokens(nonsense))

Expected output:

longitud de ambos: 35 == 35
tokens (prosa)      : 8
tokens (sin sentido): 8

Explanation: both texts produce exactly 8 estimated tokens, even though one is real Spanish prose and the other is a meaningless sequence. A real tokenizer would tell them apart — "Sofia" is a recognizable word that probably tokenizes differently than "xxxxx" — but len(text) // 4 only looks at length, never at content. This is, precisely, the central limitation lesson 03 names and labels: an order-of-magnitude estimate, useful for this guide's purpose, but blind to any difference that isn't one of length.


Summary and next step

  • The token is a language model's real unit of cost — neither a character nor a word, but a fragment learned by a tokenizer, specific to each model family.
  • Every call moves two token streams, counted and billed separately: input (what the model reads: the question, the accumulated history, every tool_result) and output (what the model generates: the tool_use it requests, the final response).
  • We confirmed, with real execution, that characters, words, and estimated tokens are three distinct figures over the same text — and that the token estimate grows linearly with the text's length, without distinguishing its content.
  • Billing is per token, not per call or per second, because the token is the unit that most closely reflects an autoregressive model's real computational work.

Next lesson: 03 — Estimating Tokens with len // 4. With the token concept now clear, we build estimate_tokens, the real function this guide uses in everything that follows — with its complete justification, its limits confirmed with code, and the explicit honesty that it never replaces a real tokenizer.


Additional resources

  1. Anthropic — Token counting — How the Claude API really counts tokens; the reference this guide measures its own estimate against starting in the next lesson.
  2. Anthropic — Glossary: tokens — The official definition of token, input, and output, in the Claude documentation.
  3. Python — str.split() — The method used in the worked example to count words by splitting on spaces.
  4. Python — json.dumps — The function that produces a tool_result's compact text, used in several of this lesson's examples.
  5. Python 3.14 — What's New — The version every line of code in this lesson ran on.