Why does the same text cost more in Vietnamese?
Because the tokenizer was trained mostly on English. The same support notice is 163 tokens in English and 237 in Vietnamese — 1.45x for identical meaning — so a Vietnamese user pays 45% more to say the same thing. Measured with o200k_base and cl100k_base across seven languages, plus the accent test
The same document costs 1.45x more in Vietnamese than in English — 237 tokens against 163 for identical meaning — so a Vietnamese user pays 45% more to send the same request. Under the older cl100k_base tokenizer it was 2.69x. That is the headline finding and also the good news: the gap is real, and it has narrowed sharply.
LLM pricing is quoted per token, never per word or per idea. A token is whatever the tokenizer decides it is, and every production BPE tokenizer was fitted to a corpus that is mostly English. The consequence is a per-language tax that nobody puts on the pricing page. Our users write in Vietnamese, so we measured it.
Hardware: Apple M3, 16 GB, macOS 26.4.1. Node v23.5.0 arm64, gpt-tokenizer 4.0.0, cross-checked against js-tiktoken 1.0.21. Everything runs offline — the token counts are computed locally by the same BPE merge tables the API uses, not estimated and not billed. All fourteen counts matched exactly between the two libraries, so the numbers below are not an artifact of one implementation.
On the corpus. I wrote two short documents — a support-and-billing notice (143 English words) and a product-care notice (49 words) — and then wrote each one in Vietnamese, Chinese, Japanese, Thai, French and German myself, keeping the same eleven statements in the same order. Japanese renders one of those statements as two sentences, and Thai does not mark sentence ends with a period; the content is matched, not the punctuation. I am not a professional translator; the wording is plain and idiomatic enough to be fair, but a certified translation would move these numbers by a few percent. It would not move them by a factor. Two independent documents are reported precisely so you can see which findings survive a change of subject matter.
The short answer
- Vietnamese costs 1.45x more tokens than English for the same document (237 against 163) under
o200k_base, the encoding used by GPT-4o and current models. Across both test documents the combined figure is 1.37x. - The old tokenizer was far worse. On
cl100k_basethe same Vietnamese text was 439 tokens — 2.69x English. Moving too200k_basecut Vietnamese by 46% and Thai by 59%, while leaving English unchanged at 0.0%. - Chinese is now essentially at parity with English — 170 tokens against 163, a 1.04x ratio — and it is cheaper than French (1.28x) or German (1.26x). That was not the expected result.
- Stripping Vietnamese accents now makes text more expensive, not less. Removing diacritics raised the count from 237 to 262 tokens (+10.5%) on
o200k_base, while oncl100k_basethe identical operation saved 28.5%. The common advice reversed when the tokenizer changed. - A 128,000-token context holds 785 copies of the English document and 540 of the Vietnamese one — 0.69x the meaning for the same window, and 0.60x in Thai.
How many tokens is a Vietnamese word?
Usually two, where the English equivalent is one. The mechanism is visible at the level of a single phrase. Here is o200k_base on a greeting, with each token quoted:
"Xin" " ch" "ào" "," " hôm" " nay" " bạn" " có" " khỏe" " không" "?" 11 tokens
"Hello" "," " how" " are" " you" " today" "?" 7 tokens
Xin chào is two words in Vietnamese and three tokens; Hello is one word and one token. Common vocabulary splits the same way — thanh toán (payment) is 2 tokens against 1 for payment, lời nhắc (reminder) is 3 against 1, and khách hàng (customer) is 2 against 1.
Here is the full corpus. Characters are Unicode code points, bytes are UTF-8:
| Language | Script | Chars | Bytes | o200k | cl100k | tok/char (o200k) |
|---|---|---|---|---|---|---|
| English | Latin | 793 | 793 | 163 | 163 | 0.206 |
| Vietnamese | Latin + diacritics | 903 | 1,201 | 237 | 439 | 0.262 |
| Chinese | Han | 257 | 771 | 170 | 248 | 0.661 |
| Japanese | Kana + Han | 352 | 1,056 | 250 | 355 | 0.710 |
| Thai | Thai | 721 | 2,127 | 270 | 661 | 0.374 |
| French | Latin | 962 | 986 | 209 | 243 | 0.217 |
| German | Latin | 970 | 984 | 205 | 253 | 0.211 |
Tokens per character is the wrong metric to reason about cost, and the table shows why: Chinese has the second-highest tokens-per-character in the list and the lowest total. Han characters carry far more meaning each, so 257 of them say what takes 793 Latin characters. The only fair unit is the whole document, because that is the only place the meaning is held constant.
What is the ratio against English?
| Language | o200k, doc 1 | o200k, doc 2 | Combined | cl100k, doc 1 |
|---|---|---|---|---|
| English | 1.00x | 1.00x | 1.00x | 1.00x |
| Chinese | 1.04x | 1.07x | 1.05x | 1.52x |
| German | 1.26x | 1.18x | 1.24x | 1.55x |
| French | 1.28x | 1.20x | 1.26x | 1.49x |
| Vietnamese | 1.45x | 1.14x | 1.37x | 2.69x |
| Japanese | 1.53x | 1.41x | 1.50x | 2.18x |
| Thai | 1.66x | 1.41x | 1.59x | 4.06x |
Vietnamese moves the most between the two documents — 1.45x on the longer, prose-heavy notice and 1.14x on the short imperative one. Ratios of this kind are a property of the text as much as the language, so treat any single number as ±0.15 and the ordering as the durable part. The ordering is stable across both documents: English cheapest, Chinese next, then the European pair, then Vietnamese, Japanese and Thai.
The European control is the interesting row. French and German pay a 24–26% premium for the same meaning, which is close to Vietnamese on the short document and worse than Chinese on both. This is not a Latin-script-versus-everything-else story. Whatever is happening is about how much of a language's vocabulary made it into the merge table, not about its writing system.
Did o200k fix this?
Largely, yes — and it is worth saying plainly, because the fair version of this article is not an accusation. Vendors did not choose to overcharge non-English users. BPE learns merges from frequency, an English-dominated corpus produces English-dominated merges, and enlarging the vocabulary from roughly 100k to 200k entries buys room for everyone else's words.
| Language | cl100k | o200k | Change | Tokens saved |
|---|---|---|---|---|
| English | 163 | 163 | 0.0% | 0 |
| French | 243 | 209 | −14.0% | 34 |
| German | 253 | 205 | −19.0% | 48 |
| Japanese | 355 | 250 | −29.6% | 105 |
| Chinese | 248 | 170 | −31.5% | 78 |
| Vietnamese | 439 | 237 | −46.0% | 202 |
| Thai | 661 | 270 | −59.2% | 391 |
The second document reproduces the pattern: Vietnamese −42.3%, Thai −56.6%, English 0.0%. English gained nothing from the new tokenizer, and everyone else gained between 14% and 59%. The entire budget of the vocabulary expansion went to non-English text. That is a deliberate and effective fix, and it means any cost estimate built on cl100k_base numbers — most of what is written about this online — now overstates the penalty by roughly a factor of two.
Do accents cost extra?
No — and removing them costs you 10.5% more tokens. This is the measurement that came out backwards, and it is the most actionable thing here.
The folk advice is that Vietnamese is expensive because of its diacritics: each ế or ạ is three UTF-8 bytes, so surely stripping them saves money. It used to. It no longer does.
| Variant | Bytes | o200k | cl100k |
|---|---|---|---|
| Vietnamese, as written | 1,201 | 237 | 439 |
| Vietnamese, diacritics stripped | 903 | 262 | 314 |
| Effect of stripping | −25% | +10.5% | −28.5% |
The second document agrees: +12.5% on o200k_base, −27.9% on cl100k_base. The reason shows up at word level. Under o200k_base, người is one token and nguoi is two; được is one and duoc is two; những is one and nhung is two. The accented forms are real Vietnamese words that appeared often enough in training to earn their own merges. The unaccented forms are not words in any language, so they fall back to fragments.
So the practical rule is the opposite of the old one. Send correctly accented Vietnamese. It is cheaper, and the model understands it better. If you have a pipeline that normalizes diacritics away before hitting the API — a legacy search index, an SMS gateway, a database column that is still latin1 — it is now costing you money on both sides of the call.
The one caveat is that this is an o200k_base result. If you are calling a model still on cl100k_base, stripping accents does save 28.5%, at the price of text your users find harder to read. Check which encoding your model uses before acting on either number.
What does it cost in money?
Example only, and the price is the part that will go stale first: $2.50 per million input tokens, the published standard input rate for GPT-4o, checked on 31 August 2026. Substitute your own. The shape of the table is what matters.
Sending 100,000 copies of the support notice as input:
| Language | Tokens/doc | Total tokens | Cost | vs English |
|---|---|---|---|---|
| English | 163 | 16,300,000 | $40.75 | — |
| Chinese | 170 | 17,000,000 | $42.50 | +$1.75 |
| German | 205 | 20,500,000 | $51.25 | +$10.50 |
| French | 209 | 20,900,000 | $52.25 | +$11.50 |
| Vietnamese | 237 | 23,700,000 | $59.25 | +$18.50 |
| Japanese | 250 | 25,000,000 | $62.50 | +$21.75 |
| Thai | 270 | 27,000,000 | $67.50 | +$26.75 |
Eighteen dollars on a forty-dollar workload. At the scale most teams operate, the absolute number is small enough to ignore and the percentage is not: it is a flat 45% surcharge on every Vietnamese customer interaction, and it compounds with anything that multiplies your token count — retries, few-shot examples, a system prompt repeated on every call.
How much context do you lose?
More than the price difference, because context is a hard wall rather than a meter. Filling a 128,000-token window with copies of the same document:
| Language | Copies in 128k | vs English |
|---|---|---|
| English | 785 | 1.00x |
| Chinese | 752 | 0.96x |
| German | 624 | 0.80x |
| French | 612 | 0.78x |
| Vietnamese | 540 | 0.69x |
| Japanese | 512 | 0.65x |
| Thai | 474 | 0.60x |
A Vietnamese RAG system fits 69% of the source material an English one does, at the same window and the same retrieval settings. If you sized your chunk budget on English documents, you are silently retrieving less evidence per query in Vietnamese. This is worth folding into how you chunk documents for retrieval: a 500-token chunk holds about a third less Vietnamese content than the English chunk you tested with, so a fixed token budget quietly changes what a chunk is when the language changes.
Is the tokenizer itself slower?
Yes, and Vietnamese is the worst case in this corpus. Encoding the same corpus repeated 40 times, median of 21 runs, single core:
| Language | o200k tokens/s | o200k MB/s | cl100k tokens/s |
|---|---|---|---|
| Vietnamese | 4.82M | 24.4 | 8.06M |
| English | 9.76M | 47.8 | 10.11M |
| Thai | 11.95M | 94.1 | 14.04M |
| Chinese | 12.76M | 57.9 | 22.58M |
| French | 14.01M | 66.1 | 14.13M |
| German | 14.02M | 67.3 | 15.03M |
| Japanese | 15.49M | 65.7 | 25.01M |
Vietnamese encodes at roughly half the English rate by either measure. The BPE merge loop does more work when the byte sequence is long and the winning merges are late in the table, which is exactly the multi-byte accented Latin case. In absolute terms this only matters if you tokenize at scale — counting tokens before dispatch, splitting documents, or enforcing a budget across millions of records. At 24 MB/s a gigabyte of Vietnamese text takes about 41 seconds of CPU; at 48 MB/s the English gigabyte takes 21. Note also that o200k_base is slower than cl100k_base everywhere: the larger vocabulary produces fewer tokens but costs more time to find them.
What should you actually do?
Four things, in order of payoff.
Check which encoding your model uses, and count tokens locally before you send. The difference between cl100k_base and o200k_base is a factor of two on Vietnamese, so a budget estimated against the wrong one is not approximately right, it is wrong. Counting offline is free and instant.
Stop stripping diacritics. It is now a cost increase as well as a quality loss.
Budget context in documents, not tokens, when the language varies. Cap your retrieval at "twelve support notices", not "8,000 tokens", or the Vietnamese path silently gets less evidence than the English one.
Keep the structured parts of your prompt in English. Field names, enum values and instructions are not user-facing, and they tokenize best in English — one of the cheaper reasons that asking a model for JSON with an explicit schema pays for itself. Put Vietnamese where it belongs, in the content, and let the scaffolding be cheap.
What does not help is the storage-side lever. Embeddings for Vietnamese cost the same bytes as embeddings for English — a 384-dimension vector is 1,536 bytes whatever produced it — so quantising them to int8 saves exactly as much in either language and does nothing about the token count. The tokenizer tax lands on the API call, not on the disk.
Check it yourself
One dependency, no API key, no network after the install. Runs in about a second.
mkdir tokcost && cd tokcost
npm init -y >/dev/null && npm i gpt-tokenizer@4.0.0
Save as tokcost.mjs and run node tokcost.mjs. Replace the strings with your own text — the ratios are the point, not these particular sentences.
// tokcost.mjs -- what the same meaning costs in different languages.
// gpt-tokenizer 4.0.0. Offline: these are the real BPE tables, not an estimate.
import { encode as o200k } from 'gpt-tokenizer/encoding/o200k_base';
import { encode as cl100k } from 'gpt-tokenizer/encoding/cl100k_base';
const PRICE = 2.50; // USD per 1M input tokens. EXAMPLE. Substitute yours.
const doc = {
English: `The battery lasts about ten hours. Charge it fully before the first use. Do not leave the device in a hot car. If the screen does not turn on, hold the side button for ten seconds. Keep the receipt; the warranty lasts two years from the day of purchase.`,
Vietnamese: `Pin dùng được khoảng mười giờ. Hãy sạc đầy trước lần sử dụng đầu tiên. Không để thiết bị trong ô tô nóng. Nếu màn hình không sáng, hãy giữ nút bên cạnh trong mười giây. Giữ lại hóa đơn; bảo hành kéo dài hai năm kể từ ngày mua.`,
Chinese: `电池可以使用大约十个小时。首次使用前请充满电。不要把设备留在炎热的车内。如果屏幕不亮,请按住侧面按钮十秒钟。请保留收据;保修期为自购买之日起两年。`,
Thai: `แบตเตอรี่ใช้งานได้ประมาณสิบชั่วโมง ชาร์จให้เต็มก่อนใช้งานครั้งแรก อย่าทิ้งอุปกรณ์ไว้ในรถที่ร้อน หากหน้าจอไม่ติด ให้กดปุ่มด้านข้างค้างไว้สิบวินาที เก็บใบเสร็จไว้ การรับประกันมีอายุสองปีนับจากวันที่ซื้อ`,
French: `La batterie dure environ dix heures. Chargez-la complètement avant la première utilisation. Ne laissez pas l'appareil dans une voiture chaude. Si l'écran ne s'allume pas, maintenez le bouton latéral enfoncé pendant dix secondes. Conservez le reçu ; la garantie dure deux ans à compter du jour de l'achat.`,
};
const EN = o200k(doc.English).length;
console.log(`node ${process.versions.node} o200k_base vs cl100k_base $${PRICE}/1M tokens\n`);
console.log('lang bytes o200k vs EN cl100k vs EN $/100k docs 128k fits');
for (const [lang, text] of Object.entries(doc)) {
const o = o200k(text).length, c = cl100k(text).length;
console.log(
lang.padEnd(12) +
String(Buffer.byteLength(text, 'utf8')).padStart(6) +
String(o).padStart(8) + (o / EN).toFixed(2).padStart(7) + 'x' +
String(c).padStart(9) + (c / cl100k(doc.English).length).toFixed(2).padStart(7) + 'x' +
('$' + ((o * 100000 / 1e6) * PRICE).toFixed(2)).padStart(14) +
String(Math.floor(128000 / o)).padStart(12));
}
// The accent test. Strip diacritics and re-count.
const strip = (s) => s.normalize('NFD').replace(/[̀-ͯ]/g, '')
.replace(/đ/g, 'd').replace(/Đ/g, 'D').normalize('NFC');
const vi = doc.Vietnamese, bare = strip(vi);
console.log('\nAccent test (Vietnamese):');
console.log(` o200k : ${o200k(vi).length} with accents -> ${o200k(bare).length} without` +
` (${o200k(bare).length > o200k(vi).length ? 'WORSE' : 'better'})`);
console.log(` cl100k: ${cl100k(vi).length} with accents -> ${cl100k(bare).length} without` +
` (${cl100k(bare).length > cl100k(vi).length ? 'WORSE' : 'better'})`);
for (const w of ['người', 'được', 'những']) {
console.log(` ${w} = ${o200k(' ' + w).length} tok ${strip(w)} = ${o200k(' ' + strip(w)).length} tok`);
}
On the M3 laptop above that prints:
node 23.5.0 o200k_base vs cl100k_base $2.5/1M tokens
lang bytes o200k vs EN cl100k vs EN $/100k docs 128k fits
English 253 56 1.00x 56 1.00x $14.00 2285
Vietnamese 303 64 1.14x 111 1.98x $16.00 2000
Chinese 219 60 1.07x 82 1.46x $15.00 2133
Thai 588 79 1.41x 182 3.25x $19.75 1620
French 311 67 1.20x 87 1.55x $16.75 1910
Accent test (Vietnamese):
o200k : 64 with accents -> 72 without (WORSE)
cl100k: 111 with accents -> 80 without (better)
người = 1 tok nguoi = 2 tok
được = 1 tok duoc = 2 tok
những = 1 tok nhung = 2 tok
The line to watch is the cl100k vs EN column against o200k vs EN. Thai drops from 3.25x to 1.41x and English does not move at all. Change one string to your own product copy and the ratio you get is the surcharge your non-English users are already paying.