Tokens, Context and Cost: Measure Before You Spend
Hands-on lab · IDE in your browser

Tokens, Context and Cost: Measure Before You Spend

Count tokens with tiktoken and see what a token really is, compare your count with the provider's billed prompt tokens, price a 20,000-review job against a budget before running it, truncate text and chat history to fit a context window, and catch replies that were silently cut off by max_tokens.

Time
45 min
Checked steps
5
Level
Beginner
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

Lab cockpit45 min · 5 stepsSession running
4 / 5 steps passingReplies that get cut off, and the shared window · step 5 of 5
tokens.py▶ Run✓ Check
# ---------- Step 5: notice when the reply was cut off ----------def complete(prompt, max_tokens, client=None):    """One call. Return {"text", "finish_reason", "truncated"} where truncated means the    model stopped because it hit max_tokens, not because it was done."""    client = client or CLIENT      def reply_budget(messages, context_window, wanted):    """The prompt and the reply share one context window. Return how many tokens the reply    may use: `wanted`, or less if the window is nearly full. Raise ValueError if nothing is left."""    
TerminalOutput

The job

Brightline Books wants 20,000 archived reviews classified and has set a budget of five dollars. Before anyone runs that job, you work out what it will cost, and on the way you learn the unit everything is measured in. You count tokens, compare your count with what the provider bills, price the job on three model sizes, cut long text and long chats to fit a window, and make sure a reply that ran out of tokens is noticed rather than stored as if it were complete.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Count tokens, and see what a token is

    Models do not read letters or words.

    You writecount_tokens()chars_per_token()
  2. 2

    Your count vs the provider's receipt

    The provider's usage.prompt_tokens is what you pay for, and it is always a bit more than your count of the message.

    You writemeasure()
  3. 3

    Price the job before you run it

    Brightline wants all 20,000 of its archived reviews classified as positive, negative or mixed, and the owner has set a budget of $5.

    You writecall_cost()quote_job()
  4. 4

    Make it fit the window

    A model reads at most its context window, and many deployments cap it far lower to save money.

    You writetruncate_tokens()fit_history()
  5. 5

    Replies that get cut off, and the shared window

    max_tokens is a hard stop.

    You writecomplete()reply_budget()

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Count tokens, and see what a token is

Models do not read letters or words. A tokenizer first cuts the text into tokens: common words are one token, rare words are several, and spaces usually stick to the word after them. Everything about a model is measured in tokens: the price, the speed, and the most it can read at once (its context window).

tiktoken is the tokenizer library OpenAI publishes. Its cl100k_base encoding is close to Llama 3's tokenizer for English (Llama 3 started from it), so it is a good estimator for the model in this lab. Close, not identical: Step 2 measures the gap.

ENC.encode(text) returns a list of token ids (integers). ENC.decode(ids) turns ids back into text.

Do this

1. Write count_tokens(text): the length of ENC.encode(text).

2. Write chars_per_token(text): characters divided by tokens, rounded to 2 places, and 0.0 for empty text, since dividing by zero crashes.

3. Answer the question below, then Run. It prints each sample's counts and its first tokens as text. Look at the made-up word, and at the pieces in the Japanese line: some tokens are only half a character, the raw bytes of a letter that only makes sense with its neighbour.

tokens.py, the file you edit76 lines
"""Tokens, context and cost: count before you send, price before you run, fit before you overflow."""
import json

import tiktoken
from openai import OpenAI

CLIENT = OpenAI()
MODEL = "meta/llama-3.1-8b-instruct"
ENC = tiktoken.get_encoding("cl100k_base")
PRICES = json.load(open("prices.json"))["models"]


# ---------- Step 1: count ----------
def count_tokens(text):
    """How many tokens `text` is, with the cl100k_base tokenizer."""
    # TODO (Step 1): ENC.encode(text) turns text into a list of token ids.
    # Return how many there are.
    raise NotImplementedError("Step 1: write count_tokens()")


def chars_per_token(text):
    """Average characters per token, rounded to 2 places (0.0 for empty text)."""
    # TODO (Step 1): len(text) / count_tokens(text), rounded to 2 places.
    # Return 0.0 for empty text (dividing by zero crashes).
    raise NotImplementedError("Step 1: write chars_per_token()")


# ---------- Step 2: your count vs the provider's receipt ----------
def measure(text, client=None):
    """Send `text` as one user message (max_tokens=1) and compare counts.
    Return {"local": your count, "reported": usage.prompt_tokens, "overhead": reported - local}."""
    client = client or CLIENT
    raise NotImplementedError("measure() arrives in Step 2")


# ---------- Step 3: price a job before you run it ----------
def call_cost(prompt_tokens, completion_tokens, model):
    """US dollars for one call, from PRICES (per 1M tokens)."""
    raise NotImplementedError("call_cost() arrives in Step 3")


def quote_job(texts, instructions, model, output_tokens_each, overhead_each=40):
    """Estimate a job that sends `instructions` + one text per call.
    Return {"calls", "prompt_tokens", "completion_tokens", "usd"} (usd rounded to 4 places)."""
    raise NotImplementedError("quote_job() arrives in Step 3")


# ---------- Step 4: make it fit ----------
def truncate_tokens(text, max_tokens):
    """The longest start of `text` that is at most max_tokens tokens (cut on tokens, not characters)."""
    raise NotImplementedError("truncate_tokens() arrives in Step 4")


def estimate_messages(messages):
    """Our rule of thumb for a whole request: each message costs its content + 4, plus 3 for the reply."""
    return sum(count_tokens(m["content"]) + 4 for m in messages) + 3


def fit_history(messages, budget):
    """Drop the oldest messages after the system message until estimate_messages() <= budget.
    Never drop the system message or the last message. Return a new list."""
    raise NotImplementedError("fit_history() arrives in Step 4")


# ---------- Step 5: notice when the reply was cut off ----------
def complete(prompt, max_tokens, client=None):
    """One call. Return {"text", "finish_reason", "truncated"} where truncated means the
    model stopped because it hit max_tokens, not because it was done."""
    client = client or CLIENT
    raise NotImplementedError("complete() arrives in Step 5")


def reply_budget(messages, context_window, wanted):
    """The prompt and the reply share one context window. Return how many tokens the reply
    may use: `wanted`, or less if the window is nearly full. Raise ValueError if nothing is left."""
    raise NotImplementedError("reply_budget() arrives in Step 5")
Provided for you:prices.jsonsample_reviews.pytry_it.py

Frequently asked questions

How many words is a token?

For English, about three quarters of a word, or roughly four characters, with the cl100k_base tokenizer. Code, numbers and many non-Latin scripts take more tokens for the same length. Step 1 measures five kinds of text side by side.

Why does the provider bill more prompt tokens than I counted?

Providers wrap your messages in a chat template that marks where each message starts and whose it is, and some add a default system message. Those tokens are billed. The overhead varies between providers, so local counts are estimates that need a margin.

How do I estimate what a batch job will cost?

Count the tokens of the instructions and of each input, add a per-call overhead, multiply the expected output length by the number of calls, and apply the input and output prices per million tokens. The lab builds that estimator and uses it on a 20,000-review job.

What happens when a reply hits max_tokens?

It stops mid-sentence without an error. The only signal is finish_reason, which is 'length' instead of 'stop'. Code that stores model output should check it.

Why tokens decide the price, the speed and the limits

Language models are priced, rate-limited and sized in tokens, not words or characters. A token is a piece of text chosen by the model's tokenizer: a common word is one token, a rare word several, and some scripts need a token or more per character. The same paragraph can cost three times as much in one language as in another. This lab turns that into numbers you can plan with. You count tokens with tiktoken and look at the pieces, measure the gap between your count and the provider's billed prompt tokens caused by the chat template, quote a batch job from per-million-token prices before running it, truncate by tokens instead of characters, drop the oldest turns of a chat to fit a budget, and read finish_reason so that replies cut off by max_tokens are caught.