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.
Search across all documentation pages
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.
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:
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_cost isolates the pricing math from the request itself, so it can be reused for logging, gating, or comparison without duplicating the rate table.max_tokens budget is consumed, giving a conservative upper bound rather than an optimistic guess.send_with_budget only calls messages.create after the estimate clears the budget check, so a rejected request never reaches the API and never gets billed.BudgetExceeded exception lets calling code distinguish "too expensive" from a real API error and handle each differently.messages.count_tokens accepts the same model, system, tools, and messages parameters as messages.create, so it counts exactly the input tokens your real call would produce.max_tokens, since it never generates output, it only measures the input side.max_tokens as the assumed output length gives a true worst case, since the API will never generate more than that cap.output_tokens from prior similar calls instead of always assuming the full max_tokens ceiling.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.
| 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. |
system and tools to count_tokens. If your real request includes a system prompt or tools, omitting them from the count_tokens call under-counts significantly. Fix: always call count_tokens with the identical system/tools/messages you're about to send.count_tokens estimates output. It only ever returns input_tokens, there is no output field, because output doesn't exist until generation happens. Fix: use max_tokens (worst case) or a rolling average (typical case) for the output side.max_tokens will 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.2.00 and 10.00 literals across the codebase makes a pricing update error-prone. Fix: keep RATES as a single source of truth, ideally loaded from config rather than a Python literal, so it can be updated without a code deploy.estimate_cost call 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."count_tokens pricing 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.| 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 |
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.
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.
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.
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.
The returned input_tokens will undercount, since it only reflects what you actually passed. Always mirror the exact request shape you intend to send.
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.
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.
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.
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.
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.
Reviewed by Chris St. John·Last updated Jul 19, 2026