Token-counting method reconciliation¶
TokenPak counts tokens three different ways in different places, chosen for different tradeoffs (accuracy vs. speed vs. no-network-dependency vs. provider-agnosticism). This note names where each method is load-bearing so the tradeoffs are visible in one place, rather than only inside each call site's local comment. This is a reference note, not a consolidation — the three methods stay independent; nothing here changes behavior.
The three methods¶
| Method | What it measures | Typical error vs. real provider tokenization |
|---|---|---|
tiktoken |
Real BPE tokenization via the tiktoken library |
Exact for OpenAI-family models; a close approximation for other providers (no official Anthropic/Google tokenizer is vendored) |
| chars ÷ 4 | len(text) // 4 over a Python string |
Commonly cited as within a few percent for English prose; degrades for code, non-English text, and dense JSON/markup |
| bytes ÷ 4 | len(text.encode("utf-8")) // 4 (or a raw byte length already on hand) |
Similar to chars/4 for ASCII text; more correct than chars/4 for multibyte (non-ASCII) text, since it counts encoded bytes rather than code points |
None of these is "the" token count — each caller picked the one that fits its
constraint (speed, no tiktoken dependency, no network, byte-length already
in hand, etc.). A caller that surfaces a count to a user should disclose
which method produced it (several already do — see below).
Where tiktoken is load-bearing¶
tiktoken is used where an accurate count materially changes what gets sent
or kept, and the dependency cost is acceptable:
tokenpak/telemetry/tokens.py— the sharedcount_tokens()/truncate_to_tokens()utility used across the transcript parser, vault index, SQLite backend, and MCP tools. Exposestokenizer_name()so callers can disclose the active estimator.tokenpak/telemetry/budgeter.py—Budgeter.allocate()enforces a hard token budget across prompt-assembly buckets (state/recent/evidence/tools), trimming components that don't fit. An inaccurate count here would either overflow the real provider budget or trim more than necessary.tokenpak/compression/context_composer.py,span_extractor.py,pack.py— the compression pipeline's budget-fitting logic (which chunks/spans/blocks survive into the final packed prompt) is built ontiktokencounts, since these decisions directly shape the text sent onward to the provider.tokenpak/proxy/token_cache.py— an LRU-cached counter consumed by the request-forecast/spend-guard projection path.tokenpak/proxy/intelligence/server.py— backs the/v1/compressand/v1/budgetendpoints' trim-to-budget loop.tokenpak/compression/evidence_pack.py— reporting/stats only (total tokens across evidence items).
Every one of the above falls back to chars ÷ 4 when tiktoken is not
installed (it's an optional dependency) — see next section.
Where chars ÷ 4 is load-bearing¶
This is the dominant method by call-site count. Two flavors exist:
- The
tiktoken-unavailable fallback for every module listed above — same function, same call site,except ImportErrorbranch. - Standalone estimators, used where a full tokenizer dependency isn't
worth it for the decision being made: CLI command reports
(
tokenpak/cli/commands/compress_cmd.py,optimize_prompt.py,pak.py), SDK/integration adapters (tokenpak/sdk/*), vault retrieval/injection sizing (tokenpak/vault/*), most compression-engine estimators (tokenpak/compression/*), provider request-token approximations (tokenpak/proxy/providers/{anthropic,openai,google}.py), and Spend Guard's pre-flight cost projection (tokenpak/proxy/spend_guard/estimator.py, which defines the module's own_CHARS_PER_TOKEN = 4constant).
Two call sites are worth calling out because they gate something other than a report:
tokenpak/companion/hooks/pre_send.py— the pre-send hook's prompt-text branch deliberately avoidstiktokento stay well under its latency budget; the resulting estimate feeds the pre-send allow/block decision.tokenpak/proxy/app_endpoints.py's/tokens/estimatehandler labels its fallback responseestimator: "chars-per-4-heuristic"with an explicit user-facing note that the count is approximate — the disclosure pattern other estimators should follow when they surface a number externally.tokenpak/proxy/server.py's_estimate_tokens_from_body()andtokenpak/proxy/server_async.py's_estimate_tokens()— their primary parsed-JSON paths use chars ÷ 4. The synchronous estimator counts strings in recognizedmessages,input, orcontentsstructures; the asynchronous estimator countsmessagesorinputplussystem. They use raw bytes ÷ 4 only when JSON parsing or recognized-structure traversal fails.
There is no single shared constant for chars ÷ 4 across the codebase; each
module defines its own 4 (or a locally named _CHARS_PER_TOKEN). That is a
known inconsistency, not a defect this note is fixing — see "Out of scope"
below.
Where bytes ÷ 4 is load-bearing¶
Rarer, and used specifically where a byte length is already on hand (a raw request body, a file's on-disk size) and re-decoding to a string first would be wasted work, or where multibyte correctness matters more than raw speed:
tokenpak/telemetry/tokens.py's exportedestimate_tokens()— a public-API convenience function distinct fromcount_tokens(); counts UTF-8-encoded byte length rather than character count so multibyte text is handled correctly.tokenpak/companion/hooks/pre_send.py— the transcript-file branch usesos.path.getsize(...) // 4(the file isstat'd, never opened) for speed, alongside the chars ÷ 4 branch for inline prompt text in the same function.tokenpak/proxy/server.py's_estimate_tokens_from_body()andtokenpak/proxy/server_async.py's_estimate_tokens()— bytes ÷ 4 is their fallback when JSON parsing or recognized-structure traversal fails; their primary parsed-string paths are listed in the chars ÷ 4 section above.tokenpak/proxy/server.py's_stable_cache_hookclosure — proxy-side observe-only telemetry over the raw request body. The body forwarded to the provider is never altered by this estimate.tokenpak/proxy/capsule_integration.py— the outer fallback when the request body doesn't parse as JSON (the primary path there is a chars ÷ 4 walk of the parsed JSON, promoted to bytes ÷ 4 only on parse failure).tokenpak/proxy/cache_invalidation_alerts.py— estimates tokens lost to a cache-prefix invalidation, feeding a lost-savings dollar figure shown to the user.tokenpak/cli/cli_diagnose.py— runs the ratio in reverse (tokens × 4 → estimated bytes) purely to size a diagnostic cache-memory display; not a token count at all, included here for completeness since it uses the same 4:1 constant.
Out of scope for this note¶
- Consolidating the ~2 dozen independent chars ÷ 4 implementations behind one shared constant/helper. That's a real cleanup opportunity but a separate, larger change — this note only maps what exists today.
- Changing which method any call site uses. Every call site above keeps its existing method unchanged.