Repository navigation
Conversation
One run of underscores, spaces, dots or letters is a single regex piece, and bytePairMerge is quadratic in the piece length. At 100k characters the four shapes take 14 to 20 s here, so this test fails until the merge is fixed. See #6. Change-Id: I140f5a21ac255a662fa4d06a4098144c0429857f Signed-off-by: Thomas Kosiewski <tk@coder.com>
bytePairMerge rescanned every pair rank and spliced two arrays for each merge, so one long piece (a run of underscores, spaces or dots, which the split pattern does not bound) cost O(n^2). Keep the parts in a linked list and pick merges from a min-heap ordered by (rank, position). The merges and their order are the same as before: lowest rank first, leftmost on ties. The differential test compares the new merge with a copy of the old one on random, adversarial and real pieces for all four encodings. Change-Id: I69cf8dbccdf791d5c1b7963df679631d13e4f208 Signed-off-by: Thomas Kosiewski <tk@coder.com>
Packs the package, installs it into a fresh project and typechecks the README example under bundler, node16 (ESM and CJS) and node10 resolution. It fails today: index.d.ts and sdk.d.ts each declare their own Tokenizer class, so a Tokenizer from 'ai-tokenizer' is not assignable to the one 'ai-tokenizer/sdk' expects. See #1. Change-Id: I60e43f6f077fdee94e19394b9bb943fb4910edc4 Signed-off-by: Thomas Kosiewski <tk@coder.com>
index and sdk were two separate tsdown builds, so each bundled its own copy of the Tokenizer class and its .d.ts. TypeScript treats classes with private members as nominal, so a Tokenizer from 'ai-tokenizer' was not assignable to the one 'ai-tokenizer/sdk' expects. Build both entries together so the bundler emits the class and its declaration once, in a shared chunk. Fixes #1. PR #2 found the cause and proposed externalising a shared tokenizer module, which also needs a tsdown upgrade and a new ./tokenizer export. A single multi-entry build needs neither. Change-Id: I849322c12091fa284281a23a8cfa0ded1833712e Signed-off-by: Thomas Kosiewski <tk@coder.com>
ThomasK33
marked this pull request as ready for review
November 3, 2026 11:25
Member
Author
|
@codex review |
The check only counted error lines from the example file and ai-tokenizer. An option error in the temporary tsconfig makes tsc skip the type check of the example, and that error line was dropped, so the check printed PASS. Count every 'error TS' line except those in other packages' node_modules, and treat a failed tsc run with no diagnostic as an error. Change-Id: I66a0c4e16331914e044d902178a91d56074c8132 Signed-off-by: Thomas Kosiewski <tk@coder.com>
Typed arrays drop writes past their end, so a wrong capacity bound would give wrong tokens instead of an error. Route the two pushes made during merging through one function that throws a RangeError when the heap is full. Change-Id: Ib0753ac41e67c820320340b6a46f2b73dc88edf8 Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: Ibe1233321b43e3ec1a03d22a264a5389ad2ad08a Signed-off-by: Thomas Kosiewski <tk@coder.com>
The tokenizer now decodes with ignoreBOM, so its rank lookup and the test's agree on pieces that contain U+FEFF. Drop the skip for them. Change-Id: I75bfdefdf3eb11a5257521a810e4a8d4f8f0bb64 Signed-off-by: Thomas Kosiewski <tk@coder.com>
Member
Author
|
Review record for head
Generated with |
3 tasks
Change-Id: Ifef471c90c94b5027ad2b896cf4e571705daa13d Signed-off-by: Thomas Kosiewski <tk@coder.com>
Encodings with a kind are typed as CodecEncoding, a second member of the Tokenizer constructor's parameter type. Pass one through the shared Tokenizer declaration in the packed-tarball check too. Change-Id: Ie0977de7912f32be1156c837d222c49863477acc Signed-off-by: Thomas Kosiewski <tk@coder.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The operator's goal: "this library wasn't updated in a long while. It needs to get the new tokenizer for opus and for a few other models ... Please take it from here on autopilot and get this landed."
This PR fixes two open bugs in the library first, so the new tokenizer work lands on a base that works.
bytePairMergetook O(n²) time on it. A 1.1M-character OCR-like document took 172 s to count here. One production service went down four times from this.Tokenizerfromai-tokenizertocount()fromai-tokenizer/sdkfailed to typecheck ("Types have separate declarations of a private property 'name'").What
src/tokenizer.ts). The parts of a piece form a linked list. A min-heap of adjacent pairs, ordered by (rank, position), picks the next merge. This is the same rule as before: lowest rank first, leftmost pair on ties. Stale heap entries are skipped when popped. I measured the old short-piece scan against the heap on 61.8k real pieces that need a merge. The heap was not slower, so there is no second code path.tsdown.config.ts).indexandsdkwere two separate builds, so each bundled its ownTokenizerclass and its own.d.ts. TypeScript treats classes with private members as nominal types, so the two classes were not compatible. Both entry points are now one build, and the bundler emits the class and its declaration once, in a shared chunk. No new dependency, nopackage.jsonchange, no new export.src/byte-pair-merge.test.ts: time bounds for the issue's inputs, a differential test against a copy of the old algorithm, and long-run inputs against tiktoken.scripts/check-package-types.tsplus two CI steps: pack the package, install the tarball in a fresh project, and typecheck the README example under bundler, node16 (ESM and CJS) and node10 resolution.The commits are in failing-first order: the timing test (red), the fix, the package-types check (red), the fix.
Credit: the diagnosis in #2 (each entry point bundles its own
Tokenizer, so share one) is what I built on. That PR does it by externalising atokenizermodule, upgrading tsdown and adding a./tokenizerexport. A single multi-entry build gives the same result without those. I did not reuse its code or touch that PR.I did not edit
src/sdk.tsorsrc/models.json. TheTokenizerconstructor and public API are unchanged.Evidence
Issue #6, before and after. Node 22 on the built
dist, fresh tokenizer per row, a shared host with load average 70 to 130, so expect ±30% noise. Token counts are identical before and after in every row.abx 25,000I measured with a small script (not committed) that calls
count()on each input. The sweep of 23 repeated-character shapes x 4 encodings at 100k and 200k characters (92 rows) shows linear growth: the slowest 200k-character row takes 255 ms.Identity with the old algorithm.
bun test src/byte-pair-merge.test.tsprints these count lines:The differential test runs the old merge (a copy in the test, with an independent rank lookup built from the encoding data) and the new merge on 35,428 pieces for
o200k_base,cl100k_base,p50k_baseandclaude. The pieces are random bytes (mostly invalid UTF-8), one repeated byte at 300 lengths, two and three letter alphabets, vocabulary tokens joined and cut at random, random code points with 2, 3 and 4 byte UTF-8, and every distinct regex piece of the repo's own files and some Chinese, Japanese, Arabic, Russian and emoji text. The end to end test comparesTokenizer#encodewith tiktoken on 10 repeated characters x 14 lengths for three encodings. I also broke the new merge four ways (ties go right, no stale-entry check, no update of the left neighbour, a wrong final lookup). The differential test failed on all four.Normal text is not slower.
bun cold-ab.ts: same process, merge cache size 0 so almost every piece that needs a merge pays for it, old and newTokenizerinterleaved, CPU time, 15 rounds, median.On 61.8k real pieces that need a merge: old scan min 302.6 ms / median 382.9 ms, heap min 275.9 ms / median 332.5 ms. Only 50 of the 309k pieces that need a merge are 128 bytes or longer.
bun run bench(runsbench/merge-cache.ts) over 3 alternating rounds: old 15,832 / 25,615 / 18,779 ms, new 13,214 / 19,320 / 16,846 ms.bun run bench/versus.tsshows no consistent difference between old and new, within the host noise (rounds differ by up to 2x). The cache hides the merge in that benchmark.Issue #1. Before:
bun scripts/check-package-types.tson the old build printsFAILfor all four resolutions withTS2322 ... Types have separate declarations of a private property 'name', andpackage types: 0 of 4 resolutions typecheck. After:PASSfor bundler, node16 (ESM), node16 (CJS) and node10, andpackage types: 4 of 4 resolutions typecheck.dist/index.d.tsanddist/sdk.d.tsnow both importTokenizerfrom one sharedindex-<hash>.d.ts. A Node smoke test counts the same total (14) through the ESM and CJS builds.Gates on head
1125a60:bun run format --check,bun run typecheck,bun test(614 pass, 0 fail),bun run build --dts=false,bun run build,bun scripts/check-package-types.ts.Risks
distnow has hashed shared chunks (tokenizer-<hash>.js,index-<hash>.d.ts). Consumers import only the package entry points, so nothing changes for them.npm installof typescript andai(about 30 s).bun run format --checkrunsprettier --write, rewritessrc/models.jsonand cannot fail.bun run benchruns onlybench/merge-cache.ts. Both are pre-existing.ai-tokenizeras ESM and requiresai-tokenizer/sdkas CJS can still see the TS2322 error. The same split exists on main.Review record
@codex reviewon2eafae8at 2026-11-03T11:25:28Z and got no response in over 60 minutes. No other PR in this repo has a Codex response either. No review round was used.indexandsdkare unchanged.a93d793and9bca535:scripts/check-package-types.tsnow fails on anyerror TSline outside other packages'node_modules(with a badmoduleoption in one tsconfig it printsFAIL node10and3 of 4, and the old build config still gives0 of 4). The merge heap throws aRangeErrorinstead of dropping a write if it ever overflows.ab45414) and removed the U+FEFF skip. The differential test now compares every piece.6dec619) with no conflicts. Itskind+codecdispatch in theTokenizerconstructor,encodeanddecodeis untouched, and theclaude_v3,claude_v4_7,claude_v4_8andclaude_v5encodings do not callbytePairMerge.src/claude.test.tspasses (claude count_tokens fixture: 392/392 exact). The packed-types check now also passes aclaude_v5tokenizer tocount(), and a Node smoke test gives the same tokens from the ESM and CJS builds for all four.Generated with
xum• Model:anthropic:claude-sonnet-5-5• Thinking:xhigh