AI EngineeringZero to ProductionHome·About·Contact
Anthropic API in Practice · Part 4

Token counting & usage

Know the cost before and after every call. Count tokens with count_tokens as a pre-flight budget gate, read the usage object precisely, turn it into dollars, track spend per request, respect rate limits, and page through list endpoints the right way.

⏱️ ~1.5 hours🧪 5 recipes🎯 Beginner→Tech-lead

Learning objectives

  • Count tokens BEFORE sending with client.messages.count_tokens(...).
  • Read the usage object on a response field by field.
  • Compute the dollar cost of a call from usage + a per-model price table.
  • Track and log usage per request, and budget against context + rate limits.
  • Page through list endpoints (batches, files) with the SDK's auto-paging iterator.

1 · Why count tokens at all essential

Every request to Claude is billed by the token — a chunk of text roughly ¾ of a word. You pay for the tokens you send (input) and the tokens the model writes back (output), at different per-model prices. Two questions follow you around: will this prompt even fit in the model's context window, and what will it cost. Both have exact answers, and this lesson is the recipe book for getting them.

The one rule that saves the most grief: do not estimate Claude tokens with tiktoken — that's OpenAI's tokenizer and it undercounts Claude by 15–20% (much more on code). The only accurate count is from Anthropic's own count_tokens endpoint, which uses the same tokenizer as inference for the model you name.

count_tokens pre-flight fits budget? decide send messages.create read usage resp.usage log cost $ per call
🗺️ How to read this diagram

This is the whole lesson in one picture: the small loop you run around every call to control what it fits in and what it costs.

  • The first box (count_tokens) is a pre-flight — a cheap call that returns the exact input-token size of your prompt without generating anything.
  • The second box (fits budget?) is your decision: is the prompt under the budget you set and under the model's context window? If not, trim or reject it now, before spending on output.
  • The third box (send) is the real messages.create call — you only reach it once the prompt passes the gate.
  • The last two boxes (read usage → log cost) are the receipt: pull the usage object off the response, turn it into dollars, and record it.

In short: Count first, decide, send, then read and log. Every recipe below is just one of these five boxes done concretely.

Read the diagram left to right: you count first (a cheap, output-free call), decide whether the prompt fits your token budget and the model's context window, send the real request only if it does, then read the usage that comes back and log the cost. The rest of the lesson is one recipe per box.

2 · Pre-flight: count tokens before you send essential

client.messages.count_tokens(...) takes the same arguments you'd pass to messages.createmodel, messages, and optionally system and tools — and returns an object whose input_tokens is exactly what that prompt will cost as input. It runs no generation, so it's fast and cheap. Use it to reject or trim an over-budget prompt before paying for a full call.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Python · count tokens before sending — ▶ needs API key + network
count_tokens.py# ▶ NEEDS API KEY + NETWORK. Real Anthropic SDK — will not run offline.
# pip install anthropic ; export ANTHROPIC_API_KEY=sk-ant-...
import anthropic

client = anthropic.Anthropic()

messages = [{"role": "user", "content": "Summarize the theory of relativity."}]

# Pre-flight: same args as messages.create, but no generation happens.
count = client.messages.count_tokens(
    model="claude-opus-4-8",
    system="You are a concise physics tutor.",
    messages=messages,
)
print("input tokens:", count.input_tokens)   # e.g. 24

# A pre-flight budget/window check before you spend on a real call:
MODEL_CONTEXT_WINDOW = 1_000_000      # Opus 4.8 context window
MY_INPUT_BUDGET = 50_000              # your own per-request cap
if count.input_tokens > MY_INPUT_BUDGET:
    raise SystemExit("Prompt over budget — trim it before sending.")
if count.input_tokens > MODEL_CONTEXT_WINDOW:
    raise SystemExit("Prompt exceeds the model context window.")

# Only now do you pay for generation:
resp = client.messages.create(
    model="claude-opus-4-8", max_tokens=1024,
    system="You are a concise physics tutor.", messages=messages,
)
input tokens: 24
▶ How this works

This is the pre-flight: ask the API how big a prompt is before you pay to run it. count_tokens takes the same arguments as a real call but generates nothing, so it's cheap and fast.

  1. client.messages.count_tokens(...) is passed the same model, system, and messages you'd send to messages.create. It returns an object whose input_tokens is the tokenizer-accurate input size for that exact prompt on that exact model.
  2. The two if checks are the budget gate: reject the prompt if it's over your per-request cap (MY_INPUT_BUDGET), or over the model's MODEL_CONTEXT_WINDOW. Both happen before any generation is billed.
  3. Only after both checks pass do we call messages.create — the call that actually costs output tokens.

What the output means: It prints the input token count (e.g. 24). On a real over-budget prompt one of the raise SystemExit lines fires and no paid call is made.

Try this: This block is labeled ▶ needs API key + network — it calls the live Anthropic API, so it won't run with a bare python file.py. Set MY_INPUT_BUDGET to something tiny like 5 and picture the gate refusing the prompt before it's ever sent.

count_tokens is your budget gateThe returned input_tokens is the real, tokenizer-accurate input size for that exact prompt on that exact model. Gate on it: reject oversized prompts, trim retrieved context to fit, or pick a bigger-context model — all before you've spent a cent on output.

3 · The usage object on a response essential

Every response carries a usage object — the receipt for that call. Read it field by field. The four you care about most:

FieldMeaningPriced at
input_tokensuncached prompt tokens processedfull input rate
output_tokenstokens the model generatedfull output rate
cache_creation_input_tokenstokens written to the prompt cache~1.25× input rate
cache_read_input_tokenstokens served from the cache~0.1× input rate

A subtle trap: input_tokens is the uncached remainder only. The full prompt size is input_tokens + cache_creation_input_tokens + cache_read_input_tokens. If a long-running agent shows a tiny input_tokens, the rest was served from cache — check the sum, not the one field.

Python · read resp.usage after a call — ▶ needs API key + network
read_usage.py# ▶ NEEDS API KEY + NETWORK. Real Anthropic SDK — will not run offline.
import anthropic

client = anthropic.Anthropic()

resp = client.messages.create(
    model="claude-opus-4-8", max_tokens=256,
    messages=[{"role": "user", "content": "Name three prime numbers."}],
)

u = resp.usage
print("input_tokens:                ", u.input_tokens)
print("output_tokens:               ", u.output_tokens)
print("cache_creation_input_tokens: ", u.cache_creation_input_tokens)
print("cache_read_input_tokens:     ", u.cache_read_input_tokens)

# Full prompt size = uncached input + cache write + cache read
full_prompt = (u.input_tokens
               + (u.cache_creation_input_tokens or 0)
               + (u.cache_read_input_tokens or 0))
print("full prompt tokens:          ", full_prompt)

# Log the request id alongside usage so you can trace a call with Anthropic.
print("request id:", resp._request_id)
▶ How this works

Every response carries a usage object — the receipt for that call. This block reads all four token fields and shows the one calculation beginners get wrong.

  1. u = resp.usage grabs the receipt. input_tokens and output_tokens are the prompt and the generated reply; the two cache_* fields are tokens written to and read from the prompt cache.
  2. The trap: input_tokens is the uncached remainder only. The full prompt size is input_tokens + cache_creation + cache_read — that's why the code adds all three (with or 0 in case a field is absent).
  3. resp._request_id is the handle Anthropic support uses to trace a call. Despite the leading underscore it's a public, loggable value.

What the output means: Four token fields print, then the summed full-prompt size, then the request id you'd store next to the usage in your logs.

Try this: This is ▶ needs API key + network — real SDK, no offline run. If a long agent ever shows a tiny input_tokens, don't panic: add the cache fields and you'll see the real prompt size was served cheaply from cache.

Streaming still gives you usageWhen you stream, the running totals arrive on the message_delta events and the complete usage is on stream.get_final_message().usage. You never lose the receipt by streaming.

4 · Computing dollar cost from usage (offline recipe) intermediate

Cost is pure arithmetic: multiply each token bucket by its per-token price and add them up. Prices are quoted per 1,000,000 tokens, so divide by a million. Cache reads are the wrinkle — they bill at roughly 0.1× the input rate, which is the whole point of caching. The helper below is stdlib-only and RUNS offline so you can unit-test your cost math without touching the network: it takes a fake usage dict plus a price table and returns the dollar cost, handling the cache-read discount.

Python · cost-from-usage calculator (runs offline)
cost_from_usage.py# (runs offline) — pure stdlib, no SDK, no network. `python cost_from_usage.py`
# Prices are $ per 1,000,000 tokens and DRIFT — always re-check current pricing.
PRICES = {
    # model: (input_per_mtok, output_per_mtok)
    "claude-opus-4-8":   (5.00, 25.00),
    "claude-sonnet-4-6": (3.00, 15.00),
    "claude-haiku-4-5":  (1.00,  5.00),
}

CACHE_READ_MULTIPLIER = 0.1   # cache reads bill at ~0.1x the input rate

def cost_from_usage(model, usage):
    """usage: dict with input_tokens / output_tokens and optional cache fields.
    Returns the dollar cost of one call, rounded to 6 decimals."""
    in_price, out_price = PRICES[model]
    inp   = usage.get("input_tokens", 0)
    out_  = usage.get("output_tokens", 0)
    creat = usage.get("cache_creation_input_tokens", 0)   # billed ~1.25x input
    read  = usage.get("cache_read_input_tokens", 0)       # billed ~0.1x input

    dollars = (
        inp   * in_price
        + out_  * out_price
        + creat * in_price * 1.25
        + read  * in_price * CACHE_READ_MULTIPLIER
    ) / 1_000_000
    return round(dollars, 6)

# A fake usage receipt — no API call needed.
fake_usage = {
    "input_tokens": 1_000,
    "output_tokens": 500,
    "cache_creation_input_tokens": 0,
    "cache_read_input_tokens": 8_000,   # served cheaply from cache
}
cost = cost_from_usage("claude-opus-4-8", fake_usage)
print("call cost: $" + format(cost, ".6f"))
# 1000*5 + 500*25 + 8000*5*0.1 = 5000 + 12500 + 4000 = 21500 microdollars
print("that's", 1_000 + 500 + 8_000, "tokens touched")
call cost: $0.021500
that's 9500 tokens touched
▶ How this works

This is the one recipe that runs offline — pure stdlib, no SDK, no network. It turns a usage dict into a dollar cost, so you can unit-test your cost math without spending anything.

  1. PRICES maps each model to (input_per_mtok, output_per_mtok) — dollars per million tokens, which is why the final total divides by 1_000_000.
  2. cost_from_usage multiplies each token bucket by its rate: input and output at full price, cache writes at ~1.25×, and cache reads at 0.1× (the CACHE_READ_MULTIPLIER). Cache reads are the cheap seats.
  3. fake_usage is a made-up receipt — 1,000 input, 500 output, 8,000 served from cache — so no API call is needed to exercise the math.
  4. The comment does the arithmetic by hand: 1000×5 + 500×25 + 8000×5×0.1 = 21,500 microdollars = $0.021500.

What the output means: call cost: $0.021500 then that's 9500 tokens touched — the exact numbers you'll see when you run python cost_from_usage.py.

Try this: Change cache_read_input_tokens to 0 and make those 8,000 tokens fresh input_tokens instead. The cost jumps from $0.0215 to $0.0575 — that gap is exactly what caching buys you.

Cache reads are the cheap seatsIn the example, 8,000 cached tokens cost the same as 800 fresh input tokens (0.1×). That's why a stable prompt prefix + caching is the single biggest cost lever on repeated calls — the cache-read line in your cost function stays tiny while the work stays the same.

5 · Tracking & logging usage per request intermediate

Knowing one call's cost is nice; knowing your spend over time is what keeps a product alive. Wrap every call so it logs usage the moment the response returns, keyed by request id, and accumulate a running total. The recipe below shows the shape — a thin wrapper that records model, tokens, cost, and request id per call.

Python · a per-request usage logger — ▶ needs API key + network
usage_logger.py# ▶ NEEDS API KEY + NETWORK. Real Anthropic SDK — will not run offline.
# (The cost_from_usage helper it calls IS the offline one from recipe 4.)
import anthropic
from cost_from_usage import cost_from_usage

client = anthropic.Anthropic()
LEDGER = []   # in real life: a DB row or a metrics counter per call

def tracked_create(**kwargs):
    resp = client.messages.create(**kwargs)
    u = resp.usage
    usage_dict = {
        "input_tokens": u.input_tokens,
        "output_tokens": u.output_tokens,
        "cache_creation_input_tokens": u.cache_creation_input_tokens or 0,
        "cache_read_input_tokens": u.cache_read_input_tokens or 0,
    }
    cost = cost_from_usage(kwargs["model"], usage_dict)
    LEDGER.append({
        "request_id": resp._request_id,
        "model": kwargs["model"],
        "usage": usage_dict,
        "cost_usd": cost,
    })
    return resp

tracked_create(
    model="claude-opus-4-8", max_tokens=256,
    messages=[{"role": "user", "content": "Hello"}],
)
print("calls logged:", len(LEDGER))
print("total spend: $" + format(sum(row["cost_usd"] for row in LEDGER), ".6f"))
▶ How this works

One call's cost is nice; your spend over time is what matters. This wraps every call so it records usage, cost, and request id the moment the response returns.

  1. tracked_create(**kwargs) forwards its arguments to the real client.messages.create, then reads resp.usage into a plain dict — the same shape the offline cost_from_usage from recipe 4 expects.
  2. It calls that offline helper to price the call, then appends a row to LEDGER with the request id, model, usage, and dollar cost. In production that append is a DB insert or a metrics counter.
  3. Because the cost function is the offline, testable one, the same math you unit-tested now runs on live usage — tested offline, trusted online.

What the output means: After one call it prints calls logged: 1 and the running total spend — your ledger's first row.

Try this: This block is ▶ needs API key + network. Notice it imports the offline cost_from_usage — the boundary between 'real API' and 'pure math' is deliberate, so you can test the math without the network.

Log the request idresp._request_id (public despite the underscore) is the handle Anthropic support needs to trace a call. Store it in the same row as your usage and cost — when something looks wrong, that id turns a shrug into an answer.

6 · Budget, rate limits & the headers advanced

Two different ceilings can stop you, and beginners conflate them. The context window is how many tokens fit in one request (input + output). Rate limits are how much you may send per minute/day across all requests — requests-per-minute (RPM), input- and output-tokens-per-minute (ITPM/OTPM), and tokens-per-day (TPD).

When you exceed a rate limit the API returns HTTP 429 with a retry-after header and x-ratelimit-* headers telling you your remaining quota. The SDK already retries 429 and 5xx with exponential backoff (default max_retries=2) — so most of the time you do nothing. When you need the raw numbers, reach for the headers via with_raw_response:

Python · read rate-limit headers & handle 429 — ▶ needs API key + network
rate_limits.py# ▶ NEEDS API KEY + NETWORK. Real Anthropic SDK — will not run offline.
import anthropic

client = anthropic.Anthropic()

# .with_raw_response exposes the HTTP headers alongside the parsed message.
raw = client.messages.with_raw_response.create(
    model="claude-opus-4-8", max_tokens=64,
    messages=[{"role": "user", "content": "ping"}],
)
h = raw.headers
print("requests remaining:", h.get("anthropic-ratelimit-requests-remaining"))
print("input tokens remaining:", h.get("anthropic-ratelimit-input-tokens-remaining"))
message = raw.parse()   # the Message you'd normally get from .create(...)
print("output tokens:", message.usage.output_tokens)

# The SDK auto-retries 429/5xx, but you can still catch a give-up:
try:
    client.messages.create(
        model="claude-opus-4-8", max_tokens=64,
        messages=[{"role": "user", "content": "ping"}],
    )
except anthropic.RateLimitError as e:
    retry_after = int(e.response.headers.get("retry-after", "60"))
    print(f"Rate limited; retry after {retry_after}s.")
▶ How this works

Two ceilings can stop you: the context window (how much fits in one request) and rate limits (how much per minute across all requests). This shows how to read the rate-limit headers and handle a 429.

  1. client.messages.with_raw_response.create(...) gives you the raw HTTP response so you can read headers. raw.headers holds the anthropic-ratelimit-*-remaining values — your remaining quota this minute.
  2. raw.parse() turns that raw response back into the normal Message object, so you still get message.usage.
  3. The try/except anthropic.RateLimitError is the give-up case: the SDK already auto-retries 429s twice, but if it exhausts retries you read the retry-after header and back off.

Try this: Also ▶ needs API key + network. The key idea to carry away: a prompt can be well under the context window and still get a 429 because you've spent your tokens-per-minute — count_tokens guards the window, the x-ratelimit-* headers guard the rate.

Context window ≠ rate limitA prompt can be well under the model's context window and still get a 429 because you've burned your tokens-per-minute across many requests. Count tokens to stay under the window; watch the x-ratelimit-* headers and back off to stay under the rate.

7 · Pagination: list endpoints & the auto-paging iterator professional

List endpoints — Message Batches, Files — return results a page at a time so a huge account doesn't dump everything at once. The SDK hides this: iterating the return value of a .list() call auto-paginates across every page. The rookie mistake is reading only .data (the first page) and thinking that's all there is.

Python · auto-paging over batches & files — ▶ needs API key + network
paginate.py# ▶ NEEDS API KEY + NETWORK. Real Anthropic SDK — will not run offline.
import anthropic

client = anthropic.Anthropic()

# Iterating the list result auto-fetches EVERY page — not just the first.
total_batches = 0
for b in client.messages.batches.list(limit=20):
    total_batches += 1
    print(b.id, b.processing_status)
print("batches seen across all pages:", total_batches)

# Same pattern for the Files API (beta):
for f in client.beta.files.list(betas=["files-api-2025-04-14"]):
    print(f.id, f.filename, f.size_bytes)

# .data is only the CURRENT page — do NOT treat it as the full set:
first_page = client.messages.batches.list(limit=20)
print("this is ONLY page 1:", len(first_page.data), "items")
if first_page.has_next_page():
    print("...more pages exist; iterate the list to get them all")
▶ How this works

List endpoints (batches, files) return one page at a time. The SDK hides this: iterating the result of .list() walks every page for you. The rookie mistake is reading only .data and thinking that's everything.

  1. for b in client.messages.batches.list(limit=20): auto-fetches page after page — the loop keeps going past the first 20 until the whole account is seen.
  2. The Files API list works exactly the same way (it just needs the beta header). Same iterate-don't-index pattern.
  3. first_page.data is only page one. first_page.has_next_page() tells you more exist — proof that indexing .data silently drops the rest.

Try this: ▶ needs API key + network. Remember the rule: iterate the list to get everything; .data is a single page. For manual control there's get_next_page() and the last_id cursor, but the iterator is what you want almost always.

Iterate, don't indexfor b in client.messages.batches.list(): walks the whole account; client.messages.batches.list().data is page one only. For manual control there's has_next_page() / get_next_page() and the last_id cursor — but the iterator is what you want 95% of the time.

8 · Tech-lead — owning the token & cost surface tech-lead

A lead turns these recipes into a system contract. Pre-flight every request through count_tokens against a budget you own, not just the model's window. Log usage + cost + request id on every call into one place, so cost per feature is a query, not a guess. Price the workload from a table you keep current — prices drift, so the numbers in recipe 4 are a snapshot, not gospel; re-check them and keep the table in one module. Attack cost where the tokens are: cache stable prefixes (the 0.1× cache-read line), pick the smallest model that passes your evals, and cap max_tokens so a runaway generation can't blow the budget.

The offline cost_from_usage helper is deliberately dependency-free so it can live in your test suite: feed it recorded usage dicts and assert the cost math, then let the real SDK calls feed it live resp.usage in production. Same function, tested offline, trusted online.

Prices drift — own the tableModel IDs and per-token prices change. Keep the price table in one module, date it, and re-verify against current pricing before you quote a cost to anyone. A cost number is only as trustworthy as the table behind it.

Exercise AP4.1 — Gate a prompt on a budget

Context: The cheapest token is the one you never generate. A pre-flight gate that counts a prompt and refuses oversized requests stops a runaway system string from ever reaching billing — the token check costs nothing.

Your task: Using count_tokens.py as the shape, write a pre-flight that counts a prompt, rejects it if it exceeds a 50,000-token budget you set, and only then calls messages.create; prove it with a deliberately huge system string.

Requirements:

  • Count input with messages.count_tokens against the target model before generating
  • Set a 50,000-token budget and reject when input_tokens exceeds it
  • Only call messages.create when the count is within budget
  • Demonstrate the refusal by feeding an oversized system string — no generation is billed
  • Requires an API key to run the count

💡 Hint: Gate on the returned input_tokens before you build or send the real call — the count and the generate must use the same model to stay honest.

Exercise AP4.2 — Cost a cached vs uncached call

Context: Prompt caching is the single biggest cost lever precisely because cache reads are billed at 0.1× the input rate — the same 8,000-token prefix costs an order of magnitude less on a cache hit than as fresh input.

Your task: Run cost_from_usage.py offline with two fake usage dicts for the same work — one where an 8,000-token prefix is fresh input_tokens, one where it's a cache_read_input_tokens hit — then report the dollar difference.

Requirements:

  • Build two usage dicts identical except for where the 8,000-token prefix lands
  • Price fresh input at the base rate and the cache read at 0.1× that rate
  • Compute and print the dollar difference between the two
  • Explain why the 0.1× multiplier makes caching the biggest cost lever
  • Run entirely offline — no API key needed

💡 Hint: Hold output tokens equal across both dicts so the delta you print isolates exactly the input-vs-cache-read pricing gap.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Count tokens before you sendBeginner

Context: Every prompt costs money and every model has a context ceiling, so knowing a request's exact input size before you send it is the difference between a predictable bill and a surprise 400. Token counts are model-specific, which is why estimates from other ecosystems quietly mislead you.

Your task: Use messages.count_tokens to pre-flight a prompt's input size against the exact model you'll call, and explain why tiktoken is the wrong tool for Claude.

Requirements:

  • Call client.messages.count_tokens(model=..., messages=[...]) with the same model id you intend to generate with
  • Read the exact count off resp.input_tokens
  • State that tiktoken is OpenAI's tokenizer and undercounts Claude tokens (~15–20% on prose, more on code)
  • Note that count_tokens is the only accurate source and is free to call

💡 Hint: The endpoint takes the same model and messages shape as messages.create — feed it your real payload, not a guess.

Show solution

Token counts are model-specific — count against the same model you'll use:

import anthropic
client = anthropic.Anthropic()

resp = client.messages.count_tokens(
    model="claude-opus-4-8",
    messages=[{"role": "user", "content": open("CLAUDE.md").read()}],
)
print(resp.input_tokens)   # exact input token count for this model

Do not use tiktoken — it is OpenAI's tokenizer and undercounts Claude tokens by ~15–20% on text and far more on code. The count_tokens endpoint is the only accurate source, and it costs nothing to call.

Exercise 2 · Compute dollar cost from the usage object (OFFLINE)Intermediate

Context: Once a response comes back, its usage object is the ground truth for what you were actually charged — and output tokens are priced several times higher than input, so a short-output task can be far cheaper than its prompt size suggests.

Your task: Given a response's usage (input_tokens, output_tokens), write cost(usage, model) that computes the exact dollar cost for Opus, Sonnet, and Haiku.

Requirements:

  • Key an offline price table by model id with (input $/MTok, output $/MTok) pairs
  • Cover claude-opus-4-8, claude-sonnet-4-6, and claude-haiku-4-5
  • Convert token counts to millions before multiplying by the per-MTok rate
  • Sum input and output contributions into one dollar figure
  • Drive it from real usage.input_tokens/usage.output_tokens, not an estimate

💡 Hint: Divide each token count by 1e6 and multiply by its side's rate; output is the expensive side, so keep the two rates separate.

Show solution

Offline calculator keyed by the per-MTok price table:

PRICES = {   # (input $/MTok, output $/MTok)
    "claude-opus-4-8":   (5.0, 25.0),
    "claude-sonnet-4-6": (3.0, 15.0),
    "claude-haiku-4-5":  (1.0,  5.0),
}

def cost(input_tokens, output_tokens, model):
    pin, pout = PRICES[model]
    return input_tokens/1e6*pin + output_tokens/1e6*pout

# e.g. a response with usage.input_tokens=1200, usage.output_tokens=800
print(f"${cost(1200, 800, 'claude-opus-4-8'):.5f}")     # $0.02600
print(f"${cost(1200, 800, 'claude-sonnet-4-6'):.5f}")   # $0.01560

Output tokens are 5x the price of input on Opus, so terse-output tasks are far cheaper than they look from input size alone. Read the real numbers off resp.usage, not an estimate.

Exercise 3 · Full cost including cache and cached-read tiers (OFFLINE)Advanced

Context: Prompt caching splits input into three billed tiers, and the headline input_tokens field is only the uncached remainder — an agent that ran for hours can show a tiny input_tokens while serving most of the prompt from cache. Bill the sum or you'll wildly misprice the workload.

Your task: Write full_cost(usage, model) that prices a real usage with input_tokens, cache_creation_input_tokens, cache_read_input_tokens, and output_tokens.

Requirements:

  • Treat input_tokens as uncached input at the base rate
  • Price cache_creation_input_tokens at 1.25× the input rate (cache write)
  • Price cache_read_input_tokens at 0.1× the input rate (cache read)
  • Add output tokens at the output rate
  • Total prompt size is the sum of all three input fields, not just input_tokens

💡 Hint: Compute a single per-token input rate once, then apply the 1.25× and 0.1× multipliers to the two cache fields before summing.

Show solution

The complete cost model — total prompt = uncached + creation + read:

PRICES = {"claude-opus-4-8": (5.0, 25.0)}

def full_cost(u, model):
    pin, pout = PRICES[model]
    pin_t = pin / 1e6
    return (u["input_tokens"]                * pin_t          # uncached input
          + u["cache_creation_input_tokens"] * pin_t * 1.25   # cache write
          + u["cache_read_input_tokens"]      * pin_t * 0.10   # cache read
          + u["output_tokens"]               * pout / 1e6)    # output

u = {"input_tokens": 500, "cache_creation_input_tokens": 0,
     "cache_read_input_tokens": 20000, "output_tokens": 800}
print(f"${full_cost(u, 'claude-opus-4-8'):.5f}")   # $0.03250

input_tokens is only the uncached remainder — total prompt size is the sum of all three input fields. An agent that ran for hours showing input_tokens=500 served the rest from cache; bill the sum, not the single field.

Exercise 4 · Per-request usage logger with running totalsExpert

Context: In production you rarely see cost until the monthly bill; a thin logging seam around every model call turns "the bill was high" into per-request attribution you can actually debug — and the request id is exactly what Anthropic support needs to trace a failure.

Your task: Wrap messages.create so every call logs its _request_id, its token usage, and its dollar cost, and accumulates a running total across calls.

Requirements:

  • Delegate to client.messages.create(**kwargs) and return the real response unchanged
  • Read resp.usage for input_tokens/output_tokens and compute cost from a price table
  • Log resp._request_id on every call (it is public despite the leading underscore)
  • Maintain a running total dollar figure across all wrapped calls
  • Print per-call usage, per-call cost, and the accumulated total

💡 Hint: Keep the total in a module-level mutable (a dict or a small object) so it survives across calls without a global rebind.

Show solution

A thin logging wrapper — _request_id is public despite the underscore:

PRICES = {"claude-opus-4-8": (5.0, 25.0)}
_total = {"usd": 0.0}

def logged_create(**kw):
    resp = client.messages.create(**kw)
    u = resp.usage
    pin, pout = PRICES[kw["model"]]
    usd = u.input_tokens/1e6*pin + u.output_tokens/1e6*pout
    _total["usd"] += usd
    print(f"req={resp._request_id} in={u.input_tokens} out={u.output_tokens} "
          f"${usd:.5f} total=${_total['usd']:.5f}")
    return resp

Log _request_id on every call — it is what Anthropic support needs to trace a failure. Accumulating cost per request turns "the bill was high" into a per-call attribution you can actually debug.

Exercise 5 · Respect rate-limit headers and back offProfessional

Context: The SDK already retries 429s and 5xx with backoff, but a robust bulk client also reads the rate-limit headers so it can pace itself and surface remaining capacity to a dashboard instead of blindly hammering the API.

Your task: Show how to reach the retry-after and x-ratelimit-* response headers on a 429 and back off, layering only the behavior the SDK's built-in retries don't give you.

Requirements:

  • Catch the typed anthropic.RateLimitError rather than a bare exception
  • Read e.response.headers["retry-after"] and sleep for that many seconds
  • Surface x-ratelimit-remaining-tokens (or similar) for observability
  • Acknowledge the SDK auto-retries 429/5xx (default ~2 retries) so custom logic is additive, not a replacement
  • Retry the same messages.create call after honoring the wait

💡 Hint: The typed error carries the raw HTTP response on e.response — the headers you need are already sitting there; default the wait if the header is absent.

Show solution

Catch the typed error and honor retry-after; the SDK already retries 429/5xx:

import time, anthropic

def call_with_backoff(**kw):
    try:
        return client.messages.create(**kw)
    except anthropic.RateLimitError as e:
        wait = int(e.response.headers.get("retry-after", "60"))
        print("rate limited; sleeping", wait,
              "remaining:", e.response.headers.get("x-ratelimit-remaining-tokens"))
        time.sleep(wait)
        return client.messages.create(**kw)

The SDK auto-retries 429/5xx with backoff (default 2 retries), so only add custom logic when you need behavior beyond that — e.g. surfacing x-ratelimit-remaining-* to a dashboard or pacing a bulk job under the token-per-minute limit.

Exercise 6 · Own the token & cost surface for the orgIndustry scenario

Context: As the platform owner you hold two levers over the org's spend: which model each route uses, and a hard budget gate so one runaway job can't drain the month's allowance. Picking the cheapest tier that clears the quality bar is where most of the savings live.

Your task: Write an offline router that picks the cheapest model clearing a per-request quality bar and refuses any request whose estimated cost would exceed the remaining budget.

Requirements:

  • Map a quality tier (e.g. simple/standard/hard) to Haiku/Sonnet/Opus respectively
  • Estimate cost from est_in/est_out and the chosen model's price
  • Reject with a clear message when the estimate exceeds remaining_usd
  • Otherwise return the chosen model and its estimated cost
  • Run entirely offline — no API key, pure routing/budget logic

💡 Hint: Route first (quality → tier → price), then gate: compute the estimate and compare against the remaining allowance before returning the model.

Show solution

Route by quality need, gate by budget — pure logic, no API needed:

PRICES = {"claude-haiku-4-5": (1.0,5.0), "claude-sonnet-4-6": (3.0,15.0),
          "claude-opus-4-8": (5.0,25.0)}

def route(quality, est_in, est_out, remaining_usd):
    tier = {"simple": "claude-haiku-4-5", "standard": "claude-sonnet-4-6",
            "hard": "claude-opus-4-8"}[quality]
    pin, pout = PRICES[tier]
    est = est_in/1e6*pin + est_out/1e6*pout
    if est > remaining_usd:
        return f"REJECT — est ${est:.4f} exceeds remaining ${remaining_usd:.4f}"
    return f"{tier} (est ${est:.4f})"

print(route("simple",   2000, 500, 100.0))   # haiku
print(route("hard",  4_000_000, 1_000_000, 5.0))  # REJECT — over budget

The owner's levers are model choice (use the cheapest tier that clears the quality bar — Haiku for classification, Opus for hard reasoning) and a hard budget gate that pre-flights estimated cost against the remaining allowance so one runaway job cannot drain the month.

✓ Checkpoint — you can move on when you can…

  • Count tokens before sending with count_tokens and gate on a budget.
  • Name the four usage fields and compute full prompt size from them.
  • Turn a usage dict + price table into a dollar cost, cache-read discount included.
  • Log usage, cost, and request id per call; distinguish context window from rate limit.
  • Page through a list endpoint with the auto-paging iterator, not just .data.

Knowledge check check yourself

✓ Knowledge check

Why is the context window a different ceiling from rate limits, and how can a prompt be under the window yet still get a 429?

Show answer
The context window limits tokens in a single request (input + output); rate limits (RPM, ITPM, OTPM, TPD) cap throughput across all requests over time. A small prompt can fit the window but still hit a 429 if the per-minute token budget for the whole account is already exhausted.
✓ Knowledge check

On a response with caching, why is input_tokens alone the wrong number for the true prompt size, and how do you compute it correctly?

Show answer
input_tokens reports only the uncached remainder, so relying on it undercounts. The true prompt size is input_tokens + cache_creation_input_tokens + cache_read_input_tokens, since the cached portions are billed separately (~1.25x on write, ~0.1x on read) but are still part of the prompt.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in