Repository navigation
Conversation
countTokens() from @anthropic-ai/tokenizer builds and frees a WASM tokenizer on every call (~70 ms each) and its encode() runs synchronously on the host's event loop with super-linear cost on huge or degenerate inputs. On long sessions this blocked OpenCode's server for seconds per transform, up to the point of being killed by the client health check mid-compression (upstream Tarquinen#638, Tarquinen#659; see also Tarquinen#639 and Tarquinen#660). - build the tokenizer once and share it (~70 ms -> ~0.2 ms per call) - memoize exact counts (LRU, bounded by entries and total chars) because the same summaries, system prompt and tool outputs are re-counted on every transform and after tool-cache evictions - above DCP_TOKEN_COUNT_MAX_EXACT_CHARS (default 16k characters), extrapolate from head/middle/tail samples instead of encoding everything; sequences with a long single-character run are sampled as well, since their encode cost grows super-linearly - count each item in estimateTokensBatch instead of joining all texts into one string - DCP_TOKEN_DEBUG=1 logs each count for measurement; resetTokenCountingForTests() clears the shared state for tests scripts/bench-token-count.ts measures wall time and missed timer ticks against the package's own countTokens; sampled estimates stay within ~0.6% of exact counts on the bench corpus.
aiev
force-pushed
the
fix/tokenizer-cost
branch
from
October 19, 2026 22:55
bdf3dd5 to
6ce1410
Compare
aiev
marked this pull request as ready for review
October 19, 2026 22:56
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.
Problem
countTokens()from@anthropic-ai/tokenizerbuilds and frees a WASM tokenizer on every call (~70 ms, see #638) and itsencode()runs synchronously on the host's event loop with super-linear cost on huge inputs (see #659). DCP counts a lot of text per chat-messages transform: uncached tool outputs, summaries, the system prompt, and the whole-session breakdown behind/dcp.On a long session (1.5k messages, 7.8 MB) that blocked the server for seconds per pass and ended with the clients' health check (2 s timeout × 3 attempts) declaring the service unresponsive — three times in one afternoon, always during compression — plus a SIGKILL of the service after a stalled stream. The compaction never finished and was retried in a loop every ~2 minutes ("the DCP compact keeps restarting the service"):
countTokensover the session text: ~3.1 s (1 MB ~0.5 s, 100 KB ~0.25 s)/api/infounanswered for 9 sWhat changes
lib/token-utils.tsonly:MAX_TOOL_CACHE_SIZEevictions re-count tool parts.DCP_TOKEN_COUNT_MAX_EXACT_CHARS(default 16k characters) count head/middle/tail samples and extrapolate. Inputs containing a long single-character run (≥ 512) are sampled regardless of size, because their encode cost grows super-linearly (measured ~20 ms at 4k → ~315 ms at 16k characters, warm).estimateTokensBatchcounts each item instead of joining every text into one string.Measured on the same box (
missed ticks= 10 ms ticks lost while the call blocks the event loop):Sampled estimates vs exact counts (prose / JSON / code / half-prose-half-base64, 32 KB–8 MB): −0.6% … +0.4%.
Tests
tests/token-counting.test.tsgains:countTokensfor small inputs (NFKC, emoji, special tokens, a 1k single-char run);DCP_TOKEN_COUNT_MAX_EXACT_CHARS=0.npm test(123 passed),npm run typecheck,npm run format:checkandnpm run check:packagepass on this branch.Benches (added, all numbers above reproducible)
Relation to existing work
DCP_TOKEN_COUNT_MAX_EXACT_CHARS(0 = sample everything, a huge value = count everything exactly) andDCP_TOKEN_DEBUG=1(log each count with its path and size, for measurement).Known limitations
Disclosure
This PR was written with AI assistance (diagnosis, patch, tests and benches). I reviewed the diff and validated the behaviour myself:
npm test(123 passed),npm run typecheck,npm run format:checkpass on this branch.Background service is unresponsivewarning and no kill since the change (they only appear during compression before it).lib/plus tests and benches. No new dependencies, no behaviour change below the exact-count limit.