如何比較 token 計數方式各異的 AI 模型價格
How to Compare Model Prices When Each Model Counts Tokens Differently
要比較 AI 模型價格,應以各模型的 tokenizer 測量自有文本的 token 數,而非只看每 token 單價。
文章提供以自有文本測量各模型每千個字元的 token 數、換算任務成本並重設 token 限制的流程,呈現 tokenizer 差異對價格比較的影響。
Engineer's view. This is comparing cloud machines by price per vCPU when one provider's vCPU is a full core and the other's is half of one. The unit has the same name and a different size. A token is that kind of unit: every model defines its own.
TL;DR. Each model splits text into tokens with its own vocabulary, so the same prompt is a different number of tokens on each. A price per token only compares after you measure tokens per character on your own text. The same effect quietly shrinks context windows and output limits.
Explain Like I'm 5
Two shops sell rope by the "length." At the first shop, a length is your arm. At the second, a length is the shopkeeper's arm, which is shorter. Both signs say ten coins per length. You need enough rope to cross a river. At the second shop you need more lengths, so you pay more. The price signs matched, and the bills did not. To compare, measure the river once in your own steps. Then ask each shop how many of its lengths that is.
The Problem
You have shipped this bug before, in a system with no AI in it.
Your team picked a cloud machine by price per vCPU. Provider A was cheaper per vCPU, so the batch jobs moved there. The jobs ran slower, and the monthly bill went up. On provider B, a vCPU was a full core. On provider A, it was one hyperthread, about half a core. Both units were called vCPU. They were not the same size, so the comparison was wrong before anyone ran it.
Model pricing has the same bug. Every provider lists a price per million tokens, the chunks of text a model reads and writes. Comparison sites line those prices up in one table. Your usage dashboard and your plan's limits are in tokens too. It looks like one shared unit.
It is not. Each model cuts text into tokens its own way. In April 2026, Anthropic shipped Opus 4.7 at the same price as Opus 4.6, with a new tokenizer. OpenRouter then studied over a million real requests from users who switched. Their prompts now came out 32% to 45% longer in tokens. Simon Willison pasted one system prompt into both models. 5,039 tokens became 7,335. The price tag did not move, and the bill did.
The fix: convert every price to your own text before you compare, then recheck every limit you set in tokens.
The Fix: Measure Tokens per Character on Your Own Text
What is a token, exactly?
A tokenizer turns text into tokens before the model sees it. It is trained once, before the model. Training starts from single characters. It then keeps merging the pair that appears most often in its training text into a new, longer token. After enough merges, common words are one token each. Rare words, odd code and unfamiliar scripts stay split into small pieces. This method is called byte-pair encoding.
Why do two models disagree on the same text?
Each tokenizer learned from different text. In code_example.py, two toy tokenizers get the same number of merges. One trained on prose, the other on prose plus code. An 80-character line of Python costs 70 tokens on the first and 44 on the second. The prose sample costs 33 on both. Code, JSON and languages other than English vary the most.
So what number can I compare?
The cost of one of your tasks: price per token, times tokens per character, times characters sent. Say model B lists 17% cheaper but cuts your text into 35% more tokens. It costs 13% more per task.
Does 35% more tokens always mean a 35% bigger bill?
If nothing else changes, yes. Cached and uncached tokens scale alike. But a new model also changes how much it writes. OpenRouter found median answers on short prompts got 62% shorter, so that bill stayed flat. Above 2,000 tokens, costs rose 12% to 27%.
What This Means for You
When this matters. You choose between models by their price page. You switch the default model in Cursor, Claude Code or your own app. A provider ships a new version "at the same price." You run near a context limit, a usage cap or a tight max_tokens. In each case, a number you set or compare is measured in that model's tokens.
How it affects you. Your bill is the obvious part. The quieter part is every limit. A limit in tokens is a limit in text only for the model it was set on. Move to a tokenizer that makes 35% more tokens, and a 200,000-token window holds 26% less of your repository. An 800-token answer cap now stops at about 2,370 characters instead of 3,200. Long answers get cut off sooner, and nothing errors. Agents start compacting their history earlier. A weekly token allowance runs out sooner, for the same work.
This is the vCPU bug again. The number on the label is right. It is counted in a unit whose size you never checked.
What to do about it. Pick 20 to 50 real prompts and answers from your own logs. Count them with each candidate model's own token counter. Divide by characters to get tokens per thousand characters. Multiply by each price to get cost per task. Do the same check whenever a model changes version, even at an unchanged price. Then recompute every limit you set in tokens.
Implementing It
The change.
Three roles touch this: whoever picks the model, whoever tracks spend, and whoever sets limits.
Role 1, picking a model. Count your own sample with each provider's counter. Each counter counts with that model's tokenizer, so the ratio is honest:
import tiktoken, anthropic
from google import genai
def tokens_per_kchar(samples, count):
return 1000 * sum(count(s) for s in samples) / sum(len(s) for s in samples)
enc = tiktoken.get_encoding("o200k_base") # check which encoding your model uses
openai_tpk = tokens_per_kchar(samples, lambda s: len(enc.encode(s)))
claude = anthropic.Anthropic()
claude_tpk = tokens_per_kchar(samples, lambda s: claude.messages.count_tokens(
model=CLAUDE_MODEL, messages=[{"role": "user", "content": s}]).input_tokens)
gem = genai.Client()
gemini_tpk = tokens_per_kchar(samples, lambda s: gem.models.count_tokens(
model=GEMINI_MODEL, contents=s).total_tokens)
Use real prompts with their system prompt and tool definitions included. Those are often the largest part of each request. A sample of only short user messages will mislead you. Keep the sample in a file, so every later comparison counts the same text. Never estimate with "four characters per token." That rule of thumb was tuned on English prose for one tokenizer.
Role 2, tracking spend. Record cost per task, not tokens per day. A token count cannot be compared across models, and dollars can. Read the usage fields each response already returns:
u = resp.usage # Anthropic Messages API
dollars = (u.input_tokens * P_IN + u.cache_read_input_tokens * P_CACHE_READ
+ u.cache_creation_input_tokens * P_CACHE_WRITE + u.output_tokens * P_OUT) / 1e6
log.info("task_cost", task=task_id, model=MODEL, dollars=dollars, chars=len(prompt))
OpenAI reports prompt_tokens, completion_tokens and prompt_tokens_details.cached_tokens instead. Gemini reports them in usage_metadata. Group by task type, then compare models on the same tasks. Store the model name on every record. Without it, a model switch looks like a change in traffic.
Role 3, setting limits. Every limit in tokens was tuned for one tokenizer. First list them: search your code and config for max_tokens, context budgets, history truncation and compaction thresholds. Then rescale them when you switch:
import math
def rescale(limit_tokens, tpk_old, tpk_new):
return math.ceil(limit_tokens * tpk_new / tpk_old)
MAX_TOKENS = rescale(800, 250, 338) # 1082: same answer length in text
COMPACT_AT = min(rescale(150_000, 250, 338), # 202,800 would pass a 200k window,
int(0.8 * NEW_WINDOW)) # so cap at the model's real limit
The window itself does not grow. When the rescaled number passes it, less of your text fits, and you plan for that.
How you know it worked. Your cost-per-task chart is continuous across a model switch. A jump there is a real change, not a unit change. The truncation rate stays the same after the switch. If answers suddenly hit max_tokens more often, a limit was not rescaled. Finally, your measured tokens per thousand characters for each model sits in your notes. The next price comparison should take five minutes.
When Per-Character Comparison Is the Wrong Tool
When quality decides, not price. A model that solves the task in one try beats a cheaper one that needs three. Cost per task must include retries, and per-character math does not see them.
When you pay a flat seat price with no token cap. The tokenizer then changes nothing you pay. It still changes how much fits in the context window.
When your inputs are mostly images or audio. Those are counted by size, resolution or duration, with rules of their own. Measure them separately.
When the gap is small. A 5% difference in tokens per character is inside the noise of most samples. Do not switch models over it.
When the workload will change soon. Tokens per character depends on what you send. A ratio measured on chat prompts will not hold for a code agent.
Before you adopt it, ask:
- How many tokens per thousand characters does each model use on my own text?
- Which limits in my system are set in tokens?
- Did the last model switch change how long the answers are?
Glossary
- token — a chunk of text a model reads or writes; each model defines its own.
- tokenizer — the program that splits text into a model's tokens before the model sees it.
- byte-pair encoding — training a tokenizer by repeatedly merging the most frequent pair of symbols.
- tokens per thousand characters — how many tokens one model uses for 1,000 characters of your text.
- context window — the most tokens a model can read in one request.
Further reading
- Opus 4.7's New Tokenizer: What It Actually Costs — OpenRouter (Justin Summerville)
- Claude Token Counter, now with model comparisons — Simon Willison's Weblog
- tiktoken — OpenAI, GitHub
Originally published at theaicommit.com, a daily AI engineering lab for software engineers. The original has an interactive version you can play with, and the full runnable code is on GitHub.
來源:DEV Community · dev.to

