Building a Pre-Request Cost Calculator with the Token Counting API
A pipeline that only discovers a request's cost after the response comes back can't enforce a budget, it can only report a violation after the money is already spent.
messages.count_tokens closes that gap: it returns an exact input token count without running the model, so you can price a request before it ever hits the completion endpoint.
This page builds a reusable cost calculator around that call, then wires it into a gate that can reject or downgrade an oversized request before it's sent.
Recipe
Quick-reference recipe card, copy-paste ready.
import anthropic
client = anthropic.Anthropic()
RATES = {
"claude-sonnet-5": {"input": 2.00, "output": 10.00},
"claude-opus-4-8": {"input": 5.00, "output": 25.00},
"claude-haiku-4-5": {"input": 1.00, "output": 5.00},
}
def estimate_cost(client, model: str, max_tokens: int, **kwargs) -> float:
count = client.messages.count_tokens(model=model, **kwargs)
rate = RATES[model]
input_cost = (count.input_tokens / 1_000_000) * rate["input"]
worst_case_output_cost = (max_tokens / 1_000_000) * rate["output"]
return input_cost + worst_case_output_cost
cost = estimate_cost(
client,
model="claude-sonnet-5",
max_tokens=500,
messages=[{"role": "user", "content": "Draft a release note for v2.4."}],
)
print(f"${cost:.6f}")When to reach for this:
- You're building a pipeline where a single bad or oversized request (e.g. a user-pasted 50-page document) could blow a per-request or per-tenant budget.
- You want to log an estimated cost alongside a request before it's sent, for observability or approval workflows.
- You're comparing the cost of routing the same request to two or three model tiers before picking one.
- You need a hard ceiling for cost in an automated pipeline, not just a soft warning after the fact.
Working Example
import anthropic
client = anthropic.Anthropic()
RATES = {
"claude-sonnet-5": {"input": 2.00, "output": 10.00},
"claude-opus-4-8": {"input": 5.00, "output": 25.00},
"claude-haiku-4-5": {"input": 1.00, "output": 5.00},
}
class BudgetExceeded(Exception):
pass
def estimate_cost(client, model: str, max_tokens: int, **request_kwargs) -> dict:
"""Return a breakdown of estimated input cost, worst-case output cost, and total."""
count = client.messages.count_tokens(model=model, **request_kwargs)
rate = RATES[model]
input_cost = (count.input_tokens / 1_000_000) * rate["input"]
worst_case_output_cost = (max_tokens / 1_000_000) * rate["output"]
return {
"input_tokens": count.input_tokens,
"input_cost": input_cost,
"worst_case_output_cost": worst_case_output_cost,
"worst_case_total": input_cost + worst_case_output_cost,
}
def send_with_budget(client, max_cost_usd: float, model: str, max_tokens: int, **request_kwargs):
"""Estimate cost, enforce a budget, then send the request only if it fits."""
breakdown = estimate_cost(client, model, max_tokens, **request_kwargs)
if breakdown["worst_case_total"] > max_cost_usd:
raise BudgetExceeded(
f"Worst-case cost ${breakdown['worst_case_total']:.4f} exceeds "
f"budget ${max_cost_usd:.4f} for model {model}"
)
response = client.messages.create(model=model, max_tokens=max_tokens, **request_kwargs)
return response, breakdown
try:
response, breakdown = send_with_budget(
client,
max_cost_usd=0.02,
model="claude-sonnet-5",
max_tokens=500,
messages=[{"role": "user", "content": "Draft a release note for v2.4."}],
)
print(response.content[0].text)
print(f"Estimated: ${breakdown['worst_case_total']:.6f}")
except BudgetExceeded as e:
print(f"Rejected: {e}")What this demonstrates:
estimate_costisolates the pricing math from the request itself, so it can be reused for logging, gating, or comparison without duplicating the rate table.- The worst-case output estimate assumes the full
max_tokensbudget is consumed, giving a conservative upper bound rather than an optimistic guess. send_with_budgetonly callsmessages.createafter the estimate clears the budget check, so a rejected request never reaches the API and never gets billed.- Raising a dedicated
BudgetExceededexception lets calling code distinguish "too expensive" from a real API error and handle each differently.
Deep Dive
How It Works
messages.count_tokensaccepts the samemodel,system,tools, andmessagesparameters asmessages.create, so it counts exactly the input tokens your real call would produce.- It does not accept or need
max_tokens, since it never generates output, it only measures the input side. - The call itself does not consume billable tokens, it is a metering operation, not a generation operation, so calling it liberally in a pre-flight check adds no meaningful cost.
- Output cost can only ever be estimated, never counted exactly, before the call, because the model hasn't generated anything yet. Using
max_tokensas the assumed output length gives a true worst case, since the API will never generate more than that cap. - For a tighter (but non-guaranteed) output estimate, you can track a rolling average of actual
output_tokensfrom prior similar calls instead of always assuming the fullmax_tokensceiling.
Building a Tighter Estimate with Historical Averages
class RollingOutputEstimator:
"""Tracks actual output token usage per model to refine the worst-case guess."""
def __init__(self):
self._history: dict[str, list[int]] = {}
def record(self, model: str, output_tokens: int) -> None:
self._history.setdefault(model, []).append(output_tokens)
def typical(self, model: str, fallback: int) -> int:
samples = self._history.get(model)
if not samples:
return fallback
return sum(samples) // len(samples)
estimator = RollingOutputEstimator()
def estimate_cost_v2(client, model: str, max_tokens: int, **request_kwargs) -> dict:
count = client.messages.count_tokens(model=model, **request_kwargs)
rate = RATES[model]
input_cost = (count.input_tokens / 1_000_000) * rate["input"]
typical_output = estimator.typical(model, fallback=max_tokens)
typical_output_cost = (typical_output / 1_000_000) * rate["output"]
return {"input_cost": input_cost, "typical_output_cost": typical_output_cost}This pattern trades a guaranteed upper bound for a more realistic estimate once you have real usage data, useful for dashboards where you want a representative number rather than a worst case.
Parameters & Return Values
| Parameter | Type | Description |
|---|---|---|
model | str | Required, same model identifier used for messages.create. |
system | str | list | None | Optional, mirrors the system you'd send in the real request. |
tools | list | None | Optional, mirrors the tools you'd send; large tool catalogs count toward input_tokens. |
messages | list | Required, the same message list you'd send to messages.create. |
count.input_tokens | int | The return value: exact input token count for the given request shape. |
Gotchas
- Forgetting to pass
systemandtoolstocount_tokens. If your real request includes a system prompt or tools, omitting them from thecount_tokenscall under-counts significantly. Fix: always callcount_tokenswith the identicalsystem/tools/messagesyou're about to send. - Assuming
count_tokensestimates output. It only ever returnsinput_tokens, there is no output field, because output doesn't exist until generation happens. Fix: usemax_tokens(worst case) or a rolling average (typical case) for the output side. - Treating the worst-case total as the expected cost. A budget gate built on
max_tokenswill often reject requests that would have finished well under budget in practice. Fix: use the worst-case gate for hard budget enforcement, but track actual costs separately for realistic reporting. - Hardcoding rates inline instead of in one shared table. Scattering
2.00and10.00literals across the codebase makes a pricing update error-prone. Fix: keepRATESas a single source of truth, ideally loaded from config rather than a Python literal, so it can be updated without a code deploy. - Not re-checking budget after a model fallback. If your pipeline retries a rejected request on a cheaper model, skipping a second
estimate_costcall means you're trusting the fallback fits without verifying it. Fix: re-run the estimate for the fallback model, don't just assume "cheaper model" always means "under budget." - Ignoring that
count_tokenspricing rates themselves can change. A rate table baked in at authoring time will silently drift from reality after a pricing update. Fix: review the rate table on a schedule, or better, pull it from a config source you control independently of code deploys.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Pre-request count_tokens gate (this page) | You need to reject or reroute before paying for a call | Latency to the gate check itself matters more than budget precision |
Post-request usage logging only | You want cost visibility without blocking any requests | You need a hard budget ceiling that can never be exceeded |
| Approximate token estimate via character count | You need a fast, rough guess with no API round-trip | You need exact figures for a hard budget decision |
| Server-side spend caps (platform-level) | You want an account-wide backstop independent of application code | You need per-request or per-tenant granularity |
FAQs
Does calling count_tokens cost anything?
No, it's a metering operation that doesn't run the model or generate output, so it doesn't consume billable input or output tokens.
Can count_tokens tell me how many output tokens a request will use?
No, output tokens don't exist until generation happens. The best you can do pre-call is a worst-case estimate using max_tokens, or a typical estimate from historical averages.
Why does my worst-case cost estimate look much higher than what I actually got billed?
Because it assumes the full max_tokens ceiling was used as output, which is a true upper bound, not a prediction of actual usage. Most completions finish well under their max_tokens cap.
Should every request in my pipeline go through this gate?
It's most valuable for requests with unpredictable size, like user-supplied documents, and less necessary for fixed-shape internal calls where you already know the token count is small and stable.
What happens if I omit system or tools from the count_tokens call?
The returned input_tokens will undercount, since it only reflects what you actually passed. Always mirror the exact request shape you intend to send.
Is a rolling average estimator safe to use for a hard budget cap?
Not on its own, since it can underestimate on an unusually long response. Use it for reporting or soft warnings, and keep the max_tokens worst case for any hard enforcement.
How often should I update the RATES table?
Whenever pricing changes are announced, and especially around known transition dates like intro-pricing expirations. Storing rates in external config rather than code makes this a data update, not a deploy.
Can I use this same pattern to compare cost across models before choosing one?
Yes, call estimate_cost once per candidate model with the same request shape and compare the totals, that's a direct extension of this pattern.
Does this calculator account for prompt caching discounts?
Not as written, it assumes full-price input tokens. A caching-aware version would need to know which portion of the prefix is expected to hit cache, which count_tokens alone can't tell you.
Related
- Token Economics Basics - the underlying count_tokens and usage patterns this calculator builds on.
- How Claude Token Pricing Actually Works - why the rate table has separate input and output prices per model.
- Model Tiering: Routing Simple Tasks to Haiku, Hard Tasks to Opus - using a cost estimate as one input to a routing decision.
- Batch API Economics: When 50 Percent Off Beats Real-Time Calls - extending this kind of calculator to compare sync vs batch pricing.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, pricing, and SDK versions move quickly - verify current specifics at platform.claude.com/docs before relying on them.