A small web app that takes a prompt and shows how many tokens it costs across Claude and ChatGPT models — then lets you click any word to see how it decomposes into tokens, model by model.
- Type a prompt → submit → get a per-model token count for Claude (Opus 4.8 / 4.7 / 4.5, Sonnet 4.6, Haiku 4.5) and ChatGPT (GPT-5, GPT-4o, GPT-4o mini, GPT-4.1, GPT-4 Turbo, GPT-3.5 Turbo).
- A best-estimate headline number (defaults to Opus 4.8).
- A comparison view, grouped by provider, that makes the tokenizer splits visible: on the Claude side Opus 4.7 / 4.8 / Fable 5 share one tokenizer while Opus 4.5/4.6, Sonnet 4.6 and Haiku 4.5 use an earlier one; on the GPT side GPT-4o-era models (o200k) differ from GPT-4 / 3.5 (cl100k).
- A token stream rendered as colored tiles — leading spaces shown as
·, newlines as↵, and multi-byte characters like emoji shown as their byte-fragment tokens (e.g.f0 9f 8d). These boundaries are exact for GPT models and an approximation for Claude (see below). - Click any tile → an "Inside one word" panel shows how that word splits into tokens for every model, with token IDs.
npm install
npm start
# → http://localhost:5050 (override with PORT=8080 npm start)No build step, no bundler. Open the URL and start typing.
The app runs one local byte-pair tokenizer (js-tiktoken) with OpenAI's two published vocabularies, o200k_base and cl100k_base. What that means for honesty differs sharply by provider, and the UI keeps the distinction visible everywhere (every count is badged exact or approx):
-
GPT models — exact.
o200k_base/cl100k_baseviatiktokenare OpenAI's real tokenizers, so for ChatGPT models the counts and the per-tile boundaries are exact for the text itself (the chat API adds only a few message-framing tokens on top). No key needed; this works fully client-side too. -
Claude models — approximate, unless a key is set. Anthropic does not publish the tokenizer for Claude 3/4, so those same vocabularies are only a stand-in (
o200k_basefor the 4.7/4.8/Fable family,cl100k_basefor the earlier one). The tiles are a faithful illustration, not Claude's literal split. To get exact Claude counts, put anANTHROPIC_API_KEY(orANTHROPIC_AUTH_TOKEN) in the server's environment — the headline and bars then switch to Anthropic's officialPOST /v1/messages/count_tokens:ANTHROPIC_API_KEY=sk-ant-... npm start
Everything works with no key — Claude just falls back to the labeled approximation, GPT stays exact, and the footer says which mode you're in.
Token counts are model-specific: the same text genuinely lands on different counts across tokenizers. For billing-grade Claude numbers, use the API path above; GPT numbers are already exact.
server.js native http server: static host + /api/health + /api/analyze
tokenizer.js js-tiktoken wrapper → tokens with exact char offsets (handles emoji/CJK)
public/
index.html markup
styles.css the "typographic instrument" theme (Fraunces + JetBrains Mono)
app.js frontend: counts, comparison bars, token strip, word inspector
GET /api/health→{ apiAvailable, models }POST /api/analyze{ "prompt": "..." }→ per-model counts, per-tokenizer token spans, word segmentation. Prompts are capped at 25,000 characters to keep the visualizer responsive.
npm run build bundles a fully client-side version into dist/ — esbuild inlines the tokenizer (js-tiktoken) and the shared analysis core into dist/bundle.js, and the frontend (app.js) auto-detects that there's no /api and tokenizes in the browser instead. On Pages there's no API key, so it behaves exactly like the no-key server mode: GPT counts are exact, Claude counts are the labeled approximation.
.github/workflows/deploy.yml builds and publishes dist/ on every push to main. One-time setup in the repo: Settings → Pages → Source: GitHub Actions (the workflow's configure-pages step will otherwise fail with a message telling you to do this). The site then publishes to https://<owner>.github.io/<repo>/.
npm run build # → dist/ (static, no server needed)
npx serve dist # preview locally