export interface TokenCountResult { tokens: number; characters: number; estimatedCost?: number; } /** * TokenCounter — delegates tokenization to the pluggable * TokenizerFactory from issue #124 while preserving the callable * surface (`count`, `countBatch`, `estimate`, `calculateSavings`, * `calculateCacheSavings`, `exceedsLimit`, `truncate`, * `getTokenCharRatio`, `free`) the rest of the codebase relies on. * * Synchronous counting and truncation use a lazily allocated local encoder. * Async counting uses a separate factory-owned tokenizer only when requested. */ export declare class TokenCounter { private localEncoder; private freed; private get encoder(); private get tokenizer(); readonly model: string; constructor(model?: string); /** * Longest slice handed to the tokenizer in one call. * * BPE COST IS SUPERLINEAR IN THE LENGTH OF A SINGLE RUN, and pathologically * so on highly repetitive text, because every merge pass has more to merge. * Measured on 100,000 characters: * * repeated single character 23,004 ms * a 26-character cycle 6,856 ms * minified json 28 ms * base64 28 ms * minified javascript 18 ms * * Ordinary content is fine; repetitive content is not. This is not a * hypothetical input either -- `count_tokens` is the most-called tool in the * product (2,738 of 4,735 recorded captures), and a padding run, an ASCII * separator or a repetitive blob would stall the server for twenty seconds. * It surfaced as a 38-second test suite, of which one case was 23 seconds. * * SLICING MAKES THE COST LINEAR, and slicing at a LINE START makes it very * nearly free of accuracy cost. Measured across all 342 files in this * repository over 8 KB, against an exact unsliced encode: * * cut at the byte limit aggregate +0.06437% * cut at the last space aggregate +0.00952%, 64.3% of files exact * cut after the last newline aggregate +0.00097%, 98.0% of files exact * * A cl100k token can carry its leading whitespace, which is why cutting at a * space still splits one and cutting after a newline does not. 19 tokens * differ across 1,963,504. Text shorter than one slice is encoded in a single * call and is bit-identical to before. */ private static readonly ENCODE_SLICE; /** * Memoised counts, because the same text is tokenised repeatedly. * * MEASURED, not assumed. `count()` had no cache at all: 200 calls on the same * 79 KB source cost 4,595 ms, a median of 22.8 ms EACH, every one of them * recomputing a result it had already produced. Tools routinely count the same * buffer more than once -- once to size the input and again to report savings * on the output -- and `calculateSavings()` counts its argument a third time. * * KEYED ON THE TEXT ITSELF, never on a fingerprint. A length-plus-samples key * would collide, and a collision here does not fail loudly: it silently * reports the wrong token count, which flows into every `tokensSaved` figure * the product publishes. V8 caches a string's hash in its header, so repeat * lookups of the same string are far cheaper than re-encoding it. * * BOUNDED TWO WAYS, because a server process is long-lived. An entry cap * alone still lets a few enormous buffers pin tens of megabytes, and a byte * cap alone still lets millions of tiny strings accumulate. Insertion order * gives cheap FIFO eviction via Map iteration. */ private static readonly CACHE_MAX_ENTRIES; private static readonly CACHE_MAX_BYTES; private readonly cache; private cachedBytes; /** * Serves a memoised count, computing and storing it on a miss. * * BOUNDED IN BYTES, NOT IN `length`. `text.length` counts UTF-16 code units, * so it is not a memory measure: `'\u{1F600}'.repeat(4 * 1024 * 1024)` has a * length of 8 Mi but occupies about 16 MiB, and a cap written against * `length` would admit twice what it claims. Each entry therefore carries the * byte size it was admitted with, so eviction subtracts exactly what * admission added rather than recomputing it from the key. * * Text past the cap is counted and NOT stored: admitting it would evict the * whole cache to hold one entry that may never be asked for again. */ private counted; /** * Encodes in bounded slices, so one pathological input cannot stall a call. */ private encodeBounded; /** * Count tokens in text (synchronous). * * Synchronous on tiktoken-backed tokenizers, which is all we expose * externally via Anthropic/OpenAI. Remote tokenizers (Google AI) are * reachable via `countAsync`. */ count(text: string): TokenCountResult; /** * Async token counting through the pluggable tokenizer — accurate for * both local tiktoken and remote Google AI paths. */ countAsync(text: string): Promise; countBatch(texts: string[]): TokenCountResult; estimate(text: string): number; calculateSavings(originalText: string, contextTokens?: number): { originalTokens: number; contextTokens: number; tokensSaved: number; percentSaved: number; }; calculateCacheSavings(originalText: string): { originalTokens: number; contextTokens: number; tokensSaved: number; percentSaved: number; }; exceedsLimit(text: string, limit: number): boolean; truncate(text: string, maxTokens: number): string; getTokenCharRatio(text: string): number; free(): void; } //# sourceMappingURL=token-counter.d.ts.map