AI EngineeringZero to ProductionHome·About·What’s new·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

Picture the invoice nobody warned you about. You ship an agent that quietly stuffs a growing chat history and a fat retrieved-context blob into every call. In the demo it's a few cents. A month later finance forwards you a bill with an extra zero, and the worst part is you can't say which feature spent it. That surprise is entirely avoidable — because unlike almost everything else about a language model, the cost of a call is not fuzzy. It's a number you can compute exactly, before and after, if you know where to look.

Here's the mental model that makes the rest of this lesson click: a token is the model's unit of both work and money, so counting tokens is simply reading the meter. Think of it like a taxi with the fare display on the dash — you don't guess what the ride will cost, you watch it tick. count_tokens is the display before you get in (a quote); the usage object is the receipt after you get out. Miss either and you're back to guessing, which is how the surprise-bill story starts.

The mistake almost everyone makes first: reaching for a familiar token estimator from a different ecosystem and trusting it. It feels close enough — until "close enough" is a 20% miscount on the exact number your budget gate depends on, and your "safe" prompt silently blows the window in production. The rule below exists precisely because this trap is so easy to fall into and so quiet when it bites.

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

The cheapest token is the one you never generate. There's a discipline pilots follow that maps perfectly here: the pre-flight check. You don't taxi to the runway and then discover the fuel gauge is empty — you check on the ground, where a problem costs nothing but a delay. Counting tokens before you send is exactly that check. It runs on the "ground" (no generation, so no output bill), tells you the real size of what you're about to fly, and lets you turn back if the load is wrong.

Why this deserves its own recipe rather than a footnote: the alternative is discovering your prompt is too big after the model has already read it. By then you've paid to process a giant input, or the request errors out at the window limit, or worse, it squeaks under the window but leaves no room for the answer and the model gets cut off mid-sentence. A pre-flight count converts all of those failure modes into a single cheap boolean — "does this fit?" — that you answer before spending anything.

The common mistake here is subtle and worth naming: people count the prompt but forget to count it the same way the real call will be built. If your generation call includes a system prompt and a tools definition, those tokens count too — and a count that omits them is a lie that passes review and fails in prod. The fix is baked into the API's design, which is why the next paragraph stresses passing the identical arguments.

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.

same args model·msgs·system·tools count_tokens free · no generation over budget? input_tokens vs cap reject / trim — $0 spent fits create billed The gate runs on the ground. The identical prompt arguments feed a free count_tokens; the returned input_tokens is compared to your cap. Over budget → reject or trim for $0; only a passing prompt reaches the billed messages.create.
🗺️ How to read this diagram

Follow the arrows left to right: the prompt is measured before it can ever cost you anything.

  • The blue "same args" box is the key discipline — you pass count_tokens the identical model, messages, system, and tools the real call will use, so the count isn't a lie.
  • The green "count_tokens" box is free and generates nothing; it hands back the exact input_tokens.
  • The amber "over budget?" decision splits the flow: the downward amber arrow to the red box is the cheap escape — reject or trim, having spent nothing — while the passing branch is the only path to the billed messages.create.

In short: Count with the real args, compare to your cap, and only a prompt that fits ever reaches a paid 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

If the pre-flight count was the quote, this is the itemized receipt — and you should read it like one. Nobody glances at a restaurant bill and assumes the total; you scan the line items, because that's where the surprise charge hides. The usage object is that itemization for a Claude call: it doesn't just say "you spent tokens," it breaks the spend into buckets that are each priced differently. Skip the line items and you'll mis-attribute cost — and cost you can't attribute is cost you can't cut.

Here's why the breakdown matters and isn't just bookkeeping. Input and output tokens are billed at different rates (output is the expensive side), and prompt-cache traffic splits input further into a "write" tier and a "read" tier at their own multipliers. So two calls that look identical on the surface — same prompt, same answer — can cost wildly different amounts depending on how much was served from cache. The only way to know which call you actually made is to read usage.input_tokens and usage.output_tokens together with the two cache fields, rather than eyeballing the prompt.

The mistake this section is built to prevent has burned careful engineers: assuming input_tokens is "the size of my prompt." It isn't — it's the uncached remainder. A long-lived agent replaying a cached prefix can report a shockingly small input_tokens and you'll conclude your prompt shrank, when really most of it was served cheaply from cache. Read the receipt as a whole, and the numbers stop lying to you. The four fields 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.

resp.usage — four billed buckets full prompt size = the sum of these three input_tokens uncached · 1× cache_creation write · ~1.25× cache_read read · ~0.1× ↑ add all three = true prompt size ↑ output_tokens generated · full × the expensive side trap: reading input_tokens alone under-reports a cached prompt Read the receipt as line items. Three input buckets — uncached, cache-write, cache-read — sum to the true prompt size; output_tokens is billed separately at the expensive rate. Trusting input_tokens alone under-counts anything served from cache.
🗺️ How to read this diagram

Scan the top row left to right: the four fields the usage object reports, each priced on its own.

  • The three boxes left of the dashed line — input_tokens, cache_creation, cache_read — are all input. Their small-print multipliers (1×, ~1.25×, ~0.1×) are why they can't be lumped together.
  • The dashed line and the "add all three" label is the fix for the trap: the true prompt size is the sum of those three, not the first field alone.
  • The separate purple output_tokens box is what the model wrote back — billed at the full, expensive output rate, kept apart because it is not part of the prompt.
  • The red band names the mistake: a long agent replaying a cached prefix shows a tiny input_tokens, so reading that one field makes the prompt look smaller than it was.

In short: Prompt size = the three input buckets summed; output is a separate, pricier line. Never read input_tokens by itself when caching is on.

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

Once you can read the receipt, converting it to dollars is the easy part — which is exactly why it's worth doing well. This is the one step in the whole loop that needs no API key, no network, and no luck: it's a price list times some counts. That makes it the perfect thing to pin down in code you can unit-test, so that the number you quote to your boss or your budget alert is arithmetic you've verified, not a vibe. Get this seam right and every other cost claim in your system inherits its trustworthiness.

The analogy is a grocery receipt: each line is quantity × unit_price, and the total is just the sum. Tokens work the same way, with one twist that trips people up — the "unit price" isn't one number per model, it's a small table of four: input, output, cache-write, and cache-read, and they are not equal. Output typically costs several times more than input, and a cache read is the bargain bin at roughly a tenth of the input rate. Multiply each bucket by the right rate, divide by a million (because prices are quoted per million tokens), and add. That's the entire calculation.

Two mistakes to sidestep. First, do not hard-code prices and forget them — model prices drift, and a stale table quietly mis-quotes every cost you report; keep the table in one place and date it. Second, do not price only input_tokens and output_tokens when caching is in play — the cache-write and cache-read buckets are real money (one above the input rate, one far below it), and dropping them either over- or under-states the bill. The helper below makes both easy to get right by taking the whole usage dict and the price table as inputs.

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.

count × rate (one line each) input × 1× cache-write × 1.25× cache-read × 0.1× output × out-rate Σ sum add the lines ÷ 1,000,000 prices are per-Mtok $ the four rates are NOT equal — output dear, cache-read cheap Cost is a receipt: quantity × unit price, summed. Each token bucket carries its own rate (output dear, cache-read ~0.1×); add the lines and divide by a million because prices are quoted per-Mtok. No API, no network — pure arithmetic you can unit-test.
🗺️ How to read this diagram

Read it left to right, like totalling a shopping bill: price each line, add them, then rescale.

  • The four stacked boxes on the left are the token buckets, each tagged with its own multiplier — input at 1×, cache-write ~1.25×, cache-read ~0.1×, and output at the (higher) output rate.
  • The arrows converging on "Σ sum" show every priced line being added into one subtotal — miss a bucket and the total is wrong.
  • The "÷ 1,000,000" box is the one rescaling step: prices are quoted per million tokens, so the sum is divided by a million to land in dollars.
  • The green "$" box is the payoff — a single number you can assert in a test because nothing here touches the network.

In short: Multiply each bucket by its rate, sum, divide by a million → dollars. The rates differ, so price every bucket, not just input and output.

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

A single call's cost is trivia; a month of calls with no record is a mystery you pay for. The failure mode is familiar to anyone who has run a service: the bill arrives, it's higher than expected, and there's no way to say which endpoint, which user, or which feature drove it. You end up bisecting production by intuition. The cure is boring and cheap — write down what each call cost as it happens — but almost nobody does it until after the first scary invoice. This recipe is you doing it before.

Think of it as double-entry bookkeeping for your model calls. Every time money leaves (a call is made), a row goes into the ledger: the model, the tokens, the dollar cost, and the request id. Do that consistently and questions that were once archaeology become one-line queries — "cost per feature this week," "which request ids are our most expensive," "did that new prompt actually get cheaper." The value isn't any single row; it's that the ledger exists at all, so the data is there when you need to ask.

The mistake to avoid is scattering this logic — computing cost one way in a script, another way in a notebook, a third way in the service — so your numbers never quite agree. The clean move is a single thin wrapper around messages.create that reads usage once and reuses the same offline cost function you already unit-tested in recipe 4. Tested offline, trusted online: the math that priced your fake receipts is the math pricing your real ones, with no second implementation to drift.

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.

tracked_create thin wrapper messages.create the real call read usage + cost_from_usage append row to LEDGER request_id · model usage · cost_usd offline math from recipe 4 — tested once, reused here One seam, every call logged. The wrapper forwards to the real messages.create, reads usage, prices it with the same offline cost_from_usage you unit-tested, then writes a ledger row (request id, model, tokens, dollars). Cost per feature becomes a query, not a guess.
🗺️ How to read this diagram

Follow the top row left to right — it is the life of a single tracked call — then look at what drops into the ledger below.

  • The blue "tracked_create" box is the one thin wrapper every call goes through, so no request escapes unlogged.
  • The purple "messages.create" box is the real API call, forwarded untouched; the wrapper adds behavior around it, not inside it.
  • The green "read usage" box reuses the same offline cost_from_usage from recipe 4 — the note underneath stresses "tested once, reused here", so live and test math never drift.
  • The amber card below is the ledger row that gets appended: request id, model, usage, and dollar cost — the four columns that turn a scary bill into a one-line query.

In short: Route every call through one wrapper that reads usage, prices it with the tested offline helper, and appends a row — so spend is recorded as it happens.

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

Nothing humbles a working prototype like the day it starts getting rejected under load — for a reason that has nothing to do with your prompt. Your calls were fine yesterday; today, at real traffic, they bounce with a 429. You stare at the prompt, it's small, it fits the window, so what gives? The answer is that there are two ceilings in this system, and the one that stopped you isn't the one you were watching. This section is about telling them apart, because conflating them sends you optimizing the wrong thing.

Here's the analogy that keeps it straight. The context window is the size of a single suitcase — how much you can pack into one request (input plus the space the answer needs). Rate limits are the airline's rule for how many suitcases you may check per hour across all your trips. A tiny suitcase (small prompt) that clears the size rule effortlessly can still be refused because you've already checked your hourly quota of bags. One limit is about the shape of a single request; the other is about your throughput over time. Fixing a window problem (trim the prompt) does nothing for a rate problem (slow down or raise the quota).

The beginner mistake, then, is to see a 429 and start hacking at the prompt size. That's treating a rate symptom with a window remedy. The right instinct is to read what the API is telling you: a 429 comes with a retry-after and x-ratelimit-* headers that spell out your remaining quota and how long to wait. The reassuring part — covered below — is that the SDK already retries these for you with backoff, so most of the time the correct action is to do nothing and let it recover; you reach for the raw headers only when you want to pace a bulk job or surface remaining capacity on a dashboard.

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:

ceiling 1 · context window one request (input + output) a single suitcase — must fit small prompt fits the window ✓ ceiling 2 · rate limit per minute / day · all requests RPM · ITPM · OTPM · TPD quota spent → 429 ✗ same tiny prompt, two verdicts under the window yet refused, because the per-minute quota is gone Two ceilings, not one. The context window sizes a single request; rate limits cap throughput over time across every request. A prompt can clear the window and still get a 429 because the account's per-minute token quota is exhausted.
🗺️ How to read this diagram

The dashed line splits the picture into the two separate limits that beginners conflate — read the left ceiling, then the right.

  • The left "context window" panel is one request. The small green "prompt" chip sits comfortably inside it — labelled fits the window ✓. This is the suitcase-size rule: does one request fit?
  • The right "rate limit" panel is throughput over time — RPM, ITPM, OTPM, TPD. Its red bar is nearly full: the per-minute quota is spent, so the verdict is 429 ✗.
  • The wide bottom box is the aha: the same tiny prompt gets two different verdicts. Passing the window says nothing about the rate — trimming the prompt won't fix a 429.

In short: count_tokens guards the window (one request); the x-ratelimit-* headers guard the rate (throughput). Diagnose which ceiling you hit before you fix anything.

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

This is the bug that passes every test and lies to production. You write a job that lists your batches, sums their costs, and reports the total. In dev you have eight batches, the number's right, you ship. Six months later the account has four hundred batches, your report still says a tidy sum — and it's wrong, because you were only ever counting the first page. Nothing errored. Nothing warned you. The code just quietly stopped telling the truth the moment the data outgrew one page. That silence is what makes pagination worth its own section.

Why do list endpoints page at all? Because a large account could hold thousands of files or batches, and dumping all of them in one response would be slow, memory-hungry, and fragile. So the API hands back a window onto the results — a page — plus a way to ask for the next one. Think of it like search results: page one shows ten hits, and "there are 4,000 results" doesn't mean they're all on the screen in front of you. Reading only what's on screen and calling it "all the results" is precisely the trap.

The good news is the SDK removes the tedium if you let it: iterating the object a .list() call returns transparently fetches page after page until the account is exhausted. The mistake — the one that caused the silent-undercount story above — is reaching for .data, which is only the current page, and treating it as the full set. The rule to internalize is short: iterate to get everything; index .data only when you truly want a single page. Get this reflex right and the "worked in dev, wrong in prod" class of pagination bugs simply never happens to you.

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.

.list() result = many pages page 1 page 2 page 3 … until exhausted for x in .list(): walks every page ✓ .data = page 1 only ✗ — silently drops the rest Iterate to get everything; index .data for one page. A .list() result is a chain of pages. Looping the result auto-fetches page after page to the end of the account; reading .data returns only the first page — the silent-undercount bug.
🗺️ How to read this diagram

Read the top row as the real shape of the data, then compare the two ways to consume it below.

  • The top row of "page" boxes with arrows is the account's full result — not one blob but a chain of pages, continuing "until exhausted".
  • The green bar is the right move: for x in .list() transparently follows that chain, walking every page so your totals are complete.
  • The red row is the trap: .data is only the first box in the chain. It never errors — it just quietly leaves out every later page, which is how a report "works in dev" and lies in prod.

In short: Iterate the list to see the whole account; reach for .data only when you truly want a single page.

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

Here's the shift that separates a lead from an engineer: the recipes above are things you do; a lead's job is to make them things the system guarantees. An individual remembering to count tokens is good hygiene; a platform where no request can reach billing without passing a budget gate is a contract. The difference matters because hygiene depends on everyone being careful forever, and they won't be — a rushed teammate ships the un-gated call, a new service forgets to log. A contract holds even on the bad day. Your task is to move each of these habits from "please remember" to "can't skip."

Think of it as the difference between telling drivers to be careful and installing guardrails. The four habits from this lesson become four rails: a pre-flight gate so an oversized prompt is refused before it costs anything; a usage-and-cost-and-request-id log on every call so spend is a query rather than a guess; a single dated price table so nobody quotes a stale number; and a hard budget cap plus max_tokens ceiling so one runaway job can't drain the month. None of these is clever on its own — the leadership is in making them non-optional and putting them where the tokens actually are.

The mistake leads make here is optimizing cost everywhere except where it lives. Chasing a 5% saving on a rarely-used route while the expensive path runs Opus on work Haiku could handle is motion, not progress. Attack the tokens: cache the stable prefixes (that 0.1× cache-read line is the biggest lever on repeated calls), route each request to the smallest model that clears your eval bar, and cap generation length so a loop can't run away. And keep the offline cost_from_usage helper dependency-free on purpose, so the same tested math prices both your CI assertions and your live traffic.

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 four recipes become four rails — non-optional, where the tokens are 1 · pre-flight gate count_tokens vs YOUR budget oversized prompt refused free 2 · log every call usage + cost + request_id spend is a query, not a guess 3 · one dated price table prices drift — single module nobody quotes a stale number 4 · hard cap + max_tokens route to smallest model that passes one runaway job can't drain the month Move each habit from "please remember" to "can't skip." A pre-flight gate, a log on every call, one dated price table, and a hard budget-plus-max_tokens cap — the leadership is making them non-optional and putting them where the tokens actually are.
🗺️ How to read this diagram

Read the four quadrants as a checklist — each is one recipe from this lesson turned into a system guarantee.

  • The amber "pre-flight gate" (1) refuses an oversized prompt against a budget you own — not just the model's window — before anything is billed.
  • The green "log every call" (2) writes usage, cost, and request id on every call, so spend becomes a query instead of a guess.
  • The blue "one dated price table" (3) keeps the drifting prices in a single module so no one quotes a stale number.
  • The purple "hard cap + max_tokens" (4) routes to the cheapest model that clears your evals and caps generation, so a single runaway job can't drain the month.

In short: Gate, log, one price table, hard cap — four rails that make token-and-cost discipline something the system enforces rather than something people must remember.

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