How do you count tokens before calling an LLM API? Send the exact request payload to the provider's counting endpoint: POST /v1/responses/input_tokens on OpenAI, POST /v1/messages/count_tokens on Anthropic, or countTokens on Gemini. Each returns the input token count for that model. A local tokenizer such as tiktoken is faster and free, but it only counts plain text correctly; tools, images, files and the formatting around messages are where local counts go wrong.
This guide covers the three providers' methods, what each counts, and how to turn a count into a cost bound you can act on.
Why count before the call
Four decisions need a token count before the provider sees the request:
- Cost. Input count times input rate, plus the output limit times the output rate, is the most a request can cost. That is what a budget check needs.
- Context fit. A prompt that will not fit the model's window should be trimmed or summarised before it is sent, not after the 400.
- Pricing thresholds. Some rates change with prompt length: OpenAI above 272K input tokens, Claude Haiku 5.5 above 100,000, Gemini 3.1 Pro above 200K. A request just over the line is re-priced as a whole.
- Routing. Small prompts can go to a small model and very large ones to a model with the window for them.
The three providers' methods
| Provider | Method | Counts | Cost to call | Notes |
|---|---|---|---|---|
| OpenAI | POST /v1/responses/input_tokens | Text, messages, images, files (PDF), tools, formatting | Not stated | Same payload as a Responses request |
| Anthropic | POST /v1/messages/count_tokens | System prompt, messages, tools, base64 images and PDFs, thinking | Free; 5,000–20,000 RPM by tier | Refuses server tools and URL-sourced media |
models.count_tokens / countTokens | Contents, system instructions, tool definitions | Not stated | Returns input total only | |
| Local | tiktoken and similar | Plain text | Free | No images, files, tools or message formatting |
OpenAI
OpenAI's counting endpoint accepts the same input as responses.create and returns {"object": "response.input_tokens", "input_tokens": N}. Its guide names exactly where local counting fails: images and files ("estimates like characters / 4 are inaccurate"), tool and schema definitions, and model-specific behaviour. It also notes that the count "includes formatting tokens used to represent request structure, such as message roles and boundaries"1.
from openai import OpenAI
client = OpenAI()
count = client.responses.input_tokens.count(
model="gpt-6-sol",
instructions=open("system_prompt.txt").read(),
input="Summarise the attached ticket thread.",
tools=[{"type": "function", "name": "lookup_order", "parameters": {"type": "object", "properties": {}}}],
)
print(count.input_tokens)For plain text in a hot path, tiktoken is OpenAI's open-source BPE tokenizer: tiktoken.encoding_for_model("gpt-4o") returns that model's encoding, and o200k_base and cl100k_base can be loaded by name2. Treat a local count as a lower bound for a chat request, because it does not see roles, tools or attachments.
Anthropic
Anthropic's endpoint takes the same body as a message request — system prompt, messages, tools, images and PDFs as base64, thinking settings — and returns input_tokens. It is free, rate-limited per minute by usage tier, and its limits are independent of message creation3. Three behaviours to know:
- It is an estimate. Anthropic says the actual input tokens "might differ by a small amount", and that tokens it adds for system optimisations are not billed3.
- It refuses some inputs. Server tools (web search, web fetch, code execution), the MCP connector and images or documents given by URL or file id return
invalid_request_error. Count those requests from theusageof the real response instead. - It counts per model. Claude 4.7 and later, including the Fable family, use a tokenizer that produces roughly 30% more tokens for the same text. Anthropic's advice is to recount against the model you plan to use rather than reuse old counts3.
curl https://api.anthropic.com/v1/messages/count_tokens \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-5-5",
"system": "You are a support assistant.",
"messages": [{"role": "user", "content": "Where is order 4471?"}]
}'
# {"input_tokens": ...}On a conversation with extended thinking, previous turns' thinking blocks count as input on models that keep them, and the current turn's thinking counts too3.
Google Gemini
Gemini's SDKs expose client.models.count_tokens (Python) and countTokens (JavaScript), which return the input total; Google recommends calling it before sending large inputs4. Its guide also gives fixed costs for media that are useful for estimates without a call:
- Text: about 4 characters per token; 100 tokens is about 60 to 80 English words.
- Images: 258 tokens up to 384 pixels on each side; larger images are tiled at 768 by 768, 258 tokens per tile.
- Audio: 32 tokens per second.
- System instructions and tool definitions count as input4.
from google import genai
client = genai.Client()
result = client.models.count_tokens(
model="gemini-3.8-flash",
contents="Summarise the attached ticket thread.",
)
print(result.total_tokens)What no endpoint can count
Output. Generated tokens, including reasoning or thinking tokens, exist only after generation. OpenAI bills reasoning as output and reports it afterwards; Gemini's output price includes thinking tokens. The only pre-call control is the output limit: max_output_tokens or max_completion_tokens on OpenAI, max_tokens on Anthropic, maxOutputTokens on Gemini. Price the limit, not the expected length.
That gives a simple upper bound:
max_cost = input_tokens × input_rate + max_output_tokens × output_rate
# input_tokens from the counting endpoint, rates per token for the target model
# a cached prefix lowers the real cost; it never raises the bound above thisCounting across providers
A count is only meaningful for the model it was taken on. The same prompt can produce materially different counts on OpenAI, Anthropic and Google, and even between two generations of one vendor's models. Three consequences:
- Do not compare rates without counts. A model with a lower rate per token can cost more per request if its tokenizer produces more tokens for your text. Compare cost per request on your own prompts.
- Re-count on migration. Moving a workload between providers, or across Claude's 4.6-to-4.7 tokenizer change, changes the count before it changes the rate.
- Keep counts per target. A gateway that may fail over from one provider to another should bound cost with the most expensive target in the chain.
Local or endpoint: which to use where
| Situation | Use |
|---|---|
| Hot path, plain text, rough sizing | A local tokenizer, with a safety margin |
| A budget or spend-cap decision | The provider's endpoint, or the real response's usage afterwards |
| Requests with images, PDFs or many tools | The provider's endpoint |
| Requests near a pricing or context threshold | The provider's endpoint |
| After the call, for accounting | The usage object of the response — never a re-count |
Endpoint calls add latency, so where counting sits on the request path, cache counts for stable prefixes (system prompts, tool lists) and count only the changing part.
Counting with INFRO
INFRO's gateway needs a bound before every request it serves, because budgets are checked before the provider is called: it reserves a worst-case estimate from the prompt size and the request's output limit, admits the request only if that fits the room left, and records the provider-reported usage afterwards. You can ask for the same estimate yourself with POST /v1/estimates without sending the request. For Claude Code and other clients on INFRO's /v1/messages, POST /v1/messages/count_tokens asks the first Anthropic connection that serves the model, is free, and leaves no request row; with no such connection the answer is an estimate and the X-INFRO-Token-Count: estimated header says so.
Summary
Count where the number decides something, with the exact payload, on the exact model. Use local tokenizers only as a fast lower bound for plain text, bound output with an explicit limit, and treat the usage in each response as the figure of record.
