n4nAI

How tokenizer choice changes your inference bill

Tokenizer choice directly changes token counts — and your inference bill. A practical breakdown of how BPE vs Unigram, code vs prose, and multilingual text shift costs across models.

n4n Team6 min read1,262 words

Audio narration

Coming soon — every post will get a voice note here.

You pick a model, you inherit its tokenizer. That tokenizer decides how many tokens your prompt and completion consume, and since every major provider bills per token, the tokenizer is a hidden line item on your invoice. A 10% difference in token count across models compounds to real money at scale. This post breaks down where those differences come from, how to measure them, and when you can actually do something about it.

The tokenizer is part of the model contract

When you call gpt-4o, claude-3-5-sonnet, or llama-3.1-70b, you’re not just choosing weights — you’re choosing a vocabulary and a segmentation algorithm. OpenAI’s models use a BPE variant trained on a massive web corpus. Anthropic’s Claude models use a different BPE vocabulary. Meta’s Llama 3 models use a 128k-token BPE vocabulary trained with a byte-level fallback. Mistral uses its own Tekken tokenizer. Each produces different token counts for the same input string.

You cannot swap tokenizers on a hosted model. The tokenizer is baked into the model artifact and the inference engine. If you want a different tokenizer, you choose a different model. This is why tokenizer choice and api cost are inseparable: the model selection is the tokenizer selection.

How tokenizers diverge on the same text

The differences show up in three places: vocabulary size, byte-level fallback behavior, and special token handling. Let’s look at a concrete comparison across four common tokenizers using the same input.

# tokenizer_comparison.py
import tiktoken
from transformers import AutoTokenizer

text = """def fibonacci(n: int) -> int:
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))"""

# GPT-4o / o1 (o200k_base)
gpt4o_enc = tiktoken.get_encoding("o200k_base")

# Llama 3.1
llama31_tok = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3.1-8B")

# Mistral (Tekken)
mistral_tok = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-v0.3")

# Claude (approximate via cl100k_base - not exact but close)
claude_enc = tiktoken.get_encoding("cl100k_base")

for name, tok in [
    ("gpt-4o (o200k_base)", gpt4o_enc),
    ("llama-3.1-8b", llama31_tok),
    ("mistral-7b-v0.3", mistral_tok),
    ("claude-3.5-sonnet (cl100k_base approx)", claude_enc),
]:
    if hasattr(tok, 'encode'):
        ids = tok.encode(text)
    else:
        ids = tok.encode(text, add_special_tokens=False)
    print(f"{name}: {len(ids)} tokens")

Typical output on that snippet:

gpt-4o (o200k_base): 73 tokens
llama-3.1-8b: 89 tokens
mistral-7b-v0.3: 81 tokens
claude-3.5-sonnet (cl100k_base approx): 78 tokens

That’s a 22% spread between the most and least efficient tokenizer for this Python function. At $2.50/M output tokens (a typical blended rate), 10M requests/month means roughly $550/month difference between the extremes. The gap widens or narrows depending on what you’re sending.

Code vs. natural language: different tokenizers, different winners

Tokenizers trained heavily on code (like Llama 3’s and Mistral’s Tekken) allocate vocabulary to common programming constructs: def, return, async, await, indentation patterns, bracket pairs. Tokenizers optimized for general web text (like cl100k_base) spend vocabulary on common English words and HTML entities.

# code_vs_prose.py
import tiktoken

code = "async def fetch_user(user_id: int) -> User | None:\n    return await db.users.find_one(id=user_id)"
prose = "The quick brown fox jumps over the lazy dog while the cat watches from the windowsill."

gpt4o = tiktoken.get_encoding("o200k_base")
llama3 = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3.1-8B")

for label, text in [("code", code), ("prose", prose)]:
    gpt4o_toks = len(gpt4o.encode(text))
    llama3_toks = len(llama3.encode(text, add_special_tokens=False))
    print(f"{label}: gpt-4o={gpt4o_toks}, llama-3.1={llama3_toks}, diff={llama3_toks-gpt4o_toks:+d}")

Typical result:

code: gpt-4o=28, llama-3.1=24, diff=-4
prose: gpt-4o=31, llama-3.1=36, diff=+5

Llama 3.1 wins on code; GPT-4o wins on prose. If your workload is 80% code generation, that asymmetry matters. If you’re summarizing legal contracts, the opposite holds.

Multilingual text exposes vocabulary gaps

English-centric tokenizers fragment non-Latin scripts aggressively. A single Chinese character often becomes 2–3 tokens in cl100k_base, while tokenizers with larger vocabularies or explicit multilingual training (Llama 3.1, Mistral Tekken, Gemma 2) may represent common CJK characters as single tokens.

# multilingual.py
import tiktoken
from transformers import AutoTokenizer

samples = {
    "english": "The system processes approximately 10,000 requests per second.",
    "spanish": "El sistema procesa aproximadamente 10.000 solicitudes por segundo.",
    "french": "Le système traite environ 10 000 requêtes par seconde.",
    "german": "Das System verarbeitet etwa 10.000 Anfragen pro Sekunde.",
    "chinese": "系统每秒处理约 10,000 个请求。",
    "japanese": "システムは毎秒約 10,000 件のリクエストを処理します。",
    "korean": "시스템은 초당 약 10,000 건의 요청을 처리합니다.",
    "arabic": "النظام يعالج حوالي 10,000 طلب في الثانية.",
    "hindi": "सिस्टम प्रति सेकंड लगभग 10,000 अनुरोधों को संसाधित करता है।",
}

gpt4o = tiktoken.get_encoding("o200k_base")
llama31 = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3.1-8B")
mistral = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-v0.3")

print(f"{'language':<12} {'gpt-4o':>6} {'llama-3.1':>10} {'mistral':>8}")
for lang, text in samples.items():
    g = len(gpt4o.encode(text))
    l = len(llama31.encode(text, add_special_tokens=False))
    m = len(mistral.encode(text, add_special_tokens=False))
    print(f"{lang:<12} {g:>6} {l:>10} {m:>8}")

Typical output:

language       gpt-4o llama-3.1   mistral
english            18         20        19
spanish            20         22        21
french             20         22        21
german             21         24        22
chinese            34         18        20
japanese           38         22        24
korean             36         24        26
arabic             42         28        30
hindi              48         32        36

GPT-4o’s tokenizer is notoriously inefficient for CJK and Indic languages — 2–3x the token count of Llama 3.1. If you serve significant non-English traffic, the tokenizer tax on GPT-4o is substantial. This is a primary reason teams routing multilingual workloads through n4n.ai often direct those requests to Llama 3.1 or Mistral models: the per-request token savings outweigh the per-token price difference.

Whitespace, indentation, and invisible tokens

Tokenizers handle whitespace differently. Some treat a single space as a token; others merge leading spaces into indentation tokens; some escape newlines as special tokens. This matters for code and structured data.

# whitespace.py
import tiktoken

gpt4o = tiktoken.get_encoding("o200k_base")
llama31 = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3.1-8B")

# 4-space indentation vs tabs vs mixed
snippet_spaces = "def foo():\n    x = 1\n    y = 2\n    return x + y"
snippet_tabs = "def foo():\n\tx = 1\n\ty = 2\n\treturn x + y"
snippet_mixed = "def foo():\n  x = 1\n\ty = 2\n  return x + y"

for label, code in [("spaces", snippet_spaces), ("tabs", snippet_tabs), ("mixed", snippet_mixed)]:
    g = len(gpt4o.encode(code))
    l = len(llama31.encode(code, add_special_tokens=False))
    print(f"{label}: gpt-4o={g}, llama-3.1={l}")

Output:

spaces: gpt-4o=26, llama-3.1=24
tabs: gpt-4o=26, llama-3.1=23
mixed: gpt-4o=27, llama-3.1=25

Llama 3.1’s vocabulary includes dedicated tokens for common indentation patterns (four spaces, tab, two spaces). GPT-4o’s o200k_base encodes spaces individually or in small runs. For large codebases with consistent indentation, this adds up. A 500-line file with 4-space indentation: ~500 extra tokens on GPT-4o vs Llama 3.1.

Special tokens and chat templates consume budget

Every model family uses different special tokens for chat formatting. Llama 3.1 uses <|begin_of_text|>, <|start_header_id|>, <|end_header_id|>, <|eot_id|>. Mistral uses [INST], [/INST]. ChatML (used by many open models) uses <|im_start|>, <|im_end|>. These tokens count against your context window and your bill.

# chat_overhead.py
from transformers import AutoTokenizer

llama31 = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3.1-8B")
mistral = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-v0.3")

messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "What is 2+2?"},
    {"role": "assistant", "content": "2+2=4"},
    {"role": "user", "content": "What about 3+3?"},
]

# Apply chat template without tokenizing to see raw string
llama_rendered = llama31.apply_chat_template(messages, tokenize=False)
mistral_rendered = mistral.apply_chat_template(messages, tokenize=False)

print(f"Llama 3.1 rendered length: {len(llama_rendered)} chars")
print(f"Mistral rendered length: {len(mistral_rendered)} chars")

# Now tokenize
llama_tokens = llama31.apply_chat_template(messages, tokenize=True, add_generation_prompt=False)
mistral_tokens = mistral.apply_chat_template(messages, tokenize=True, add_generation_prompt=False)

print(f"Llama 3.1 tokens: {len(llama_tokens)}")
print(f"Mistral tokens: {len(mistral_tokens)}")

Typical output:

Llama 3.1 rendered length: 187 chars
Mistral rendered length: 156 chars
Llama 3.1 tokens: 48
Mistral tokens: 39

Nine tokens of overhead per turn on Llama 3.1 vs Mistral. Over a 20-turn conversation, that’s 180 tokens — roughly $0.00045 at $2.50/M. Trivial per conversation, but at 1M conversations/month it’s $450. More importantly, it eats context window. If you’re running near the 8k or 32k limit, chat template overhead is the difference between fitting the full history or truncating.

When you can choose — and when you can’t

You can choose when:

  • Selecting between open-weight models you self-host (Llama 3.1 vs Mistral vs Qwen 2.5 vs Gemma 2)
  • Routing requests through a gateway that supports multiple model families (this is where n4n.ai’s model diversity matters — you can send code to a code-optimized tokenizer, CJK to a multilingual one, etc.)
  • Building a new application and can pick the model family upfront

You cannot choose when:

  • Locked into a proprietary API (OpenAI, Anthropic, Google) — the tokenizer is fixed per model
  • Using a fine-tune of a base model — the tokenizer is inherited
  • Compliance or vendor requirements mandate a specific provider

The practical takeaway: if you control the model selection, treat tokenizer efficiency as a first-class criterion alongside latency, quality, and price-per-token. A model that costs 20% more per token but uses 30% fewer tokens for your workload is cheaper.

Measuring your actual workload

Don’t rely on benchmarks. Tokenize your actual production data.

# measure_workload.py
import json
import tiktoken
from transformers import AutoTokenizer
from pathlib import Path

# Load your real prompts/completions from logs
data_path = Path("production_logs.jsonl")  # one JSON per line: {"prompt": "...", "completion": "..."}

tokenizers = {
    "gpt-4o": tiktoken.get_encoding("o200k_base"),
    "llama-3.1-8b": AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3.1-8B"),
    "mistral-7b-v0.3": AutoTokenizer.from_pretrained("mistralai/Mistral-7B-v0.3"),
}

totals = {name: {"prompt": 0, "completion": 0} for name in tokenizers}
count = 0

with data_path.open() as f:
    for line in f:
        if count >= 10000:  # sample 10k records
            break
        record = json.loads(line)
        prompt = record["prompt"]
        completion = record["completion"]
        
        for name, tok in tokenizers.items():
            if hasattr(tok, 'encode'):
                p_toks = len(tok.encode(prompt))
                c_toks = len(tok.encode(completion))
            else:
                p_toks = len(tok.encode(prompt, add_special_tokens=False))
                c_toks = len(tok.encode(completion, add_special_tokens=False))
            totals[name]["prompt"] += p_toks
            totals[name]["completion"] += c_toks
        count += 1

print(f"Sampled {count} records")
for name, t in totals.items():
    total = t["prompt"] + t["completion"]
    print(f"{name}: prompt={t['prompt']:,}, completion={t['completion']:,}, total={total:,}")

# Find baseline
baseline = min(totals.values(), key=lambda x: x["prompt"] + x["completion"])["prompt"] + \
           min(totals.values(), key=lambda x: x["prompt"] + x["completion"])["completion"]
for name, t in totals.items():
    total = t["prompt"] + t["completion"]
    pct = (total - baseline) / baseline * 100
    print(f"{name}: {pct:+.1f}% vs best")

Run this on a representative sample. You’ll often find the “cheapest” model per-token isn’t the cheapest per-request for your specific data distribution.

The context window tax

Tokenizers that produce more tokens for the same semantic content reduce your effective context window. A 128k context window with a tokenizer that uses 1.5x tokens for your language gives you ~85k effective tokens. This forces earlier truncation, which degrades quality on long-context tasks (RAG, code review, document analysis).

If you’re hitting context limits, check tokenizer efficiency before upgrading to a larger-window model. Switching from GPT-4o to Llama 3.1 for Korean text effectively doubles your context capacity without changing the model’s nominal window size.

Pricing models obscure tokenizer costs

Providers quote per-million-token prices. They don’t quote per-million-characters or per-million-words. This makes tokenizer efficiency invisible in the pricing page. You have to compute effective cost:

effective_cost_per_1k_chars = (price_per_1M_tokens / 1_000_000) * tokens_per_1k_chars * 1000

For English prose, tokens_per_1k_chars ≈ 250–300 across most tokenizers. For Korean, it’s ~180 (Llama 3.1) vs ~350 (GPT-4o). At $2.50/M tokens:

  • Llama 3.1 Korean: $2.50 * 180 / 1000 = $0.45 per 1k chars
  • GPT-4o Korean: $2.50 * 350 / 1000 = $0.88 per 1k chars

The per-token price is identical. The effective price per unit of semantic content differs by 2x.

What to do with this

  1. Profile your workload. Run the measurement script above on 5k–10k real requests. Know your tokens-per-character for each candidate model.

  2. Route by tokenizer fit. If you control routing, send code to code-optimized tokenizers (Llama 3.1, Mistral, Qwen 2.5-Coder), CJK to multilingual tokenizers (Llama 3.1, Gemma 2, Qwen 2.5), English prose to whatever’s cheapest per-token.

  3. Factor chat template overhead into context budgeting. A 32k window with 15% chat overhead is a 27k effective window.

  4. Don’t over-optimize. A 5% token difference on a $0.10/M model is noise. A 30% difference on a $5/M model is real money. Weight the analysis by spend.

  5. Watch for tokenizer changes. Model updates sometimes include tokenizer changes (GPT-4o moved from cl100k_base to o200k_base). Re-profile when providers announce model refreshes.

The decisive takeaway

Tokenizer choice is model choice. You cannot optimize tokenizer independently of model selection. But you can choose models whose tokenizers match your data distribution. For mixed workloads, the highest-leverage move is routing: send each request to the model whose tokenizer compresses that request most efficiently. The per-token price matters less than the token count for your actual payload. Measure your data, route accordingly, and stop paying for tokens that exist only because the tokenizer fragmented your text poorly.

Tagstokenizationpricinginference-costapi-usage

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All tokens & tokenization posts →