Tokenizer-Bench / script.js
deepnevro's picture
Remove Released column; rename Tokenizer Tax to Tokenizer Inefficiency
1722254 verified
Raw
History Blame Contribute Delete
16.9 kB
// ── State ──
let currentData = null;
let currentGroup = 'All';
let currentDataset = 'combined';
let sortKey = 'cpt';
let sortDir = 'desc';
let searchQuery = '';
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => document.querySelectorAll(sel);
function formatNumber(n) {
if (n === null || n === undefined) return '--';
return n.toLocaleString();
}
function formatCompact(n) {
if (n === null || n === undefined) return '--';
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(0) + 'K';
return n.toString();
}
function getBackendClass(backend) {
const map = { 'hf': 'backend-hf', 'tiktoken': 'backend-tiktoken', 'google': 'backend-google', 'anthropic': 'backend-anthropic', 'xai': 'backend-xai' };
return map[backend] || 'backend-hf';
}
function getBackendLabel(backend) {
const map = { 'hf': 'HF', 'tiktoken': 'OpenAI', 'google': 'Google', 'anthropic': 'Anthropic', 'xai': 'xAI' };
return map[backend] || backend;
}
// ── Data Loading ──
function flattenData(data) {
const rows = [];
const allDatasets = new Set();
const datasetGroups = data.dataset_groups || {};
for (const [modelId, model] of Object.entries(data.models || {})) {
for (const [dsName, ds] of Object.entries(model.datasets || {})) {
allDatasets.add(dsName);
rows.push({
model_id: modelId,
model_name: model.model_name || modelId.split('/').pop(),
release_date: model.release_date || '--',
vocab_size: model.vocab_size,
backend: inferBackend(modelId),
dataset: dsName,
group: datasetGroups[dsName] || 'Other',
total_chars: ds.total_chars || 0,
total_tokens: ds.total_tokens || 0,
chars_per_token: ds.chars_per_token || 0,
per_million_output_token_price: model.per_million_output_token_price,
throughput: model.throughput,
});
}
}
const groups = [...new Set(Object.values(datasetGroups))].sort();
return {
rows,
datasets: Array.from(allDatasets).sort(),
groups,
datasetGroups,
timestamp: data.timestamp || ''
};
}
function inferBackend(modelId) {
if (modelId.startsWith('gpt-') || modelId.startsWith('text-embedding')) return 'tiktoken';
if (modelId.startsWith('gemini-')) return 'google';
if (modelId.startsWith('claude-')) return 'anthropic';
if (modelId.startsWith('grok-')) return 'xai';
return 'hf';
}
async function loadData() {
try {
const res = await fetch('data.json');
if (!res.ok) throw new Error('data.json not found');
const data = await res.json();
if (!data.models) throw new Error('invalid format');
currentData = flattenData(data);
renderGroupTabs();
renderDatasetTabs();
renderStats();
renderTable();
renderChart();
$('#timestamp').textContent = currentData.timestamp ? 'run: ' + currentData.timestamp : '';
} catch (e) {
$('#tableBody').innerHTML = `<tr><td colspan="11" class="empty-state"><h3>oops!</h3><p>could not load data.json β€” ${e.message}</p></td></tr>`;
$('#chartContainer').innerHTML = '<div class="chart-placeholder">no data.json found</div>';
}
}
// ── Group Tabs ──
function renderGroupTabs() {
const tabs = $('#groupTabs');
tabs.innerHTML = '';
const groups = ['All', ...currentData.groups];
for (const g of groups) {
const btn = document.createElement('button');
btn.className = 'tab-btn group-tab' + (currentGroup === g ? ' active' : '');
btn.textContent = g;
btn.onclick = () => {
currentGroup = g;
currentDataset = 'combined';
if (g === 'All') {
$('#datasetBar').style.display = 'none';
} else {
$('#datasetBar').style.display = 'flex';
}
renderGroupTabs();
renderDatasetTabs();
renderStats();
renderTable();
renderChart();
};
tabs.appendChild(btn);
}
}
// ── Dataset Tabs ──
function renderDatasetTabs() {
const tabs = $('#datasetTabs');
tabs.innerHTML = '';
if (currentGroup === 'All') return;
const combinedBtn = document.createElement('button');
combinedBtn.className = 'tab-btn' + (currentDataset === 'combined' ? ' active' : '');
combinedBtn.textContent = 'combined';
combinedBtn.onclick = () => { currentDataset = 'combined'; renderDatasetTabs(); renderStats(); renderTable(); renderChart(); };
tabs.appendChild(combinedBtn);
const dsInGroup = currentData.datasets.filter(ds => currentData.datasetGroups[ds] === currentGroup);
for (const ds of dsInGroup) {
const btn = document.createElement('button');
btn.className = 'tab-btn' + (currentDataset === ds ? ' active' : '');
btn.textContent = ds;
btn.onclick = () => { currentDataset = ds; renderDatasetTabs(); renderStats(); renderTable(); renderChart(); };
tabs.appendChild(btn);
}
}
// ── Stats ──
function renderStats() {
const rows = getFilteredRows();
const models = new Set(rows.map(r => r.model_id));
let bestName = 'β€”';
let totalChars = 0;
if (rows.length) {
const sorted = [...rows].sort((a, b) => b.chars_per_token - a.chars_per_token);
bestName = sorted[0].model_name;
totalChars = sorted[0].total_chars;
}
$('#statModels').textContent = models.size;
$('#statChars').textContent = formatCompact(totalChars);
$('#statBest').textContent = bestName.length > 14 ? bestName.slice(0, 12) + '..' : bestName;
}
// ── Filter & Sort ──
function getFilteredRows() {
let rows;
if (currentGroup === 'All' || currentDataset === 'combined') {
const targetDatasets = currentGroup === 'All'
? currentData.datasets
: currentData.datasets.filter(ds => currentData.datasetGroups[ds] === currentGroup);
const modelIds = [...new Set(currentData.rows.map(r => r.model_id))];
rows = [];
for (const mid of modelIds) {
const mrows = currentData.rows.filter(r => r.model_id === mid && targetDatasets.includes(r.dataset));
if (!mrows.length) continue;
const total_chars = mrows.reduce((s, r) => s + r.total_chars, 0);
const total_tokens = mrows.reduce((s, r) => s + r.total_tokens, 0);
const cpts = mrows.map(r => r.total_tokens > 0 ? r.total_chars / r.total_tokens : 0);
const macroCpt = cpts.length > 0 ? cpts.reduce((a, b) => a + b, 0) / cpts.length : 0;
rows.push({
model_id: mid,
model_name: mrows[0].model_name,
release_date: mrows[0].release_date,
vocab_size: mrows[0].vocab_size,
backend: mrows[0].backend,
total_chars,
total_tokens,
chars_per_token: macroCpt,
per_million_output_token_price: mrows[0].per_million_output_token_price,
throughput: mrows[0].throughput,
});
}
} else {
rows = currentData.rows.filter(r => r.dataset === currentDataset).map(r => ({ ...r }));
}
if (searchQuery) {
const q = searchQuery.toLowerCase();
rows = rows.filter(r => r.model_name.toLowerCase().includes(q) || r.model_id.toLowerCase().includes(q));
}
// Compute adjusted values: adjusted = raw * C/T for the current view
for (const r of rows) {
const cpt = r.chars_per_token || 0;
if (r.per_million_output_token_price != null && r.per_million_output_token_price !== undefined) {
r.price_adj = cpt ? r.per_million_output_token_price / cpt : null;
} else {
r.price_adj = null;
}
if (r.throughput != null && r.throughput !== undefined) {
r.throughput_adj = r.throughput * cpt;
} else {
r.throughput_adj = null;
}
}
if (rows.length) {
const maxCPT = Math.max(...rows.map(r => r.chars_per_token));
for (const r of rows) {
// "% more tokens than the best tokenizer for identical text" β€” matches
// analyze_benchmark.py's compute_pct_more_tokens(): (best/mine - 1) * 100.
// NOT (best-mine)/best β€” that's a different, smaller number that understates
// the gap for anything far from the best (e.g. reads ~41% where this reads ~69%).
r.pct_more = r.chars_per_token > 0 ? ((maxCPT / r.chars_per_token) - 1) * 100 : 0;
}
}
return rows;
}
function sortRows(rows) {
const keyMap = {
'rank': 'chars_per_token', 'model': 'model_name', 'backend': 'backend',
'date': 'release_date', 'vocab': 'vocab_size', 'tokens': 'total_tokens',
'cpt': 'chars_per_token',
'price': 'per_million_output_token_price', 'price_adj': 'price_adj',
'throughput': 'throughput', 'throughput_adj': 'throughput_adj', 'tax': 'pct_more'
};
const key = keyMap[sortKey] || sortKey;
const dir = sortDir === 'asc' ? 1 : -1;
return [...rows].sort((a, b) => {
let va = a[key];
let vb = b[key];
if (typeof va === 'string') va = va.toLowerCase();
if (typeof vb === 'string') vb = vb.toLowerCase();
if (va === null || va === undefined) va = -Infinity;
if (vb === null || vb === undefined) vb = -Infinity;
if (va < vb) return -1 * dir;
if (va > vb) return 1 * dir;
return 0;
});
}
// ── Table ──
function renderTable() {
const tbody = $('#tableBody');
let rows = getFilteredRows();
if (rows.length === 0) {
tbody.innerHTML = `<tr><td colspan="11" class="empty-state"><h3>no results</h3><p>try a different search or dataset</p></td></tr>`;
return;
}
rows = sortRows(rows);
const ranked = rows.map((r, i) => ({ ...r, rank: i + 1 }));
const html = ranked.map(r => {
const rankClass = r.rank === 1 ? 'rank-1' : r.rank === 2 ? 'rank-2' : r.rank === 3 ? 'rank-3' : 'rank-other';
const vocab = (r.vocab_size !== null && r.vocab_size !== undefined) ? formatCompact(r.vocab_size) : '--';
const backendClass = getBackendClass(r.backend);
const backendLabel = getBackendLabel(r.backend);
// Price: '--' if null/undefined
const priceVal = (r.per_million_output_token_price != null && r.per_million_output_token_price !== undefined)
? r.per_million_output_token_price.toFixed(2)
: '--';
// Price Adjusted: '--' if null/undefined
const priceAdjVal = (r.price_adj != null && r.price_adj !== undefined)
? r.price_adj.toFixed(2)
: '--';
// Throughput: '--' if null/undefined
const tputVal = (r.throughput != null && r.throughput !== undefined)
? formatCompact(r.throughput)
: '--';
// Throughput Adjusted: Rounded to 0 decimals, '--' if null/undefined
const tputAdjVal = (r.throughput_adj != null && r.throughput_adj !== undefined)
? Math.round(r.throughput_adj).toLocaleString()
: '--';
// Tokenizer Tax: extra tokens vs the most efficient tokenizer in this view.
// 0 means this row IS the most efficient tokenizer β€” shown as a dash, not "+0.0%".
const taxVal = (r.pct_more != null && r.pct_more > 0)
? '+' + r.pct_more.toFixed(1) + '%'
: 'β€”';
return `
<tr data-model="${r.model_id}">
<td class="col-rank"><span class="rank-badge ${rankClass}">${r.rank}</span></td>
<td class="col-model">
<div class="model-name">${r.model_name}</div>
<div class="model-id">${r.model_id}</div>
</td>
<td class="col-backend"><span class="backend-badge ${backendClass}">${backendLabel}</span></td>
<td class="col-vocab">${vocab}</td>
<td class="col-tokens">${formatCompact(r.total_tokens)}</td>
<td class="col-cpt">${r.chars_per_token.toFixed(2)}</td>
<td class="col-price">${priceVal}</td>
<td class="col-price-adj">${priceAdjVal}</td>
<td class="col-throughput">${tputVal}</td>
<td class="col-throughput-adj">${tputAdjVal}</td>
<td class="col-tax">${taxVal}</td>
</tr>
`;
}).join('');
tbody.innerHTML = html;
}
// ── Hand-drawn Chart ──
function renderChart() {
const container = $('#chartContainer');
let rows = getFilteredRows();
if (rows.length === 0) {
container.innerHTML = '<div class="chart-placeholder">no data to sketch</div>';
return;
}
rows = sortRows(rows);
const displayRows = rows;
const maxCPT = Math.max(...displayRows.map(r => r.chars_per_token)) || 1;
const barHeight = 34;
const barGap = 14;
const labelW = 170;
const chartW = 720;
const chartH = displayRows.length * (barHeight + barGap) + 70;
const barMaxW = chartW - labelW - 90;
const colors = ['#2a9d8f', '#58a6ff', '#e76f51', '#e9c46a', '#8ab17d', '#f4a261', '#2a9d8f', '#58a6ff', '#e76f51', '#e9c46a'];
let svg = `<svg class="chart-svg" viewBox="0 0 ${chartW} ${chartH}" xmlns="http://www.w3.org/2000/svg">`;
const plotBottom = chartH - 42;
for (let i = 0; i <= 4; i++) {
const x = labelW + (barMaxW * i / 4);
const val = (maxCPT * i / 4).toFixed(1);
const wobble = (Math.random() - 0.5) * 1.5;
svg += `<path d="M${x + wobble} 18 Q${x} ${chartH / 2} ${x - wobble} ${plotBottom}" stroke="var(--border-sketch)" stroke-width="1" fill="none" stroke-dasharray="4,3" opacity="0.6"/>`;
svg += `<text x="${x}" y="${chartH - 16}" text-anchor="middle" fill="var(--ink-muted)" font-family="Kalam" font-size="12" font-weight="700">${val}</text>`;
}
svg += `<path d="M${labelW - 5} ${plotBottom} Q${chartW / 2} ${plotBottom + 2} ${chartW - 40} ${plotBottom}" stroke="var(--ink-light)" stroke-width="1.5" fill="none"/>`;
displayRows.forEach((r, i) => {
const y = 18 + i * (barHeight + barGap);
const width = (r.chars_per_token / maxCPT) * barMaxW;
const color = colors[i % colors.length];
const jitter = (Math.random() - 0.5) * 2;
const x1 = labelW + jitter;
const y1 = y + jitter;
const x2 = labelW + width + jitter;
const y2 = y + barHeight + jitter;
const curve = 3 + Math.random() * 2;
const patternId = `hatch${i}`;
svg += `<defs>
<pattern id="${patternId}" patternUnits="userSpaceOnUse" width="6" height="6" patternTransform="rotate(45)">
<line x1="0" y1="0" x2="0" y2="6" stroke="${color}" stroke-width="1" opacity="0.15"/>
</pattern>
</defs>`;
const wob = () => (Math.random() - 0.5) * 1.5;
svg += `<path d="M${x1} ${y1 + curve} Q${x1} ${y1} ${x1 + curve} ${y1} L${x2 - curve} ${y1 + wob()} Q${x2} ${y1} ${x2} ${y1 + curve} L${x2 + wob()} ${y2 - curve} Q${x2} ${y2} ${x2 - curve} ${y2} L${x1 + curve} ${y2 - wob()} Q${x1} ${y2} ${x1} ${y2 - curve} Z"
fill="${color}" opacity="0.88" stroke="${color}" stroke-width="1.5" stroke-linejoin="round"/>`;
svg += `<path d="M${x1} ${y1 + curve} L${x2 - curve} ${y1} Q${x2 + 1} ${(y1 + y2) / 2} ${x2 - curve} ${y2} L${x1} ${y2 - curve} Z"
fill="url(#${patternId})" opacity="0.4"/>`;
// Model name label (left) β€” sketchy hand font
svg += `<text x="${labelW - 10}" y="${y + barHeight / 2 + 5}" text-anchor="end" fill="var(--ink)" font-family="Kalam" font-size="13" font-weight="700">${r.model_name}</text>`;
// Value read-out (right of bar) β€” white + bold/blunt, handwritten Kalam font
svg += `<text x="${labelW + width + 8}" y="${y + barHeight / 2 + 5}" fill="#f0f6fc" font-family="Kalam" font-size="13" font-weight="700">${r.chars_per_token.toFixed(2)}</text>`;
});
svg += '</svg>';
container.innerHTML = svg;
const viewLabel = currentGroup === 'All' ? 'All Datasets' :
currentDataset === 'combined' ? currentGroup : currentDataset;
$('#chartTitle').textContent = `Efficiency Sketch β€” ${viewLabel}`;
}
// ── Event Listeners ──
$('#searchInput').addEventListener('input', (e) => {
searchQuery = e.target.value;
renderTable();
renderChart();
});
$$('.leaderboard th[data-sort]').forEach(th => {
th.addEventListener('click', () => {
const key = th.dataset.sort;
if (sortKey === key) {
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
} else {
sortKey = key;
sortDir = key === 'model' || key === 'date' ? 'asc' : 'desc';
}
$$('.leaderboard th').forEach(t => t.classList.remove('asc', 'desc'));
th.classList.add(sortDir);
renderTable();
renderChart();
});
});
// ── Init ──
loadData();