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.
Learning objectives
- Count tokens BEFORE sending with
client.messages.count_tokens(...). - Read the
usageobject 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.
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.createcall — you only reach it once the prompt passes the gate. - The last two boxes (read usage → log cost) are the receipt: pull the
usageobject 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.create — model, 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.
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
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.
client.messages.count_tokens(...)is passed the samemodel,system, andmessagesyou'd send tomessages.create. It returns an object whoseinput_tokensis the tokenizer-accurate input size for that exact prompt on that exact model.- The two
ifchecks are the budget gate: reject the prompt if it's over your per-request cap (MY_INPUT_BUDGET), or over the model'sMODEL_CONTEXT_WINDOW. Both happen before any generation is billed. - 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.
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:
| Field | Meaning | Priced at |
|---|---|---|
input_tokens | uncached prompt tokens processed | full input rate |
output_tokens | tokens the model generated | full output rate |
cache_creation_input_tokens | tokens written to the prompt cache | ~1.25× input rate |
cache_read_input_tokens | tokens 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.
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)
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.
u = resp.usagegrabs the receipt.input_tokensandoutput_tokensare the prompt and the generated reply; the twocache_*fields are tokens written to and read from the prompt cache.- The trap:
input_tokensis the uncached remainder only. The full prompt size isinput_tokens + cache_creation + cache_read— that's why the code adds all three (withor 0in case a field is absent). resp._request_idis 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.
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.
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
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.
PRICESmaps each model to(input_per_mtok, output_per_mtok)— dollars per million tokens, which is why the final total divides by1_000_000.cost_from_usagemultiplies each token bucket by its rate: input and output at full price, cache writes at ~1.25×, and cache reads at0.1×(theCACHE_READ_MULTIPLIER). Cache reads are the cheap seats.fake_usageis a made-up receipt — 1,000 input, 500 output, 8,000 served from cache — so no API call is needed to exercise the math.- 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.
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.
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"))
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.
tracked_create(**kwargs)forwards its arguments to the realclient.messages.create, then readsresp.usageinto a plain dict — the same shape the offlinecost_from_usagefrom recipe 4 expects.- It calls that offline helper to price the call, then appends a row to
LEDGERwith the request id, model, usage, and dollar cost. In production that append is a DB insert or a metrics counter. - 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.
resp._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:
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.")
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.
client.messages.with_raw_response.create(...)gives you the raw HTTP response so you can read headers.raw.headersholds theanthropic-ratelimit-*-remainingvalues — your remaining quota this minute.raw.parse()turns that raw response back into the normalMessageobject, so you still getmessage.usage.- The
try/except anthropic.RateLimitErroris the give-up case: the SDK already auto-retries 429s twice, but if it exhausts retries you read theretry-afterheader 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.
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.
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")
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.
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.- The Files API list works exactly the same way (it just needs the beta header). Same iterate-don't-index pattern.
first_page.datais only page one.first_page.has_next_page()tells you more exist — proof that indexing.datasilently 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.
for 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.
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_tokensagainst the target model before generating - Set a 50,000-token budget and reject when
input_tokensexceeds it - Only call
messages.createwhen the count is within budget - Demonstrate the refusal by feeding an oversized
systemstring — 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
usagedicts 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.
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
tiktokenis OpenAI's tokenizer and undercounts Claude tokens (~15–20% on prose, more on code) - Note that
count_tokensis 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.
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, andclaude-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.
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_tokensas uncached input at the base rate - Price
cache_creation_input_tokensat 1.25× the input rate (cache write) - Price
cache_read_input_tokensat 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.
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.usageforinput_tokens/output_tokensand compute cost from a price table - Log
resp._request_idon 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.
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.RateLimitErrorrather 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.createcall 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.
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_outand 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_tokensand gate on a budget. - Name the four
usagefields 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
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
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.