tiktoken 3.7.0

A high-performance pure-Rust implementation of OpenAI's tiktoken BPE tokenizer
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
# Changelog

## [3.7.0] - 2026-08-08

### Added
- **Six new encodings for the 2026-08 model landscape**, with emphasis on the
  Chinese open-weights ecosystem — every one verified byte-exact against its
  reference tokenizer over a 10,489–10,491-case differential corpus
  (104,903 comparisons total, 0 divergences):
  - `kimi_k2` / `kimi_k3` (Moonshot Kimi K2 / K2.5 / K2.6 and K3): 163,584-token
    native-tiktoken vocabulary, byte-identical across both generations; the
    generations differ only in special-token tables (K2's `<|im_*|>` chat
    markers vs K3's `<|end_of_msg|>` / media tokens). The split pattern is an
    o200k variant with a dedicated `[\p{Han}]+` branch and Han-excluding
    character-class intersections.
  - `glm4` (Zhipu GLM-4.5 / 4.6 / 4.7, 151,329 tokens) and `glm5` (GLM-5 /
    5.2, 154,820 tokens): independently trained vocabularies sharing the
    cl100k split pattern and a common 36-entry special-token table.
  - `minimax_m2` (MiniMax M2 / M2.1 / M2.5 / M2.7): 200,000-token vocabulary,
    byte-identical across the family; o200k letter rules with a `[\r\n/]*`
    punctuation tail (caught by the differential — the tail admits `/`).
  - `deepseek_v4` (DeepSeek V4 Pro / Flash): shares V3's vocabulary, merges,
    and pattern; extends the added-token table from 818 to 1,283 entries
    (`<think>`, DSML markup, vision/grounding tags, 415 multimodal span
    placeholders).
- Model mapping for the new families: `kimi-k2*` / `kimi-k3*` / `kimi-latest`,
  `glm-4*` / `glm-5*`, `minimax*`, `deepseek-v4*` — plus the DeepSeek API
  aliases `deepseek-chat` and `deepseek-reasoner`, which point at V4 since
  2026-07-24.
- Pricing: 13 models across three new providers — Moonshot (`kimi-k3`,
  `kimi-k2.7-code`, `kimi-k2.6`, `kimi-k2.5`), Zhipu (`glm-5.2`, `glm-5`,
  `glm-4.7`, `glm-4.5`, `glm-4.5-air`), MiniMax (`minimax-m2.7` / `-m2.5` /
  `-m2.1` / `-m2`) — all first-party rates read from the providers' official
  price cards on 2026-08-08. The table now covers **107 models across 10
  providers**. Max-output limits are not published by these three providers;
  entries carry a conservative 32K placeholder.
- `tests/fixtures/generate_kimi_fixtures.py` (reference fixtures from
  Moonshot's native `tiktoken.model` + `tokenization_kimi.py` pat_str) and
  `src/encodings/convert_hf_vocab.py` (the HF ByteLevel-BPE → `.tiktoken.zst`
  converter with a built-in differential self-check — previously this
  conversion was done by unversioned scripts).

### Changed
- **Breaking (match exhaustiveness):** `pricing::Provider` gains `Moonshot`,
  `Zhipu`, and `MiniMax` variants.
- Embedded vocabulary data grows from ~5.9 MB to ~10.3 MB compressed
  (kimi 1.05 MB, glm4 0.93 MB, glm5 0.94 MB, minimax_m2 1.29 MB), which
  increases compiled artifact sizes accordingly (the wasm binary most
  noticeably).
- The published package now excludes `tests/` (several MB of oracle fixtures
  that only validate this crate against upstream tokenizers — they remain in
  the repository and CI) and the `examples/react-app/` demo. This keeps the
  package under crates.io's 10 MB cap, at 99.4% of it; adding another
  vocabulary will require splitting the data into a separate crate or a cap
  increase from crates.io — noted here so the next release plans for it.


## [3.6.0] - 2026-08-08

### Fixed
- **`cl100k_base` and `o200k_base` split canonical newline tokens**
  ([#5]https://github.com/goliajp/rust-tiktoken/issues/5). The DFA-compatible
  pre-tokenizer applied its `\s+(?!\S)` lookahead emulation to every match,
  including matches produced by the patterns' dedicated `\s*[\r\n]+` branch.
  That branch carries no lookahead, so trimming it split `"\n\n"` and `"\r\n"`
  into separate single-newline pieces whenever non-whitespace followed —
  changing token ids and overcounting affected text. `encode("word\n\nnext")`
  returned `[1178, 198, 198, 3684]` instead of the canonical `[1178, 271, 3684]`.

  The emulation is now pattern-aware: encodings whose pattern has a newline
  branch ahead of the generic whitespace rules (`cl100k_base`, `o200k_base`,
  `o200k_harmony`, `llama3`, `mistral_v3`, `qwen2`, `deepseek_v3`) never trim a
  match ending in `\r`/`\n`, while `p50k_base` / `p50k_edit` / `r50k_base` keep
  the unconditional trim their patterns require.

  Verified against canonical `openai/tiktoken` 0.13.0 over a 10,491-case
  adversarial corpus: **2,504 divergences before, 0 after**, across all six
  OpenAI encodings (62,946 comparisons).

  **This changes token ids for affected text.** Any cached token counts or
  stored token id sequences for newline-containing text should be recomputed.

- **`mistral_v3` used the cl100k pattern instead of Tekken's.** The encoding was
  defined as `MISTRAL_V3_PATTERN = CL100K_PATTERN`, but Tekken's pre-tokenizer
  differs in three ways: it splits on case like o200k, it has no contraction
  rule at all, its number rule is `\p{N}` rather than `\p{N}{1,3}`, and its
  punctuation rule's trailing class is `[\r\n/]*` — admitting `/`. The first two
  differences happen to be masked by the Tekken vocabulary, but the `/` tail is
  not: any punctuation run followed by a newline and then a slash tokenized
  differently from the reference. `mistral_v3` now has its own pattern and a
  dedicated ASCII fast path.

- **`deepseek_v3` split whitespace runs before digits and CJK.** Upstream runs
  three sequential `Split` stages; this crate folds them into one alternation,
  which let the `\s+(?!\S)` lookahead peek past a stage boundary. A whitespace
  run followed by a digit or CJK character was trimmed when upstream would keep
  it whole (`"  1"` → two single-space tokens instead of one `"  "` token).

- **`model_to_encoding` resolved several legacy model ids incorrectly.** A flat
  prefix scan let a short prefix capture a more specific id: `davinci-codex`
  returned `r50k_base` (canonical: `p50k_base`) and `code-davinci-edit-001`
  returned `p50k_base` (canonical: `p50k_edit`). Ten first-generation search /
  similarity embedding models (`text-search-*`, `text-similarity-*`,
  `code-search-*`) returned `None`. Lookup is now exact-match-first, then
  prefix, mirroring upstream's registry structure — all 62 canonical entries now
  agree.

### Added
- **Missing added tokens.** `qwen2` gained the 8 ids upstream defines above
  `<|video_pad|>` (`<tool_call>`, `</tool_call>`, the four `<|fim_*|>` markers,
  `<|repo_name|>`, `<|file_sep|>` — ids 151657..=151664). `deepseek_v3` gained
  the 14 named tokens at 128800..=128814 — including `<|User|>` and
  `<|Assistant|>`, the primary chat-template markers — plus the 800
  `<|place▁holder▁no▁N|>` entries at 128000..=128799. Its doc comment claimed
  804 special tokens while the code registered 4;
  `encode_with_special_tokens` could not produce any of the missing ids.
- `gpt2` oracle fixture and test — the encoding was in the registry but had no
  fixture coverage.
- Pricing: 26 models across the 2026-08 landscape — OpenAI GPT-5.6
  (`sol` / `terra` / `luna`), the `-pro` SKUs and 5.1 / 5.2 point releases;
  Anthropic's Claude 5 generation (`fable-5`, `mythos-5`, `opus-5`, `sonnet-5`);
  `gemini-3.6-flash`, `gemini-3.5-flash-lite`, `gemini-2.5-flash-lite`;
  `deepseek-v4-pro` / `-flash`; `qwen3.8-max` / `qwen3.5-plus`; Mistral's
  Devstral 2 and Ministral 3 SKUs. The table now covers **94 models**.

### Changed
- Pricing corrections: Claude 4.6+ context windows are 1M (those models carry
  the full window at standard rates); `mistral-small` updated to the Small 4
  rates ($0.15/$0.60). `deepseek-v3` / `deepseek-r1` and `gemini-2.0-flash` are
  marked DEPRECATED — they are off their vendors' current price cards.
- Oracle fixtures are now generated from the reference implementations, not from
  this crate's own output — `tests/fixtures/generate_openai_fixtures.py` for the
  OpenAI encodings (canonical `openai/tiktoken`) and
  `tests/fixtures/generate_hf_fixtures.py` for the HuggingFace-sourced ones. The
  previous snapshots were self-generated, so they could only prove the crate
  still agreed with itself, which is how issue #5 survived a green test suite.
  Corpora grew from ~90 to 2,374 cases (OpenAI) and from 41 to 2,815 cases (HF),
  with systematic whitespace-run × follower matrices covering the newline,
  digit, CJK and slash axes where these patterns disagree.

### Performance
- ASCII fast paths are **5–26% faster than 3.5.1** (same machine, same
  toolchain, criterion n=100): cl100k encode 13 B 45.7→37.9 ns, 45 KB
  77.8→67.3 µs; o200k 900 B 2.06→1.52 µs, 45 KB 92.9→79.2 µs. The scanners'
  per-pattern variations (digit cap, contraction rule, punctuation tail) are
  const-generic parameters with `#[inline(always)]`, so each `FastPath` gets
  fully specialized, inlined codegen. Unicode/CJK inputs are unchanged (regex
  path).

### Internal
- Removed `tests/generate_oracle.rs` (the self-generating oracle it replaced).
- Added `tests/canonical_parity.rs`, an `--ignored` full-corpus differential
  against the reference implementations, plus `tests/canonical_corpus.py` and
  `tests/hf_corpus.py` to generate its input. The 3.6.0 fixes were verified with
  it over 104,907 comparisons across all 10 encodings, 0 divergences.
- Added fast-path/regex equivalence proptests for the new Tekken scanner
  (including a slash-dense generator), newline-dense generators for the `cl100k`
  / `o200k` fast paths, and a `p50k` equivalence proptest.

## [3.5.1] - 2026-06-07

### Changed
- Dual-licensed under **MIT OR Apache-2.0** (previously MIT only). `LICENSE` is
  split into `LICENSE-MIT` + `LICENSE-APACHE`, and the crate `license` field is
  now `"MIT OR Apache-2.0"` — the conventional permissive dual-license for Rust
  crates.

### Internal
- rustfmt + clippy (`-D warnings`) cleanup of the 3.5.0 fast-path code — no
  behavior change. Workspace README rewritten; `LICENSE-MIT` copyright year
  unified to 2023–2026.

## [3.5.0] - 2026-06-07

> Version note: `tiktoken` and `tiktoken-wasm` are now kept in lockstep at the
> same version number (both jump to 3.5.0 here), so a given release tag means the
> same code across both packages.

### Added
- ASCII fast-path pre-tokenizers for the `cl100k`, `o200k`, `qwen2`, and
  `deepseek_v3` patterns (also covering `llama3` / `mistral_v3`, which share the
  cl100k pattern). These hand-written scanners resolve the common ASCII pieces
  (letters, digits, punctuation, contractions) without invoking the regex
  engine, deferring to it only on non-ASCII bytes or whitespace runs. The
  scanner is selected per-encoding via an internal `FastPath` injected from
  `encoding.rs`, so the pre-tokenizer stays unaware of any specific pattern
  string. Verified byte-for-byte identical to the regex by per-pattern
  equivalence proptests (20k cases each).
- Pricing entries: `claude-opus-4.8`, `claude-opus-4.7`,
  `gemini-3.1-pro-preview`, `gemini-3.5-flash`, `gemini-3.1-flash-lite`
  (verified against official docs; Claude/Gemini remain pricing-only — neither
  vendor publishes a tokenizer, so they have no encoding).

### Changed
- BPE merge is now hybrid: pieces up to 32 bytes use an allocation-free,
  stack-based linear scan; longer pieces keep the heap-accelerated O(n log n)
  algorithm. After pre-tokenization most pieces are word-sized, where the heap's
  allocation + bookkeeping overhead dominated.
- `encode_parallel` rewritten as a chunked two-pass (one buffer per worker
  instead of one `Vec` per piece) and its serial-fallback threshold raised from
  4 KB to 512 KB: with the fast-path making serial encoding cheap and
  pre-tokenization staying serial (Amdahl), the parallel path only wins above
  ~460 KB.

### Performance
- ASCII-heavy `encode` / `count` is **2.3–5.5x faster** across cl100k / o200k /
  qwen2 / deepseek (e.g. cl100k encode of 45 KB English: ~350 µs → ~70 µs on an
  Apple M4 Mac mini). Unicode/CJK text is unchanged — it defers to the regex,
  whose Unicode-property matching was never the bottleneck the fast-path targets.

## [3.3.0] - 2026-06-07

### Added
- `gpt2` registered as the 11th encoding name (alias for `r50k_base`).
  `model_to_encoding` resolves `gpt2` / `gpt-2` model prefixes to
  `"r50k_base"`; `get_encoding("gpt2")` shares r50k_base's cache slot.
- Six OpenAI GPT-5.x model entries with full available tier data:
  `gpt-5`, `gpt-5-mini`, `gpt-5-nano` (Standard only), `gpt-5.4`
  (Standard + Batch + Flex + long-context >272K),
  `gpt-5.4-mini` (Standard + Batch + Flex),
  `gpt-5.5` (Standard + Batch + Flex + Priority + long-context).
- `pricing::TierRates { input_per_1m, cached_input_per_1m: Option<f64>,
  output_per_1m }` — unified rate type shared by all OpenAI service tiers.
- `ExtendedPricing.flex` and `.priority`, both `Option<TierRates>`.
- `Model::with_batch_cached`, `with_flex`, `with_priority` const builders.
- `Model::estimate_flex_cost` and `estimate_priority_cost`.

### Changed
- **Breaking**`pricing::VisionPricing` is now a provider-specific enum
  (`OpenAITileBased` / `OpenAIPatchBased` / `AnthropicDivisor` /
  `GeminiTileBased`) instead of the 3.2 placeholder
  `VisionPricing { per_image: f64 }`. Image inputs are billed at the
  model's standard `input_per_1m` rate, not a flat per-image fee, so the
  enum captures each provider's published image→tokens formula
  (`VisionPricing::image_tokens(width, height, detail)`) and
  `Model::estimate_image_cost(width, height, detail)` returns the
  end-to-end USD figure (auto-applies high-tier rates above the
  gemini-2.5-pro 200k threshold). The unused 3.2 builder
  `with_vision_per_image` is removed; use `with_vision(VisionPricing)`.
  Vision data populated for gpt-4o / gpt-4o-mini / gpt-4.1 / gpt-4.1-mini /
  gpt-4.1-nano / o1 / o3 / o4-mini, claude-haiku-4.5, claude-sonnet-4.5 /
  4.6, claude-opus-4.5 / 4.6, gemini-2.5-pro / 2.5-flash.
- `pricing::BatchPricing` is now a type alias for `TierRates` (was a
  distinct struct in 3.2.x). Existing `with_batch(input, output)` calls
  still compile; the `cached_input_per_1m` field defaults to `None`.
- README files across all 3 languages × 3 levels bump encoding count
  `9 → 11` and model count `57 → 63`; added `o200k_harmony` and `gpt2`
  rows to the Supported Encodings tables.

## [3.2.0] - 2026-06-06

### Added
- `o200k_harmony` encoding (10th encoding) for OpenAI gpt-oss models /
  harmony chat format. Shares o200k_base merge ranks and regex pattern;
  only the special-token table differs (15 named + 1075 reserved
  placeholders). `model_to_encoding` gains a `gpt-oss` prefix that
  routes to it.
- Pricing schema extensions (`pricing::BatchPricing`,
  `pricing::HighTierPricing`, `pricing::AudioPricing`,
  `pricing::VisionPricing`, grouped under `pricing::ExtendedPricing` on
  `Model`). Optional dimensions for batch-API discounts, Google's
  input-token-count-based dual-tier rates, and per-modality pricing.
- `Model::pricing_for_input(input_tokens)` returns the tier-appropriate
  `Pricing` (auto-switches when `extended.high_tier` is set and input
  exceeds its threshold).
- `Model::estimate_batch_cost(input, output)` returns `Some(cost)` when
  the model has batch pricing, else `None`.
- `const fn` builder methods on `Model`: `with_batch`, `with_high_tier`,
  `with_audio_input`, `with_vision_per_image`.
- Data filled (only where verified by 2026-06 research): batch pricing
  for 13 OpenAI + 5 active Anthropic models; `gemini-2.5-pro` high-tier
  (>200k input) rates; `gemini-2.5-flash` audio input rate $1.00/M.

### Changed
- `pricing::Model` is now `#[non_exhaustive]`. Reading its fields is
  unchanged, but constructing it via a struct literal outside the crate
  is no longer allowed (use the internal `model()` + builder pattern
  instead).
- `estimate_cost` and `estimate_cost_with_cache` now route through
  `pricing_for_input`, so they auto-pick high-tier rates for
  `gemini-2.5-pro` above the 200k input threshold.
- Meta llama prices re-sourced per-model: pinned to DeepInfra
  (`llama-3.1-8b`, `llama-3.3-70b`, `llama-4-maverick`) and Groq
  (`llama-4-scout`) with source URLs in each entry's doc comment.
  `llama-3.1-405b` and `llama-3.1-70b` marked `DEPRECATED`  no major hoster offers them as serverless inference any longer.

## [3.1.5] - 2026-06-06

### Added
- `model_to_encoding` now supports `gpt-5` and `gpt-4.5` prefixes → `o200k_base`.
- `model_to_encoding` strips a leading `ft:` prefix so fine-tuned model IDs (e.g.
  `ft:gpt-4o:my-org::abc123`) resolve to the base model's encoding.
- Azure-style `gpt-35-turbo` alias → `cl100k_base`.

### Fixed
- `davinci-002` and `babbage-002` now correctly route to `cl100k_base` (they were
  greedily caught by the `davinci`/`babbage` prefix in the `r50k_base` block).

### Changed
- Pricing refreshed against official 2026-06 docs across all 7 providers:
  - 13 price corrections (notably OpenAI `o1-mini` to match `o3-mini` at $1.10/$4.40,
    Anthropic `claude-opus-4`/`claude-sonnet-4` cache values to cache-read convention,
    Alibaba Qwen 2.5 family now uses official Model Studio split rates, Mistral
    `mistral-large` cut to $0.5/$1.5, `mistral-medium` repositioned to $1.5/$7.5).
  - 21 `DEPRECATED` doc-comments (OpenAI shutdown 2026-10-23, Anthropic Claude 3.x
    retired, claude-opus-4/sonnet-4 retire 2026-06-15, Gemini 2.0 Flash shutdown,
    Gemini 1.5 family removed from official pricing, DeepSeek `deepseek-chat`/
    `deepseek-reasoner` deprecate 2026-07-24, Mistral `pixtral-large`).
  - Module-level caveats document the Anthropic cache-read convention, the Meta
    llama legacy-Together source, and the `gemini-2.5-pro` single-tier limitation.
- Dropped `rust-version` MSRV pin from workspace; tracks current stable.
- CI release workflow: Node 20 → 24, `wasm-pack` install switched to
  `taiki-e/install-action` (prebuilt binary, ~5s vs ~5–10 min compile).

## [3.1.4] - 2026-04-24

### Changed
- Smoke-test release via the new repo's GitHub Actions publish workflow.
  No code changes.

## [3.1.3] - 2026-04-24

### Changed
- Migrated from `goliajp/airs` mono-repo to standalone `goliajp/rust-tiktoken` (shares repo with `tiktoken-wasm`).
  No code changes; `repository` URL updated.

## [3.0.1] - 2026-03-07

### Changed

- Updated benchmark tables in README to v3.0 numbers (all three languages)

## [3.0.0] - 2026-03-07

### Added

- **Multi-provider tokenizer support**: Llama 3 (`llama3`), DeepSeek V3 (`deepseek_v3`), Qwen 2 (`qwen2`), Mistral V3 (`mistral_v3`) encodings
- **Parallel encoding**: `encode_parallel()` method behind `parallel` feature flag, uses rayon for texts >= 4KB
- **count_with_special_tokens()**: counting method that recognizes special tokens, matching `encode_with_special_tokens()` behavior
- **Multi-provider pricing**: Meta Llama, DeepSeek, Alibaba Qwen, Mistral models added to pricing module (39 total models across 7 providers)
- HuggingFace oracle tests: cross-validation against Python HF tokenizer output for all new encodings
- Property-based testing: 125,000 random input roundtrip tests via proptest
- Third-party license documentation (`LICENSE-3RD-PARTY`)
- Vocab conversion scripts (`scripts/convert_vocab.py`, `scripts/generate_hf_oracle.py`)

### Changed

- **Breaking**: internal architecture rewritten for performance
  - `FxHashMap<Vec<u8>, u32>` replaced with arena-based `Vocab` (single allocation, cache-friendly)
  - BPE merge algorithm: O(n*m) linear scan replaced with O(n log n) heap-accelerated merge (BinaryHeap + doubly-linked list)
  - Pre-tokenization abstracted behind `PreTokenizer` trait (`RegexPreTokenizer` implementation)
- Vocabulary data now zstd-compressed at rest (~63% compression ratio, 7MB -> 2.6MB for OpenAI vocabs)
- Internal modules (`vocab`, `merge`, `pretokenize`) use `pub(crate)` visibility
- `model_to_encoding()` now supports Llama, DeepSeek, Qwen, and Mistral model name prefixes

### Fixed

- NBSP overflow bug in whitespace lookahead emulation: single multi-byte whitespace characters (e.g. U+00A0) no longer cause empty piece underflow
- DeepSeek ZWJ character handling: format characters (Unicode Cf category) no longer skipped by regex

### Performance

- Arena-based vocabulary: single contiguous allocation replaces 200k individual `Vec<u8>` heap allocations
- Heap-accelerated BPE merge: O(n log n) vs O(n*m) for the ~5% of pieces that enter the merge path
- Fast paths for 1-byte and 2-byte pieces in BPE merge

## [2.1.1] - 2026-03-07

### Changed

- Upgraded Rust edition 2021 -> 2024, MSRV 1.85 -> 1.94
- Upgraded `criterion` dev-dependency 0.5 -> 0.8
- Migrated Cargo.toml metadata to workspace inheritance (edition, rust-version, license, repository, homepage, authors)
- Moved to `airs` monorepo workspace

## [2.1.0] - 2026-03-06

### Performance

- Rank-cached BPE merge: only recomputes 2 neighbor ranks per merge step
- 2-byte piece fast path in `byte_pair_merge`
- Unicode encode/count improved ~52%, ASCII text improved 2-10%

### Added

- `decode_to_string()` method for ergonomic UTF-8 decoding
- `model_to_encoding()` as public API
- `o200k_base` encoding (GPT-4o, o1, o3, o4-mini)
- Multi-provider pricing module (`tiktoken::pricing`) -- OpenAI, Anthropic Claude, Google Gemini (26 models)
- WebAssembly bindings (`tiktoken-wasm`) with encode/decode/count/pricing
- React demo app (`examples/react-app`)
- Criterion benchmarks with Python tiktoken 0.12.0 comparison
- Trilingual documentation (English, simplified Chinese, Japanese)
- 93 tests (82 unit + 11 doc tests), 97% line coverage

### Changed

- **Breaking**: rewritten from scratch -- new engine, new dependencies, new API surface
- Replaced `pcre2` (backtracking) with `regex` crate (DFA engine)
- Replaced `HashMap` with `FxHashMap` for faster small-key hashing
- Replaced `lazy_static` with `OnceLock` for encoding instance caching
- Upgraded `base64` 0.21 -> 0.22
- Upgraded edition 2021 -> 2024, MSRV set to 1.85
- Removed dependencies: `anyhow`, `maplit`, `rust_decimal`, `rust_decimal_macros`

### Removed

- `src/models.rs` and `src/price.rs` (replaced by `src/pricing.rs`)

## [1.0.1] - 2023-08-18

- Initial release on crates.io