Skip to content

Instantly share code, notes, and snippets.

@AranKomat
Created April 28, 2026 14:28
Show Gist options
  • Select an option

  • Save AranKomat/6c24d7b47efa21eb3cf658c889436bd4 to your computer and use it in GitHub Desktop.

Select an option

Save AranKomat/6c24d7b47efa21eb3cf658c889436bd4 to your computer and use it in GitHub Desktop.
import React, { useState, useEffect, useRef } from 'react';
export default function TokenCounter() {
const [text, setText] = useState('');
const [tokenCount, setTokenCount] = useState(null);
const [rawApiCount, setRawApiCount] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [overhead, setOverhead] = useState(null);
const [calibrating, setCalibrating] = useState(false);
const debounceRef = useRef(null);
const lastCountedRef = useRef('');
// Calibrate: send a 1-token input, measure proxy overhead.
// "a" tokenizes to exactly 1 token, so overhead = response_tokens - 1.
const calibrate = async () => {
setCalibrating(true);
setError(null);
try {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "claude-sonnet-4-6",
max_tokens: 1,
messages: [{ role: "user", content: "a" }]
})
});
if (!response.ok) {
const t = await response.text();
throw new Error(`Calibration ${response.status}: ${t.slice(0, 200)}`);
}
const data = await response.json();
const baseline = data?.usage?.input_tokens;
if (typeof baseline !== 'number') {
throw new Error('Calibration: unexpected response shape');
}
setOverhead(baseline - 1);
lastCountedRef.current = '';
} catch (err) {
setError(err.message);
setOverhead(null);
} finally {
setCalibrating(false);
}
};
useEffect(() => { calibrate(); }, []);
const countTokens = async (input) => {
if (overhead === null) return;
if (!input || !input.trim()) {
setTokenCount(null);
setRawApiCount(null);
setError(null);
setLoading(false);
return;
}
if (lastCountedRef.current === input) return;
lastCountedRef.current = input;
setLoading(true);
setError(null);
try {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "claude-sonnet-4-6",
max_tokens: 1,
messages: [{ role: "user", content: input }]
})
});
if (!response.ok) {
const t = await response.text();
throw new Error(`${response.status}: ${t.slice(0, 200)}`);
}
const data = await response.json();
const raw = data?.usage?.input_tokens;
if (typeof raw !== 'number') {
throw new Error(`Unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
}
setRawApiCount(raw);
setTokenCount(Math.max(0, raw - overhead));
} catch (err) {
setError(err.message);
setTokenCount(null);
setRawApiCount(null);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => countTokens(text), 500);
return () => debounceRef.current && clearTimeout(debounceRef.current);
}, [text, overhead]);
const charCount = text.length;
const wordCount = text.trim() ? text.trim().split(/\s+/).length : 0;
const charsPerToken = typeof tokenCount === 'number' && tokenCount > 0 ? (charCount / tokenCount).toFixed(2) : '—';
const tokensPerWord = typeof tokenCount === 'number' && wordCount > 0 ? (tokenCount / wordCount).toFixed(2) : '—';
const CONTEXT_WINDOW = 200000;
const contextPct = typeof tokenCount === 'number' ? Math.min(100, (tokenCount / CONTEXT_WINDOW) * 100) : 0;
const INPUT_COST_PER_MTOK = 3;
const estCost = typeof tokenCount === 'number' ? ((tokenCount / 1_000_000) * INPUT_COST_PER_MTOK) : 0;
const status = calibrating ? 'calibrating' : loading ? 'counting' : overhead === null ? 'error' : 'ready';
const statusColor = status === 'ready' ? 'bg-emerald-500' : status === 'error' ? 'bg-red-500' : 'bg-amber-400 animate-pulse';
return (
<div className="min-h-screen bg-stone-950 text-stone-100 p-4 sm:p-8" style={{ fontFamily: "'JetBrains Mono', 'Fira Code', ui-monospace, monospace" }}>
<div className="max-w-4xl mx-auto">
<div className="mb-8 border-b border-stone-800 pb-6">
<div className="flex items-center gap-2 text-amber-500 text-[11px] tracking-[0.2em] uppercase mb-3 flex-wrap">
<span className={`inline-block w-1.5 h-1.5 rounded-full ${statusColor}`}></span>
<span>{status}</span>
<span className="text-stone-700">·</span>
<span className="text-stone-500">tokenizer</span>
{overhead !== null && (
<>
<span className="text-stone-700">·</span>
<span className="text-stone-500 normal-case tracking-normal">overhead {overhead.toLocaleString()}</span>
<button
onClick={calibrate}
disabled={calibrating}
className="ml-1 text-stone-500 hover:text-amber-500 transition-colors disabled:opacity-40"
title="Recalibrate proxy overhead"
>
</button>
</>
)}
</div>
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight leading-none">
<span className="text-stone-100">claude 4.6</span>
<span className="text-stone-600"> / </span>
<span className="text-amber-500">token counter</span>
</h1>
<p className="text-stone-500 mt-3 text-sm">
Paste any text. Get the exact token count from the official Anthropic tokenizer.
</p>
</div>
{calibrating && (
<div className="bg-amber-950/30 border border-amber-900/50 rounded-lg p-3 mb-4 text-amber-300/80 text-xs">
Calibrating proxy overhead with a 1-token probe…
</div>
)}
<div className="mb-6">
<div className="flex justify-between items-center mb-2">
<label className="text-[10px] uppercase tracking-[0.2em] text-stone-500">
Input
</label>
{text && (
<button
onClick={() => setText('')}
className="text-[10px] uppercase tracking-[0.2em] text-stone-500 hover:text-amber-500 transition-colors"
>
clear
</button>
)}
</div>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Paste text here. Counting happens automatically as you type..."
disabled={overhead === null}
className="w-full h-64 bg-stone-900 border border-stone-800 rounded-lg p-4 text-stone-100 placeholder-stone-600 focus:outline-none focus:border-amber-600/60 focus:ring-1 focus:ring-amber-600/30 transition-all resize-y text-sm leading-relaxed disabled:opacity-50"
spellCheck={false}
/>
</div>
<div className="bg-gradient-to-br from-stone-900 to-stone-950 border border-amber-900/40 rounded-lg p-6 mb-4 relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-amber-500/5 rounded-full blur-3xl"></div>
<div className="relative">
<div className="text-[10px] uppercase tracking-[0.2em] text-amber-600/80 mb-2">
Tokens
</div>
<div className="text-5xl sm:text-6xl font-bold tabular-nums text-amber-400 leading-none">
{loading || calibrating ? (
<span className="text-stone-600">···</span>
) : typeof tokenCount === 'number' ? (
tokenCount.toLocaleString()
) : (
<span className="text-stone-700">0</span>
)}
</div>
{typeof tokenCount === 'number' && !loading && (
<div className="mt-4 space-y-2">
<div className="flex justify-between text-[10px] uppercase tracking-[0.2em] text-stone-500">
<span>context window</span>
<span className="tabular-nums">{contextPct.toFixed(2)}% of 200k</span>
</div>
<div className="h-1 bg-stone-800 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-amber-600 to-amber-400 transition-all duration-500"
style={{ width: `${Math.max(contextPct, 0.3)}%` }}
></div>
</div>
</div>
)}
</div>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-6">
<StatCard label="Characters" value={charCount.toLocaleString()} />
<StatCard label="Words" value={wordCount.toLocaleString()} />
<StatCard label="Chars / Token" value={charsPerToken} />
<StatCard label="Tokens / Word" value={tokensPerWord} />
</div>
{typeof tokenCount === 'number' && tokenCount > 0 && !loading && (
<div className="bg-stone-900 border border-stone-800 rounded-lg p-4 mb-6 flex items-baseline justify-between">
<div>
<div className="text-[10px] uppercase tracking-[0.2em] text-stone-500 mb-1">
Estimated input cost
</div>
<div className="text-stone-400 text-xs">
at $3 / MTok
</div>
</div>
<div className="text-2xl font-bold tabular-nums text-stone-200">
${estCost < 0.01 ? estCost.toFixed(6) : estCost.toFixed(4)}
</div>
</div>
)}
{error && (
<div className="bg-red-950/40 border border-red-900/60 rounded-lg p-4 text-red-300 text-sm mb-6">
<div className="font-bold mb-1 text-red-400 text-xs uppercase tracking-widest">Error</div>
<div className="text-red-300/80 text-xs break-all">{error}</div>
</div>
)}
<div className="text-[11px] text-stone-600 leading-relaxed border-t border-stone-900 pt-4 space-y-2">
<p>
The artifact API proxy injects a hidden system prompt into every request, so{' '}
<span className="text-stone-400">usage.input_tokens</span> arrives inflated by a constant
(measured: <span className="text-amber-600/80">{overhead !== null ? overhead.toLocaleString() : '—'}</span>{' '}
tokens). On load this counter probes with a 1-token input to determine that overhead, then
subtracts it from every subsequent count.
</p>
<p>
Result: the displayed number is the tokenization of your text under the shared{' '}
<span className="text-amber-600/80">Claude 4.6</span> tokenizer (used by both{' '}
<span className="text-stone-400">claude-opus-4-6</span> and{' '}
<span className="text-stone-400">claude-sonnet-4-6</span>). Counts here are measured via{' '}
<span className="text-stone-400">claude-sonnet-4-6</span> since the proxy routes there.
</p>
<p className="text-stone-500">
<span className="text-amber-500">Note on Opus 4.7:</span> Opus 4.7 ships with a different
tokenizer that produces ~1.0–1.35x more tokens for the same text (1.45–1.47x has been
measured on technical content). This counter cannot reach the Opus 4.7 tokenizer from inside
the artifact sandbox. For an exact Opus 4.7 count, use Anthropic's{' '}
<span className="text-stone-400">/v1/messages/count_tokens</span> endpoint directly with{' '}
<span className="text-stone-400">model: "claude-opus-4-7"</span>.
</p>
{typeof rawApiCount === 'number' && (
<p className="text-stone-700">
raw api: <span className="tabular-nums">{rawApiCount.toLocaleString()}</span> · overhead:{' '}
<span className="tabular-nums">{overhead?.toLocaleString()}</span> · result:{' '}
<span className="tabular-nums text-stone-500">{tokenCount?.toLocaleString()}</span>
</p>
)}
</div>
</div>
</div>
);
}
function StatCard({ label, value }) {
return (
<div className="bg-stone-900 border border-stone-800 rounded-lg p-3">
<div className="text-[10px] uppercase tracking-[0.2em] text-stone-500 mb-1">
{label}
</div>
<div className="text-xl font-bold tabular-nums text-stone-200">
{value}
</div>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment