token-atlas-engine 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- token_atlas/__init__.py +38 -0
- token_atlas/bpe.py +98 -0
- token_atlas/cli.py +402 -0
- token_atlas/corpora.py +367 -0
- token_atlas/domains.py +333 -0
- token_atlas/engine.py +129 -0
- token_atlas/metrics.py +40 -0
- token_atlas/minify.py +130 -0
- token_atlas/models.py +69 -0
- token_atlas/registry.py +273 -0
- token_atlas/router.py +242 -0
- token_atlas/web.py +956 -0
- token_atlas_engine-0.3.0.dist-info/METADATA +223 -0
- token_atlas_engine-0.3.0.dist-info/RECORD +17 -0
- token_atlas_engine-0.3.0.dist-info/WHEEL +4 -0
- token_atlas_engine-0.3.0.dist-info/entry_points.txt +3 -0
- token_atlas_engine-0.3.0.dist-info/licenses/LICENSE +190 -0
token_atlas/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""TokenAtlas: Universal Multilingual Tokenizer Benchmark & Global Inflation Atlas."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.3.0"
|
|
4
|
+
__author__ = "Prajwal Pujari"
|
|
5
|
+
|
|
6
|
+
from token_atlas.models import (
|
|
7
|
+
TokenizationResult,
|
|
8
|
+
LanguageComparison,
|
|
9
|
+
BenchmarkReport,
|
|
10
|
+
TokenizerSpec,
|
|
11
|
+
)
|
|
12
|
+
from token_atlas.metrics import (
|
|
13
|
+
calculate_inflation_index,
|
|
14
|
+
calculate_bytes_per_token,
|
|
15
|
+
calculate_effective_context,
|
|
16
|
+
calculate_cost_multiplier,
|
|
17
|
+
)
|
|
18
|
+
from token_atlas.router import recommend_model, ModelRecommendation
|
|
19
|
+
from token_atlas.minify import minify_prompt, MinificationResult
|
|
20
|
+
from token_atlas.domains import evaluate_domain, DomainBenchmarkReport
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"__version__",
|
|
24
|
+
"TokenizationResult",
|
|
25
|
+
"LanguageComparison",
|
|
26
|
+
"BenchmarkReport",
|
|
27
|
+
"TokenizerSpec",
|
|
28
|
+
"calculate_inflation_index",
|
|
29
|
+
"calculate_bytes_per_token",
|
|
30
|
+
"calculate_effective_context",
|
|
31
|
+
"calculate_cost_multiplier",
|
|
32
|
+
"recommend_model",
|
|
33
|
+
"ModelRecommendation",
|
|
34
|
+
"minify_prompt",
|
|
35
|
+
"MinificationResult",
|
|
36
|
+
"evaluate_domain",
|
|
37
|
+
"DomainBenchmarkReport",
|
|
38
|
+
]
|
token_atlas/bpe.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Educational First-Principles Byte-Pair Encoding (BPE) Implementation.
|
|
2
|
+
|
|
3
|
+
Demonstrates how BPE merge tables are constructed from raw UTF-8 bytes and
|
|
4
|
+
why under-represented scripts suffer from severe byte-fallback fragmentation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import NamedTuple
|
|
8
|
+
from collections import Counter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BPETrainResult(NamedTuple):
|
|
12
|
+
vocab: dict[int, bytes]
|
|
13
|
+
merges: list[tuple[bytes, bytes]]
|
|
14
|
+
token_ids: list[int]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_stats(ids: list[int]) -> Counter[tuple[int, int]]:
|
|
18
|
+
"""Count frequency of consecutive token id pairs."""
|
|
19
|
+
counts: Counter[tuple[int, int]] = Counter()
|
|
20
|
+
for p in zip(ids[:-1], ids[1:]):
|
|
21
|
+
counts[p] += 1
|
|
22
|
+
return counts
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def merge_ids(ids: list[int], pair: tuple[int, int], new_id: int) -> list[int]:
|
|
26
|
+
"""Replace all occurrences of `pair` in `ids` with `new_id`."""
|
|
27
|
+
new_ids: list[int] = []
|
|
28
|
+
i = 0
|
|
29
|
+
n = len(ids)
|
|
30
|
+
while i < n:
|
|
31
|
+
if i < n - 1 and ids[i] == pair[0] and ids[i + 1] == pair[1]:
|
|
32
|
+
new_ids.append(new_id)
|
|
33
|
+
i += 2
|
|
34
|
+
else:
|
|
35
|
+
new_ids.append(ids[i])
|
|
36
|
+
i += 1
|
|
37
|
+
return new_ids
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def train_toy_bpe(text: str, num_merges: int = 20) -> BPETrainResult:
|
|
41
|
+
"""Train a first-principles BPE tokenizer from raw UTF-8 text bytes.
|
|
42
|
+
|
|
43
|
+
Initializes vocabulary with 256 byte tokens (0..255).
|
|
44
|
+
Iteratively merges the most frequent adjacent pair.
|
|
45
|
+
"""
|
|
46
|
+
raw_bytes = list(text.encode("utf-8"))
|
|
47
|
+
ids = list(raw_bytes)
|
|
48
|
+
|
|
49
|
+
# Base vocabulary: 256 individual UTF-8 bytes
|
|
50
|
+
vocab: dict[int, bytes] = {i: bytes([i]) for i in range(256)}
|
|
51
|
+
merges: list[tuple[bytes, bytes]] = []
|
|
52
|
+
|
|
53
|
+
for i in range(num_merges):
|
|
54
|
+
stats = get_stats(ids)
|
|
55
|
+
if not stats:
|
|
56
|
+
break
|
|
57
|
+
# Pick the most frequent pair
|
|
58
|
+
pair, count = stats.most_common(1)[0]
|
|
59
|
+
if count < 2:
|
|
60
|
+
# No repeated pairs left to merge
|
|
61
|
+
break
|
|
62
|
+
|
|
63
|
+
new_id = 256 + i
|
|
64
|
+
ids = merge_ids(ids, pair, new_id)
|
|
65
|
+
|
|
66
|
+
piece_a = vocab[pair[0]]
|
|
67
|
+
piece_b = vocab[pair[1]]
|
|
68
|
+
merged_piece = piece_a + piece_b
|
|
69
|
+
vocab[new_id] = merged_piece
|
|
70
|
+
merges.append((piece_a, piece_b))
|
|
71
|
+
|
|
72
|
+
return BPETrainResult(vocab=vocab, merges=merges, token_ids=ids)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def explain_fragmentation(text: str) -> dict[str, any]:
|
|
76
|
+
"""Diagnose UTF-8 byte fragmentation for an input string."""
|
|
77
|
+
encoded_bytes = text.encode("utf-8")
|
|
78
|
+
byte_len = len(encoded_bytes)
|
|
79
|
+
char_len = len(text)
|
|
80
|
+
|
|
81
|
+
char_breakdown = []
|
|
82
|
+
for ch in text[:10]:
|
|
83
|
+
ch_bytes = ch.encode("utf-8")
|
|
84
|
+
char_breakdown.append({
|
|
85
|
+
"char": ch,
|
|
86
|
+
"unicode": f"U+{ord(ch):04X}",
|
|
87
|
+
"byte_count": len(ch_bytes),
|
|
88
|
+
"raw_hex": " ".join([f"0x{b:02X}" for b in ch_bytes]),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"text_sample": text[:30],
|
|
93
|
+
"character_count": char_len,
|
|
94
|
+
"utf8_byte_count": byte_len,
|
|
95
|
+
"bytes_per_char": byte_len / max(1, char_len),
|
|
96
|
+
"sample_breakdown": char_breakdown,
|
|
97
|
+
}
|
|
98
|
+
|
token_atlas/cli.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""TokenAtlas CLI: Universal Multilingual Tokenizer Benchmark."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
|
|
11
|
+
from token_atlas import __version__
|
|
12
|
+
from token_atlas.corpora import SUPPORTED_LANGUAGES
|
|
13
|
+
from token_atlas.registry import TokenizerRegistry
|
|
14
|
+
from token_atlas.engine import compare_single_text, run_benchmark
|
|
15
|
+
from token_atlas.web import generate_web_dashboard
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
name="token-atlas",
|
|
19
|
+
help="TokenAtlas: Universal Multilingual Tokenizer Benchmark & Global Inflation Atlas",
|
|
20
|
+
add_completion=False,
|
|
21
|
+
)
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def version_callback(value: bool):
|
|
26
|
+
if value:
|
|
27
|
+
console.print(f"[bold cyan]TokenAtlas[/bold cyan] version [bold green]{__version__}[/bold green]")
|
|
28
|
+
raise typer.Exit()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.callback()
|
|
32
|
+
def callback(
|
|
33
|
+
version: Optional[bool] = typer.Option(
|
|
34
|
+
None, "--version", "-v", help="Show version and exit", callback=version_callback, is_eager=True
|
|
35
|
+
),
|
|
36
|
+
):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main():
|
|
41
|
+
app()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command("list-tokenizers")
|
|
45
|
+
def list_tokenizers(
|
|
46
|
+
category: Optional[str] = typer.Option(
|
|
47
|
+
None, "--category", "-c", help="Filter by category (general, multilingual, code, reasoning, slm)"
|
|
48
|
+
)
|
|
49
|
+
):
|
|
50
|
+
"""List all supported frontier tokenizer architectures and metadata."""
|
|
51
|
+
reg = TokenizerRegistry()
|
|
52
|
+
title_suffix = f" (Category: {category})" if category else ""
|
|
53
|
+
table = Table(title=f"[bold cyan]Supported Tokenizers in TokenAtlas{title_suffix}[/bold cyan]")
|
|
54
|
+
table.add_column("ID", style="bold green", no_wrap=True)
|
|
55
|
+
table.add_column("Display Name", style="white")
|
|
56
|
+
table.add_column("Category", style="blue", no_wrap=True)
|
|
57
|
+
table.add_column("Vocab Size", justify="right", style="cyan", no_wrap=True)
|
|
58
|
+
table.add_column("Context Window", justify="right", style="magenta", no_wrap=True)
|
|
59
|
+
table.add_column("Backend", style="yellow", no_wrap=True)
|
|
60
|
+
table.add_column("Key Strengths & Compression Profile", style="dim white")
|
|
61
|
+
|
|
62
|
+
for spec in reg.list_tokenizers(category=category):
|
|
63
|
+
table.add_row(
|
|
64
|
+
spec.id,
|
|
65
|
+
spec.name,
|
|
66
|
+
spec.category.upper(),
|
|
67
|
+
f"{spec.vocab_size:,}",
|
|
68
|
+
f"{spec.context_window:,} tok",
|
|
69
|
+
spec.backend,
|
|
70
|
+
spec.notes or "—",
|
|
71
|
+
)
|
|
72
|
+
console.print(table)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.command("list-languages")
|
|
76
|
+
def list_languages():
|
|
77
|
+
"""List all supported language evaluation benchmarks and language families."""
|
|
78
|
+
table = Table(title="[bold cyan]Supported Languages (FLORES-200 Parallel Corpus)[/bold cyan]")
|
|
79
|
+
table.add_column("Code", style="bold green", justify="center")
|
|
80
|
+
table.add_column("Language", style="white")
|
|
81
|
+
table.add_column("Family", style="magenta")
|
|
82
|
+
table.add_column("Script", style="yellow")
|
|
83
|
+
|
|
84
|
+
for meta in SUPPORTED_LANGUAGES.values():
|
|
85
|
+
table.add_row(meta.code, meta.name, meta.family, meta.script)
|
|
86
|
+
console.print(table)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command("compare")
|
|
90
|
+
def compare_cmd(
|
|
91
|
+
text: Optional[str] = typer.Option(None, "--text", "-t", help="Text to tokenize across models"),
|
|
92
|
+
file: Optional[Path] = typer.Option(None, "--file", "-f", help="Path to text file to tokenize"),
|
|
93
|
+
tokenizers: str = typer.Option("gpt-4o,gpt-4", "--tokenizers", "-m", help="Comma-separated tokenizer IDs"),
|
|
94
|
+
language: str = typer.Option("en", "--language", "-l", help="Language code (e.g. en, ja, hi, de)"),
|
|
95
|
+
):
|
|
96
|
+
"""Compare how multiple tokenizers encode a single text string or file."""
|
|
97
|
+
if file is not None:
|
|
98
|
+
if not file.exists():
|
|
99
|
+
console.print(f"[bold red]Error: File not found at {file}[/bold red]")
|
|
100
|
+
raise typer.Exit(1)
|
|
101
|
+
raw_text = file.read_text(encoding="utf-8")
|
|
102
|
+
elif text is not None:
|
|
103
|
+
raw_text = text
|
|
104
|
+
else:
|
|
105
|
+
console.print("[bold red]Error: Either --text or --file must be specified.[/bold red]")
|
|
106
|
+
raise typer.Exit(1)
|
|
107
|
+
|
|
108
|
+
tids = [t.strip() for t in tokenizers.split(",") if t.strip()]
|
|
109
|
+
comp = compare_single_text(raw_text, tids, language=language)
|
|
110
|
+
|
|
111
|
+
preview = raw_text[:300] + "..." if len(raw_text) > 300 else raw_text
|
|
112
|
+
console.print(Panel(f"[bold white]{preview}[/bold white]", title=f"[bold cyan]Input Text ({comp.language_name})[/bold cyan]"))
|
|
113
|
+
|
|
114
|
+
table = Table(title="[bold]Tokenizer Efficiency Comparison[/bold]")
|
|
115
|
+
table.add_column("Tokenizer", style="bold green")
|
|
116
|
+
table.add_column("Tokens", justify="right", style="cyan")
|
|
117
|
+
table.add_column("Bytes", justify="right", style="white")
|
|
118
|
+
table.add_column("Bytes / Token", justify="right", style="magenta")
|
|
119
|
+
table.add_column("First Token Pieces (Sample)", style="dim")
|
|
120
|
+
|
|
121
|
+
for res in comp.results:
|
|
122
|
+
pieces_display = " ".join([f"[{p}]" for p in res.token_pieces[:10]])
|
|
123
|
+
table.add_row(
|
|
124
|
+
res.tokenizer_id,
|
|
125
|
+
str(res.token_count),
|
|
126
|
+
str(res.byte_count),
|
|
127
|
+
f"{res.bytes_per_token:.2f}",
|
|
128
|
+
pieces_display,
|
|
129
|
+
)
|
|
130
|
+
console.print(table)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@app.command("bench")
|
|
134
|
+
def bench_cmd(
|
|
135
|
+
tokenizers: str = typer.Option("gpt-4o,gpt-4", "--tokenizers", "-m", help="Comma-separated tokenizer IDs"),
|
|
136
|
+
languages: str = typer.Option("all", "--languages", "-l", help="Comma-separated language codes or 'all'"),
|
|
137
|
+
samples: int = typer.Option(5, "--samples", "-n", help="Number of parallel sentences to evaluate"),
|
|
138
|
+
custom_corpus: Optional[Path] = typer.Option(None, "--custom-corpus", help="Path to JSON file containing list of parallel sentence dicts"),
|
|
139
|
+
json_out: Optional[Path] = typer.Option(None, "--json-out", help="Path to export raw JSON report"),
|
|
140
|
+
md_out: Optional[Path] = typer.Option(None, "--md-out", help="Path to export markdown summary"),
|
|
141
|
+
):
|
|
142
|
+
"""Run cross-lingual benchmark across parallel sentences and compute inflation indices."""
|
|
143
|
+
tids = [t.strip() for t in tokenizers.split(",") if t.strip()]
|
|
144
|
+
|
|
145
|
+
if languages.strip().lower() == "all":
|
|
146
|
+
langs = list(SUPPORTED_LANGUAGES.keys())
|
|
147
|
+
else:
|
|
148
|
+
langs = [lang.strip() for lang in languages.split(",") if lang.strip()]
|
|
149
|
+
|
|
150
|
+
corpus_data = None
|
|
151
|
+
source_name = None
|
|
152
|
+
if custom_corpus is not None:
|
|
153
|
+
if not custom_corpus.exists():
|
|
154
|
+
console.print(f"[bold red]Error: Custom corpus file not found at {custom_corpus}[/bold red]")
|
|
155
|
+
raise typer.Exit(1)
|
|
156
|
+
with open(custom_corpus, "r", encoding="utf-8") as f:
|
|
157
|
+
corpus_data = json.load(f)
|
|
158
|
+
source_name = f"Custom Corpus ({custom_corpus.name})"
|
|
159
|
+
|
|
160
|
+
with console.status("[bold cyan]Running parallel multilingual benchmark...[/bold cyan]"):
|
|
161
|
+
report = run_benchmark(
|
|
162
|
+
tokenizer_ids=tids,
|
|
163
|
+
language_codes=langs,
|
|
164
|
+
sample_limit=samples,
|
|
165
|
+
custom_corpus=corpus_data,
|
|
166
|
+
corpora_source=source_name,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
for bench in report.results:
|
|
170
|
+
table = Table(title=f"[bold cyan]{bench.tokenizer_name} (Vocab: {bench.vocab_size:,})[/bold cyan]")
|
|
171
|
+
table.add_column("Lang", style="bold green", justify="center")
|
|
172
|
+
table.add_column("Language Name", style="white")
|
|
173
|
+
table.add_column("Family", style="dim")
|
|
174
|
+
table.add_column("Tokens", justify="right", style="white")
|
|
175
|
+
table.add_column("Inflation (τ)", justify="right", style="bold yellow")
|
|
176
|
+
table.add_column("Effective Context", justify="right", style="cyan")
|
|
177
|
+
table.add_column("Cost Multiplier", justify="right", style="magenta")
|
|
178
|
+
table.add_column("Bytes/Tok", justify="right", style="dim")
|
|
179
|
+
|
|
180
|
+
for code, m in bench.metrics.items():
|
|
181
|
+
tau_style = "bold green" if m.inflation_index <= 1.2 else ("bold yellow" if m.inflation_index <= 2.0 else "bold red")
|
|
182
|
+
table.add_row(
|
|
183
|
+
m.language_code,
|
|
184
|
+
m.language_name,
|
|
185
|
+
m.family,
|
|
186
|
+
f"{m.token_count:,}",
|
|
187
|
+
f"[{tau_style}]{m.inflation_index:.2f}x[/{tau_style}]",
|
|
188
|
+
f"{m.effective_context:,}",
|
|
189
|
+
f"{m.cost_multiplier:.2f}x",
|
|
190
|
+
f"{m.bytes_per_token:.2f}",
|
|
191
|
+
)
|
|
192
|
+
console.print(table)
|
|
193
|
+
console.print()
|
|
194
|
+
|
|
195
|
+
if json_out:
|
|
196
|
+
json_out.parent.mkdir(parents=True, exist_ok=True)
|
|
197
|
+
with open(json_out, "w", encoding="utf-8") as f:
|
|
198
|
+
json.dump(report.model_dump(), f, indent=2)
|
|
199
|
+
console.print(f"[bold green]✓ Exported JSON report to {json_out}[/bold green]")
|
|
200
|
+
|
|
201
|
+
if md_out:
|
|
202
|
+
md_out.parent.mkdir(parents=True, exist_ok=True)
|
|
203
|
+
with open(md_out, "w", encoding="utf-8") as f:
|
|
204
|
+
f.write(f"# TokenAtlas Multilingual Benchmark Report\n\n")
|
|
205
|
+
f.write(f"- **Evaluated:** {report.timestamp}\n")
|
|
206
|
+
f.write(f"- **Source:** {report.corpora_source}\n")
|
|
207
|
+
f.write(f"- **Sample Sentences:** {samples}\n\n")
|
|
208
|
+
for bench in report.results:
|
|
209
|
+
f.write(f"## {bench.tokenizer_name} (Vocab: {bench.vocab_size:,})\n\n")
|
|
210
|
+
f.write("| Language | Code | Family | Tokens | Inflation Index (τ) | Effective Context | Cost Multiplier | Bytes / Token |\n")
|
|
211
|
+
f.write("| :--- | :---: | :--- | :---: | :---: | :---: | :---: | :---: |\n")
|
|
212
|
+
for m in bench.metrics.values():
|
|
213
|
+
f.write(f"| {m.language_name} | `{m.language_code}` | {m.family} | {m.token_count:,} | **{m.inflation_index:.2f}x** | {m.effective_context:,} | {m.cost_multiplier:.2f}x | {m.bytes_per_token:.2f} |\n")
|
|
214
|
+
f.write("\n")
|
|
215
|
+
console.print(f"[bold green]✓ Exported Markdown summary to {md_out}[/bold green]")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@app.command("export-web")
|
|
219
|
+
def export_web_cmd(
|
|
220
|
+
benchmark_json: Path = typer.Option(
|
|
221
|
+
Path("benchmarks/flores200_benchmark.json"),
|
|
222
|
+
"--benchmark-json",
|
|
223
|
+
"-b",
|
|
224
|
+
help="Path to precomputed benchmark JSON report",
|
|
225
|
+
),
|
|
226
|
+
out_dir: Path = typer.Option(
|
|
227
|
+
Path("docs"),
|
|
228
|
+
"--out-dir",
|
|
229
|
+
"-o",
|
|
230
|
+
help="Output directory for static web dashboard",
|
|
231
|
+
),
|
|
232
|
+
):
|
|
233
|
+
"""Generate standalone interactive Web Atlas dashboard for GitHub Pages / HF Space."""
|
|
234
|
+
if not benchmark_json.exists():
|
|
235
|
+
console.print(f"[bold red]Error: Benchmark report not found at {benchmark_json}[/bold red]")
|
|
236
|
+
console.print("Run 'token-atlas bench --languages all --json-out benchmarks/flores200_benchmark.json' first.")
|
|
237
|
+
raise typer.Exit(1)
|
|
238
|
+
|
|
239
|
+
with open(benchmark_json, "r", encoding="utf-8") as f:
|
|
240
|
+
data = json.load(f)
|
|
241
|
+
|
|
242
|
+
with console.status(f"[bold cyan]Compiling interactive Web Atlas into {out_dir}...[/bold cyan]"):
|
|
243
|
+
out_file = generate_web_dashboard(data, out_dir=out_dir)
|
|
244
|
+
|
|
245
|
+
console.print(f"[bold green]✓ Successfully generated Web Atlas dashboard at {out_file}[/bold green]")
|
|
246
|
+
console.print(f"[dim]Deployable directly to GitHub Pages (docs/) or Hugging Face Spaces (static HTML).[/dim]")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@app.command("explain-bytes")
|
|
250
|
+
def explain_bytes_cmd(
|
|
251
|
+
text: str = typer.Option(..., "--text", "-t", help="Text to inspect UTF-8 byte breakdown"),
|
|
252
|
+
):
|
|
253
|
+
"""Diagnose UTF-8 byte layout and explain tokenizer byte-fallback fragmentation."""
|
|
254
|
+
from token_atlas.bpe import explain_fragmentation
|
|
255
|
+
diag = explain_fragmentation(text)
|
|
256
|
+
|
|
257
|
+
console.print(Panel(f"[bold white]{text}[/bold white]", title="[bold cyan]UTF-8 Byte Fragmentation Diagnostic[/bold cyan]"))
|
|
258
|
+
console.print(f"Characters: [bold cyan]{diag['character_count']}[/bold cyan] | UTF-8 Bytes: [bold magenta]{diag['utf8_byte_count']}[/bold magenta] | Density: [bold yellow]{diag['bytes_per_char']:.2f} bytes/char[/bold yellow]\n")
|
|
259
|
+
|
|
260
|
+
table = Table(title="[bold]Per-Character UTF-8 Byte Breakdown[/bold]")
|
|
261
|
+
table.add_column("Char", style="bold green", justify="center")
|
|
262
|
+
table.add_column("Unicode", style="cyan")
|
|
263
|
+
table.add_column("Bytes", justify="right", style="magenta")
|
|
264
|
+
table.add_column("Raw Hex Bytes", style="dim")
|
|
265
|
+
|
|
266
|
+
for item in diag["sample_breakdown"]:
|
|
267
|
+
table.add_row(item["char"], item["unicode"], str(item["byte_count"]), item["raw_hex"])
|
|
268
|
+
console.print(table)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@app.command("bench-domain")
|
|
272
|
+
def bench_domain(
|
|
273
|
+
domain: str = typer.Option("all", "--domain", "-d", help="Domain to benchmark: code, indentation, tools, math, or all"),
|
|
274
|
+
tokenizers: str = typer.Option("gpt-4o,gpt-4", "--tokenizers", "-t", help="Comma-separated tokenizer IDs"),
|
|
275
|
+
json_out: Optional[Path] = typer.Option(None, "--json-out", help="Path to export raw benchmark JSON"),
|
|
276
|
+
):
|
|
277
|
+
"""Benchmark multi-domain tokenization efficiency (Code, Indentation, Tool Calling, Math)."""
|
|
278
|
+
from token_atlas.domains import evaluate_domain
|
|
279
|
+
tok_list = [t.strip() for t in tokenizers.split(",") if t.strip()]
|
|
280
|
+
|
|
281
|
+
with console.status(f"[bold cyan]Evaluating domain '{domain}' across {len(tok_list)} tokenizers...[/bold cyan]"):
|
|
282
|
+
report = evaluate_domain(domain=domain, tokenizer_ids=tok_list)
|
|
283
|
+
|
|
284
|
+
table = Table(title=f"[bold cyan]Multi-Domain Tokenization Benchmark: {domain.upper()}[/bold cyan]")
|
|
285
|
+
table.add_column("Domain", style="magenta", no_wrap=True)
|
|
286
|
+
table.add_column("Sample / Task", style="white")
|
|
287
|
+
table.add_column("Tokenizer", style="bold green", no_wrap=True)
|
|
288
|
+
table.add_column("Tokens", justify="right", style="cyan", no_wrap=True)
|
|
289
|
+
table.add_column("Bytes", justify="right", style="dim", no_wrap=True)
|
|
290
|
+
table.add_column("BPT", justify="right", style="yellow", no_wrap=True)
|
|
291
|
+
table.add_column("Rel. Tax", justify="right", style="bold red", no_wrap=True)
|
|
292
|
+
|
|
293
|
+
for row in report.rows:
|
|
294
|
+
rel_style = "bold red" if row.relative_inflation > 1.25 else ("bold green" if row.relative_inflation < 0.95 else "white")
|
|
295
|
+
table.add_row(
|
|
296
|
+
row.domain.upper(),
|
|
297
|
+
row.sample_title,
|
|
298
|
+
row.tokenizer_id,
|
|
299
|
+
f"{row.token_count:,}",
|
|
300
|
+
f"{row.byte_count:,}",
|
|
301
|
+
f"{row.bytes_per_token:.2f}",
|
|
302
|
+
f"[{rel_style}]{row.relative_inflation:.2f}x[/{rel_style}]",
|
|
303
|
+
)
|
|
304
|
+
console.print(table)
|
|
305
|
+
|
|
306
|
+
if report.summary_metrics:
|
|
307
|
+
console.print()
|
|
308
|
+
if "indentation_tax_ratio" in report.summary_metrics:
|
|
309
|
+
summary_table = Table(title="[bold yellow]Indentation Tax Summary (4-Space vs Minified)[/bold yellow]")
|
|
310
|
+
summary_table.add_column("Tokenizer", style="bold green")
|
|
311
|
+
summary_table.add_column("Indentation Inflation Multiplier", style="bold red")
|
|
312
|
+
for tok, ratio in report.summary_metrics["indentation_tax_ratio"].items():
|
|
313
|
+
summary_table.add_row(tok, f"{ratio:.2f}x")
|
|
314
|
+
console.print(summary_table)
|
|
315
|
+
|
|
316
|
+
if "tool_schema_whitespace_tax" in report.summary_metrics:
|
|
317
|
+
summary_table = Table(title="[bold yellow]Tool-Calling Schema Spacing Tax (Verbose vs Compact)[/bold yellow]")
|
|
318
|
+
summary_table.add_column("Tokenizer", style="bold green")
|
|
319
|
+
summary_table.add_column("Schema Spacing Multiplier", style="bold red")
|
|
320
|
+
for tok, ratio in report.summary_metrics["tool_schema_whitespace_tax"].items():
|
|
321
|
+
summary_table.add_row(tok, f"{ratio:.2f}x")
|
|
322
|
+
console.print(summary_table)
|
|
323
|
+
|
|
324
|
+
if json_out:
|
|
325
|
+
json_out.parent.mkdir(parents=True, exist_ok=True)
|
|
326
|
+
with open(json_out, "w", encoding="utf-8") as f:
|
|
327
|
+
f.write(report.model_dump_json(indent=2))
|
|
328
|
+
console.print(f"\n[bold green]✓ Benchmark report saved to {json_out}[/bold green]")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@app.command("route")
|
|
332
|
+
def route_cmd(
|
|
333
|
+
text: Optional[str] = typer.Option(None, "--text", "-t", help="Text/prompt to route"),
|
|
334
|
+
file: Optional[Path] = typer.Option(None, "--file", "-f", help="Path to text or prompt file"),
|
|
335
|
+
budget: str = typer.Option("balanced", "--budget", "-b", help="Budget tier: frugal, balanced, frontier"),
|
|
336
|
+
):
|
|
337
|
+
"""Recommend the optimal model/tokenizer based on language script and syntax inflation."""
|
|
338
|
+
from token_atlas.router import recommend_model
|
|
339
|
+
if not text and not file:
|
|
340
|
+
console.print("[bold red]Error: Either --text or --file must be specified.[/bold red]")
|
|
341
|
+
raise typer.Exit(1)
|
|
342
|
+
if file:
|
|
343
|
+
text = file.read_text(encoding="utf-8")
|
|
344
|
+
|
|
345
|
+
rec = recommend_model(text, budget_tier=budget)
|
|
346
|
+
console.print(Panel(rec.explanation, title="[bold cyan]Tokenizer-Aware Model Routing Decision[/bold cyan]"))
|
|
347
|
+
|
|
348
|
+
table = Table(title="[bold]Evaluated Candidate Models & Effective Costs[/bold]")
|
|
349
|
+
table.add_column("Model ID", style="bold green")
|
|
350
|
+
table.add_column("Headline $/1M", justify="right", style="cyan")
|
|
351
|
+
table.add_column("Est. Inflation", justify="right", style="yellow")
|
|
352
|
+
table.add_column("Effective $/1M", justify="right", style="bold magenta")
|
|
353
|
+
table.add_column("Tokens Est.", justify="right", style="white")
|
|
354
|
+
table.add_column("Key Profile / Strengths", style="dim white")
|
|
355
|
+
|
|
356
|
+
for c in rec.all_candidates:
|
|
357
|
+
style = "bold green" if c.model_id == rec.recommended_model else ""
|
|
358
|
+
table.add_row(
|
|
359
|
+
f"[{style}]{c.model_id}[/{style}]" if style else c.model_id,
|
|
360
|
+
f"${c.headline_price_per_1m:.2f}",
|
|
361
|
+
f"{c.estimated_inflation:.2f}x",
|
|
362
|
+
f"${c.effective_cost_per_1m:.2f}",
|
|
363
|
+
f"{c.tokens_estimated:,}",
|
|
364
|
+
c.strengths,
|
|
365
|
+
)
|
|
366
|
+
console.print(table)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@app.command("minify")
|
|
370
|
+
def minify_cmd(
|
|
371
|
+
text: Optional[str] = typer.Option(None, "--text", "-t", help="Text/prompt to minify"),
|
|
372
|
+
file: Optional[Path] = typer.Option(None, "--file", "-f", help="Path to text or prompt file"),
|
|
373
|
+
out: Optional[Path] = typer.Option(None, "--out", "-o", help="Optional output path to write minified text"),
|
|
374
|
+
tokenizer: str = typer.Option("gpt-4o", "--tokenizer", help="Tokenizer ID to calculate token savings"),
|
|
375
|
+
):
|
|
376
|
+
"""Losslessly compress prompt whitespace and embedded JSON schemas to cut token tax."""
|
|
377
|
+
from token_atlas.minify import minify_prompt
|
|
378
|
+
if not text and not file:
|
|
379
|
+
console.print("[bold red]Error: Either --text or --file must be specified.[/bold red]")
|
|
380
|
+
raise typer.Exit(1)
|
|
381
|
+
if file:
|
|
382
|
+
text = file.read_text(encoding="utf-8")
|
|
383
|
+
|
|
384
|
+
res = minify_prompt(text, tokenizer_id=tokenizer)
|
|
385
|
+
|
|
386
|
+
console.print(Panel(
|
|
387
|
+
f"Original: [cyan]{res.original_char_count}[/cyan] chars ([magenta]{res.original_tokens or '—'}[/magenta] tokens)\n"
|
|
388
|
+
f"Minified: [green]{res.minified_char_count}[/green] chars ([green]{res.minified_tokens or '—'}[/green] tokens)\n"
|
|
389
|
+
f"Tokens Saved: [bold yellow]{res.tokens_saved or 0}[/bold yellow] ([bold green]{res.token_reduction_pct or 0.0}% reduction[/bold green])",
|
|
390
|
+
title="[bold cyan]Prompt Token Minification Summary[/bold cyan]"
|
|
391
|
+
))
|
|
392
|
+
|
|
393
|
+
if out:
|
|
394
|
+
out.write_text(res.minified_text, encoding="utf-8")
|
|
395
|
+
console.print(f"[bold green]✓ Minified prompt written to {out}[/bold green]")
|
|
396
|
+
else:
|
|
397
|
+
console.print("[bold dim]Minified Output Preview:[/bold dim]")
|
|
398
|
+
console.print(res.minified_text[:500] + ("..." if len(res.minified_text) > 500 else ""))
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
if __name__ == "__main__":
|
|
402
|
+
app()
|