tokenizer
js/text/tokenizer.ts
fino:text/tokenizer - text to token ids, in pure TypeScript.
Loads the tokenizers published models actually ship with — a Hugging Face
tokenizer.json carrying a BPE or WordPiece vocabulary, or a tiktoken ranks
file — and reproduces their encoding exactly, including the normalizer,
pre-tokenizer, post-processor, and decoder stages that decide where token
boundaries fall. There is no native dependency: tokenizers is Rust with no C
ABI and sentencepiece is C++ only, so binding either would mean shipping a
compiled artifact, while BPE and WordPiece over a vocabulary are string
processing that TypeScript does well.
Encoding is deterministic and stateless. A tokenizer serializes back to the
spec it was built from, so a DataLoader worker can rebuild an identical one
from a structured-cloneable value and produce byte-identical ids — which is
also how dataset-scale tokenization gets parallelized, since a single encode
is CPU-bound and short.
Encoding.offsets index the original input string, so a prediction can be
mapped back onto the caller's own text. Special tokens the post-processor added
carry a zero-width offset to mark that they came from no input.
Unigram vocabularies and tokenizer training are deliberately absent: Unigram
needs the sentencepiece character map that accompanies it, and loading one
without the other yields ids that look right and are not. Both throw a specific
error rather than degrading quietly. A llama.cpp GGUF vocabulary is reached the
same way any other source is — by building a spec object and handing it to
Tokenizer.fromJSON — which keeps this module independent of llama.cpp.
import { Tokenizer } from 'fino:text/tokenizer';
const tokenizer = await Tokenizer.fromFile('./tokenizer.json');
const encoded = tokenizer.encode('Hello, world!');
console.log(encoded.ids, encoded.tokens);
console.log(tokenizer.decode(encoded.ids));Interfaces
interface AddedTokenSpec {
An added-token entry as a tokenizer.json spells it.
Properties
id: number
content: string
single_word?: boolean
lstrip?: boolean
rstrip?: boolean
normalized?: boolean
special?: boolean
interface TokenizerSpec {
A Hugging Face tokenizer.json.
Only the fields that affect encoding are read; version, training metadata,
and unknown keys are preserved by toJSON() but otherwise ignored.
Properties
version?: string
truncation?: Record<string, unknown> | null
padding?: Record<string, unknown> | null
added_tokens?: AddedTokenSpec[]
normalizer?: NormalizerSpec | null
pre_tokenizer?: PreTokenizerSpec | null
post_processor?: PostProcessorSpec | null
decoder?: DecoderSpec | null
model: Record<string, unknown>
interface EncodeOptions {
Per-call encoding options.
Properties
addSpecialTokens?: boolean
Apply the post-processor's special tokens. Defaults to true.
truncation?: TruncationOptions | null
Truncation for this call, overriding the tokenizer's default.
padding?: PaddingOptions | null
Padding for this call, overriding the tokenizer's default.
interface DecodeOptions {
Options for decode.
Properties
skipSpecialTokens?: boolean
Drop tokens flagged special. Defaults to true.
interface TokenizerOptions {
Construction options beyond the spec.
Properties
truncation?: TruncationOptions | null
padding?: PaddingOptions | null
interface TiktokenOptions {
Building a tokenizer from a tiktoken ranks table.
Properties
vocab: Record<string, number>
Ranks as {token: rank} in the printable byte alphabet.
pattern?: string
Word-splitting pattern; defaults to the named encoding's.
specialTokens?: Record<string, number>
Literal tokens matched ahead of the model.
interface Encoding {
The result of encoding one input.
All arrays are parallel and the same length. offsets index the original
input string, so text.slice(...offsets[i]) is the source of token i; a
zero-width offset marks a token that came from no input text, such as a
special token the post-processor added.
Properties
ids: number[]
Vocabulary ids.
tokens: string[]
Vocabulary surface forms.
typeIds: number[]
Sequence membership: 0 for the first sequence, 1 for the pair.
attentionMask: number[]
1 for real tokens, 0 for padding.
specialTokensMask: number[]
1 for special tokens the caller did not write, 0 otherwise.
offsets: Array<[number, number]>
Original [start, end) character range per token.
sequenceIds: Array<number | null>
Which input sequence each token belongs to, or null for added tokens.
overflowing: Encoding[]
Sequences that did not fit under a truncation budget.
interface PaddingOptions {
Padding configuration.
Properties
length?: number
Pad every sequence to this length; omit to pad to the longest in a batch.
padToMultipleOf?: number
Round the padded length up to a multiple of this value.
direction?: Direction
padId?: number
padTypeId?: number
padToken?: string
interface TruncationOptions {
Truncation configuration.
Properties
maxLength: number
strategy?: TruncationStrategy
stride?: number
direction?: Direction
interface AddedToken {
A token matched literally rather than by the model.
Properties
id: number
Vocabulary id.
content: string
The literal text to match.
singleWord: boolean
Match only when not surrounded by word characters.
lstrip: boolean
Extend the match left over whitespace.
rstrip: boolean
Extend the match right over whitespace.
normalized: boolean
Match against normalized text rather than the raw input.
special: boolean
Report as a special token in specialTokensMask.
interface TiktokenEncoding {
The pieces a tokenizer needs beyond the ranks table.
Properties
pattern: string
Word-splitting pattern applied before merging.
specialTokens: Record<string, number>
Literal tokens matched ahead of the model.
Types
type EncodeInput = string | readonly [string, string]
A single input, or a sequence and its pair.
type Direction = 'right' | 'left'
How to truncate or pad relative to the sequence.
type TruncationStrategy = 'longest_first' | 'only_first' | 'only_second'
Truncation strategy for sequences longer than maxLength.
type AddedTokenOptions = Partial<Omit<AddedToken, 'id' | 'content'>>
Options accepted when adding a token, beyond its content.
Classes
class Tokenizer {
A loaded tokenizer.
Instances are immutable apart from the added vocabulary, which addTokens and
addSpecialTokens extend. Encoding holds no state between calls, so one
instance is safe to share across concurrent encodes.
Constructors
constructor(spec: TokenizerSpec, options: TokenizerOptions = {})
Static Methods
static fromJSON(source: string | TokenizerSpec, options?: TokenizerOptions): Tokenizer
Parse a tokenizer.json, given as text or as an already-parsed object.
static async fromFile(path: string, options?: TokenizerOptions): Promise<Tokenizer>
Read and parse a tokenizer.json from disk.
static fromTiktoken(options: TiktokenOptions, tokenizerOptions?: TokenizerOptions): Tokenizer
Build a tokenizer from a tiktoken ranks table.
Ranks alone do not describe a tokenizer, so pattern is required unless the
table came from Tokenizer.fromTiktokenFile with a known encoding name.
static async fromTiktokenFile(
path: string,
encoding: string | TiktokenEncoding,
options?: TokenizerOptions,
): Promise<Tokenizer>
Read a .tiktoken ranks file, taking the pattern and special tokens from a
published encoding name such as cl100k_base or o200k_base.
Methods
toJSON(): TokenizerSpec
The spec this tokenizer encodes with, including any tokens added since.
vocabSize(withAddedTokens = true): number
Size of the base vocabulary, optionally including added tokens.
getVocab(withAddedTokens = true): Record<string, number>
The vocabulary as a plain object, optionally including added tokens.
tokenToId(token: string): number | null
The id for a token, or null if it is not in the vocabulary.
idToToken(id: number): string | null
The token for an id, or null if the id is out of vocabulary.
addTokens(tokens: ReadonlyArray<string | (AddedTokenOptions & { content: string })>): number
Register tokens matched literally, ahead of the model.
A token already in the vocabulary keeps its id; a new one is assigned the next free id. Returns how many were newly added.
addSpecialTokens(
tokens: ReadonlyArray<string | (AddedTokenOptions & { content: string })>,
): number
Register tokens matched literally and reported as special.
encode(input: EncodeInput, options: EncodeOptions = {}): Encoding
Encode one input, or a sequence and its pair.
encodeBatch(inputs: readonly EncodeInput[], options: EncodeOptions = {}): Encoding[]
Encode several inputs.
With padding enabled and no explicit length, every result is padded to the
longest in this batch — which is what makes a batch stackable into one tensor.
decode(ids: readonly number[], options: DecodeOptions = {}): string
Decode ids back into text.
decodeBatch(batch: ReadonlyArray<readonly number[]>, options?: DecodeOptions): string[]
Decode several id sequences.
Getters
get truncation(): TruncationOptions | null
Truncation applied when a call does not override it.
get padding(): PaddingOptions | null
Padding applied when a call does not override it.
Constants
const TIKTOKEN_ENCODINGS: Readonly<Record<string, TiktokenEncoding>>
The published tiktoken encodings.
The ranks tables themselves are large downloads rather than baked-in data, so
these describe only the surrounding configuration; pair one with a ranks file
fetched through fino:model/hub or shipped alongside the model.
Properties
r50k_base
pattern
specialTokens
ENDOFTEXT
gpt2
pattern
specialTokens
ENDOFTEXT
p50k_base
pattern
specialTokens
ENDOFTEXT
p50k_edit
pattern
specialTokens
ENDOFTEXT
FIM_PREFIX
FIM_MIDDLE
FIM_SUFFIX
cl100k_base
pattern
specialTokens
ENDOFTEXT
FIM_PREFIX
FIM_MIDDLE
FIM_SUFFIX
ENDOFPROMPT
o200k_base
pattern
specialTokens
ENDOFTEXT
ENDOFPROMPT
Functions
function parseTiktokenRanks(text: string): Record<string, number>
Parse a .tiktoken ranks file into a byte-alphabet vocabulary.
Blank lines are skipped. A malformed line throws rather than being dropped, because a ranks table with a hole in it silently produces different ids.
function byteAlphabet(): readonly string[]
The 256 printable stand-ins, in byte order.