Skip to main content

logicaffeine_compile/model/tokenizer/
mod.rs

1//! The tokenizer a GGUF checkpoint carries, read from its `tokenizer.ggml.*` metadata and run the way llama.cpp runs
2//! it (`src/llama-vocab.cpp` at the pinned commit): text to token ids, ids to text pieces, and the special tokens
3//! that end generation.
4//!
5//! Two vocabularies are tokenized: byte-pair (`gpt2` and its `gemma4`, `hybriddna` and `whitespace` forms — Qwen,
6//! Llama 3, gpt-oss, DeepSeek) and SentencePiece (`llama` — Gemma 3, Llama 2, Mistral). WordPiece, Unigram, RWKV and
7//! PLaMo vocabularies are refused by name. `model_tokenizer_lock` holds every result equal to llama.cpp's own, on its
8//! bundled vocabulary corpora and on seeded random text.
9//!
10//! Text is bytes: a fragment that is not UTF-8 tokenizes as llama.cpp tokenizes it (an invalid byte reads as
11//! U+FFFD in byte-pair splitting, and SentencePiece falls back to byte tokens).
12
13pub mod cxx;
14pub mod pre;
15pub mod regex;
16pub mod split;
17pub mod unicode;
18mod unicode_tables;
19
20use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap};
21use std::fmt;
22
23use super::gguf::{Gguf, MetaValue};
24use pre::{PreSettings, PreType};
25use regex::RegexError;
26use split::Splitter;
27use unicode::InvalidCodepoint;
28
29/// A token attribute bit, as llama.cpp's `llama_token_attr`.
30pub mod attr {
31    pub const UNDEFINED: u32 = 0;
32    pub const UNKNOWN: u32 = 1 << 0;
33    pub const UNUSED: u32 = 1 << 1;
34    pub const NORMAL: u32 = 1 << 2;
35    pub const CONTROL: u32 = 1 << 3;
36    pub const USER_DEFINED: u32 = 1 << 4;
37    pub const BYTE: u32 = 1 << 5;
38    pub const NORMALIZED: u32 = 1 << 6;
39    pub const LSTRIP: u32 = 1 << 7;
40    pub const RSTRIP: u32 = 1 << 8;
41    pub const SINGLE_WORD: u32 = 1 << 9;
42}
43
44/// The SentencePiece escaped space, U+2581.
45const ESCAPED_SPACE: &[u8] = "\u{2581}".as_bytes();
46/// The suffix a `hybriddna` vocabulary marks its k-mer tokens with.
47const DNA_KMER_MARKER: &[u8] = b"\xee\x80\x80";
48
49/// The tokenizer family a vocabulary runs.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum VocabKind {
52    /// SentencePiece byte-fallback BPE (`tokenizer.ggml.model = "llama"`).
53    Spm,
54    /// Byte-pair merges over pre-tokenized words.
55    Bpe(PreType),
56}
57
58/// How a byte-pair vocabulary's fragments are cut before the generic merge loop.
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60enum BpeSession {
61    Plain,
62    HybridDna,
63    Whitespace,
64}
65
66/// A vocabulary that cannot be read, by name.
67#[derive(Clone, Debug, PartialEq)]
68pub enum VocabError {
69    Missing(&'static str),
70    WrongType(&'static str),
71    UnknownModel(String),
72    UnsupportedModel(String),
73    UnknownPre(String),
74    TooFewScores { scores: usize, tokens: usize },
75    TooFewTypes { types: usize, tokens: usize },
76    DuplicateToken(Vec<u8>),
77    Regex(RegexError),
78    NoNewline,
79}
80
81impl fmt::Display for VocabError {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            VocabError::Missing(k) => write!(f, "the vocabulary has no `{k}`"),
85            VocabError::WrongType(k) => write!(f, "`{k}` has the wrong GGUF type"),
86            VocabError::UnknownModel(m) => write!(f, "unknown tokenizer model `{m}`"),
87            VocabError::UnsupportedModel(m) => {
88                write!(f, "the `{m}` tokenizer is not run here (byte-pair and SentencePiece vocabularies are)")
89            }
90            VocabError::UnknownPre(p) => write!(f, "unknown pre-tokenizer `{p}`"),
91            VocabError::TooFewScores { scores, tokens } => write!(f, "{scores} scores for {tokens} tokens"),
92            VocabError::TooFewTypes { types, tokens } => write!(f, "{types} token types for {tokens} tokens"),
93            VocabError::DuplicateToken(t) => write!(f, "the token {:?} appears twice", String::from_utf8_lossy(t)),
94            VocabError::Regex(e) => write!(f, "{e}"),
95            VocabError::NoNewline => write!(f, "the SentencePiece vocabulary has no newline token"),
96        }
97    }
98}
99
100impl std::error::Error for VocabError {}
101
102/// A text llama.cpp would abort on, by name.
103#[derive(Clone, Debug, PartialEq, Eq)]
104pub enum TokenizeError {
105    /// A codepoint above U+10FFFF, which no UTF-8 sequence encodes.
106    InvalidCodepoint(u32),
107    /// A byte with no byte token in a byte-fallback vocabulary.
108    MissingByteToken(u8),
109    /// `add_special` asked for a beginning- or end-of-sequence token the vocabulary does not have.
110    MissingSpecial(&'static str),
111}
112
113impl fmt::Display for TokenizeError {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        match self {
116            TokenizeError::InvalidCodepoint(c) => write!(f, "the text decodes to U+{c:X}, past U+10FFFF"),
117            TokenizeError::MissingByteToken(b) => write!(f, "the vocabulary has no token for byte 0x{b:02X}"),
118            TokenizeError::MissingSpecial(s) => write!(f, "the vocabulary has no {s} token"),
119        }
120    }
121}
122
123impl std::error::Error for TokenizeError {}
124
125impl From<InvalidCodepoint> for TokenizeError {
126    fn from(e: InvalidCodepoint) -> Self {
127        TokenizeError::InvalidCodepoint(e.0)
128    }
129}
130
131/// One vocabulary entry.
132#[derive(Clone, Debug, PartialEq)]
133pub struct TokenData {
134    pub text: Vec<u8>,
135    pub score: f32,
136    pub attr: u32,
137}
138
139/// The special token ids, each `None` when the vocabulary has none.
140#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
141pub struct Specials {
142    pub bos: Option<u32>,
143    pub eos: Option<u32>,
144    pub eot: Option<u32>,
145    pub eom: Option<u32>,
146    pub unk: Option<u32>,
147    pub sep: Option<u32>,
148    pub pad: Option<u32>,
149    pub mask: Option<u32>,
150    pub fim_pre: Option<u32>,
151    pub fim_suf: Option<u32>,
152    pub fim_mid: Option<u32>,
153    pub fim_pad: Option<u32>,
154    pub fim_rep: Option<u32>,
155    pub fim_sep: Option<u32>,
156}
157
158/// A loaded vocabulary and its tokenizer.
159#[derive(Clone, Debug)]
160pub struct Vocab {
161    pub kind: VocabKind,
162    pub model: String,
163    pub specials: Specials,
164    pub linefeed: Option<u32>,
165    pub add_space_prefix: bool,
166    pub add_bos: bool,
167    pub add_eos: bool,
168    pub ignore_merges: bool,
169    pub clean_spaces: bool,
170    pub escape_whitespaces: bool,
171    pub treat_whitespace_as_suffix: bool,
172    pub lowercase: bool,
173    tokens: Vec<TokenData>,
174    token_to_id: HashMap<Vec<u8>, u32>,
175    bpe_ranks: HashMap<Vec<u8>, u32>,
176    eog: BTreeSet<u32>,
177    special_cache: Vec<u32>,
178    piece_cache: Vec<Vec<u8>>,
179    splitter: Option<Splitter>,
180    session: BpeSession,
181}
182
183/// The key a merge `(first, second)` is ranked under.
184fn pair_key(first: &[u8], second: &[u8]) -> Vec<u8> {
185    let mut k = Vec::with_capacity(4 + first.len() + second.len());
186    k.extend_from_slice(&(first.len() as u32).to_le_bytes());
187    k.extend_from_slice(first);
188    k.extend_from_slice(second);
189    k
190}
191
192fn meta_str<'a>(meta: &'a BTreeMap<String, MetaValue>, key: &str) -> Option<&'a str> {
193    meta.get(key).and_then(MetaValue::as_str)
194}
195
196/// `std::string::find(needle, from)` over bytes.
197fn find(hay: &[u8], needle: &[u8], from: usize) -> Option<usize> {
198    if needle.is_empty() {
199        return (from <= hay.len()).then_some(from);
200    }
201    if from > hay.len() || hay.len() - from < needle.len() {
202        return None;
203    }
204    hay[from..].windows(needle.len()).position(|w| w == needle).map(|p| p + from)
205}
206
207/// `replace_all(s, search, replace)`.
208fn replace_all(s: &[u8], search: &[u8], replace: &[u8]) -> Vec<u8> {
209    let mut out = Vec::with_capacity(s.len());
210    let mut last = 0;
211    while let Some(p) = find(s, search, last) {
212        out.extend_from_slice(&s[last..p]);
213        out.extend_from_slice(replace);
214        last = p + search.len();
215    }
216    out.extend_from_slice(&s[last..]);
217    out
218}
219
220fn c_isspace(b: u8) -> bool {
221    matches!(b, b' ' | b'\t' | b'\n' | 0x0B | 0x0C | b'\r')
222}
223
224/// A fragment of the text being tokenized: raw bytes still to split, or a special token already matched.
225#[derive(Clone, Copy, Debug)]
226enum Fragment {
227    Token(u32),
228    Raw { offset: usize, len: usize },
229}
230
231impl Vocab {
232    /// Read the vocabulary of `g`.
233    pub fn from_gguf(g: &Gguf) -> Result<Vocab, VocabError> {
234        Vocab::from_meta(&g.meta)
235    }
236
237    /// Read the vocabulary a checkpoint's metadata describes: its `tokenizer.*` keys, and the `general.*` keys whose
238    /// names llama.cpp keys token attributes by.
239    pub fn from_meta(meta: &BTreeMap<String, MetaValue>) -> Result<Vocab, VocabError> {
240        let model = meta_str(meta, "tokenizer.ggml.model")
241            .ok_or(VocabError::Missing("tokenizer.ggml.model"))?
242            .to_string();
243        let mut pre_name = meta_str(meta, "tokenizer.ggml.pre").map(str::to_string);
244        let mut specials = Specials::default();
245        let mut kind_spm = false;
246        let mut bpe_ranks = HashMap::new();
247        let mut session = BpeSession::Plain;
248        let read_merges = |ranks: &mut HashMap<Vec<u8>, u32>| -> Result<(), VocabError> {
249            let merges = meta
250                .get("tokenizer.ggml.merges")
251                .ok_or(VocabError::Missing("tokenizer.ggml.merges"))?
252                .as_array()
253                .ok_or(VocabError::WrongType("tokenizer.ggml.merges"))?;
254            for (i, m) in merges.iter().enumerate() {
255                let word = m.as_bytes().ok_or(VocabError::WrongType("tokenizer.ggml.merges"))?;
256                let (first, second): (&[u8], &[u8]) = match find(word, b" ", 1) {
257                    Some(p) => (&word[..p], &word[p + 1..]),
258                    None => (b"", b""),
259                };
260                ranks.entry(pair_key(first, second)).or_insert(i as u32);
261            }
262            Ok(())
263        };
264        match model.as_str() {
265            "llama" => {
266                kind_spm = true;
267                specials.bos = Some(1);
268                specials.eos = Some(2);
269                specials.unk = Some(0);
270            }
271            "gpt2" | "hybriddna" | "whitespace" => {
272                if pre_name.as_deref() != Some("kimi-k2") || meta.contains_key("tokenizer.ggml.merges") {
273                    read_merges(&mut bpe_ranks)?;
274                }
275                specials.bos = Some(11);
276                specials.eos = Some(11);
277                session = match model.as_str() {
278                    "hybriddna" => BpeSession::HybridDna,
279                    "whitespace" => BpeSession::Whitespace,
280                    _ => BpeSession::Plain,
281                };
282            }
283            "gemma4" => {
284                read_merges(&mut bpe_ranks)?;
285                pre_name = Some("gemma4".to_string());
286            }
287            "bert" | "t5" | "rwkv" | "plamo2" | "plamo3" | "test" | "no_vocab" | "none" => {
288                return Err(VocabError::UnsupportedModel(model));
289            }
290            other => return Err(VocabError::UnknownModel(other.to_string())),
291        }
292
293        let (kind, mut add_space_prefix, add_bos, add_eos, ignore_merges, clean_spaces, escape_whitespaces, lowercase_cleared) =
294            if kind_spm {
295                (VocabKind::Spm, true, true, false, false, false, true, false)
296            } else {
297                let (pt, set): (PreType, PreSettings) = match pre_name.as_deref() {
298                    None => (PreType::Default, PreSettings::default()),
299                    Some(p) => pre::pre_type(p).ok_or_else(|| VocabError::UnknownPre(p.to_string()))?,
300                };
301                if set.no_bos_token {
302                    specials.bos = None;
303                }
304                (
305                    VocabKind::Bpe(pt),
306                    false,
307                    set.add_bos.unwrap_or(false),
308                    false,
309                    set.ignore_merges.unwrap_or(false),
310                    set.clean_spaces.unwrap_or(true),
311                    set.escape_whitespaces.unwrap_or(false),
312                    set.no_lowercase,
313                )
314            };
315        let get_bool = |k: &'static str| -> Result<Option<bool>, VocabError> {
316            match meta.get(k) {
317                None => Ok(None),
318                Some(v) => v.as_bool().map(Some).ok_or(VocabError::WrongType(k)),
319            }
320        };
321        if let Some(v) = get_bool("tokenizer.ggml.add_space_prefix")? {
322            add_space_prefix = v;
323        }
324
325        let list = meta
326            .get("tokenizer.ggml.tokens")
327            .ok_or(VocabError::Missing("tokenizer.ggml.tokens"))?
328            .as_array()
329            .ok_or(VocabError::WrongType("tokenizer.ggml.tokens"))?;
330        let n = list.len();
331        let scores: Option<Vec<f32>> = match meta.get("tokenizer.ggml.scores") {
332            None => None,
333            Some(v) => {
334                let a = v.as_array().ok_or(VocabError::WrongType("tokenizer.ggml.scores"))?;
335                if a.len() < n {
336                    return Err(VocabError::TooFewScores { scores: a.len(), tokens: n });
337                }
338                Some(
339                    a.iter()
340                        .map(|x| match *x {
341                            MetaValue::F32(f) => Ok(f),
342                            MetaValue::I32(i) => Ok(i as f32),
343                            _ => Err(VocabError::WrongType("tokenizer.ggml.scores")),
344                        })
345                        .collect::<Result<_, _>>()?,
346                )
347            }
348        };
349        let types: Option<Vec<i32>> = match meta.get("tokenizer.ggml.token_type") {
350            None => None,
351            Some(v) => {
352                let a = v.as_array().ok_or(VocabError::WrongType("tokenizer.ggml.token_type"))?;
353                if a.len() < n {
354                    return Err(VocabError::TooFewTypes { types: a.len(), tokens: n });
355                }
356                Some(
357                    a.iter()
358                        .map(|x| match *x {
359                            MetaValue::I32(i) => Ok(i),
360                            _ => Err(VocabError::WrongType("tokenizer.ggml.token_type")),
361                        })
362                        .collect::<Result<_, _>>()?,
363                )
364            }
365        };
366        let mut tokens = Vec::with_capacity(n);
367        let mut token_to_id = HashMap::with_capacity(n);
368        for (i, t) in list.iter().enumerate() {
369            let mut text = t.as_bytes().ok_or(VocabError::WrongType("tokenizer.ggml.tokens"))?.to_vec();
370            if text.is_empty() {
371                text = format!("[EMPTY_{i}]").into_bytes();
372            }
373            if token_to_id.insert(text.clone(), i as u32).is_some() {
374                return Err(VocabError::DuplicateToken(text));
375            }
376            let attr = match types.as_ref().map(|t| t[i]) {
377                None => attr::NORMAL,
378                Some(0) => attr::UNDEFINED,
379                Some(1) => attr::NORMAL,
380                Some(2) => attr::UNKNOWN,
381                Some(3) => attr::CONTROL,
382                Some(4) => attr::USER_DEFINED,
383                Some(5) => attr::UNUSED,
384                Some(6) => attr::BYTE,
385                Some(_) => attr::UNDEFINED,
386            };
387            tokens.push(TokenData { text, score: scores.as_ref().map_or(0.0, |s| s[i]), attr });
388        }
389        let map_hashes: Vec<u64> = tokens.iter().map(|t: &TokenData| cxx::hash_string(&t.text)).collect();
390        if model == "hybriddna" {
391            if let Some(&oov) = token_to_id.get(b"<oov>".as_slice()) {
392                for t in tokens.iter_mut().skip(oov as usize + 1) {
393                    if t.text.len() > DNA_KMER_MARKER.len() && t.text.ends_with(DNA_KMER_MARKER) {
394                        let keep = t.text.len() - DNA_KMER_MARKER.len();
395                        t.text.truncate(keep);
396                    }
397                }
398            }
399        }
400
401        let splitter = match kind {
402            VocabKind::Bpe(pt) => Some(Splitter::new(pt.regexes(), pt.byte_encode()).map_err(VocabError::Regex)?),
403            VocabKind::Spm => None,
404        };
405        let mut v = Vocab {
406            kind,
407            model,
408            specials,
409            linefeed: None,
410            add_space_prefix,
411            add_bos,
412            add_eos,
413            ignore_merges,
414            clean_spaces,
415            escape_whitespaces,
416            treat_whitespace_as_suffix: false,
417            lowercase: true,
418            tokens,
419            token_to_id,
420            bpe_ranks,
421            eog: BTreeSet::new(),
422            special_cache: Vec::new(),
423            piece_cache: Vec::new(),
424            splitter,
425            session,
426        };
427
428        v.linefeed = match kind {
429            VocabKind::Spm => Some(v.byte_to_token(b'\n').map_err(|_| VocabError::NoNewline)?),
430            VocabKind::Bpe(_) => v.tokenize(b"\n", false, false).ok().and_then(|ids| ids.first().copied()),
431        };
432
433        let n_tokens = v.tokens.len();
434        let in_range = |id: Option<u32>| id.filter(|&i| (i as usize) < n_tokens);
435        for slot in [&mut v.specials.bos, &mut v.specials.eos, &mut v.specials.unk] {
436            *slot = in_range(*slot);
437        }
438        let keys: [(&'static str, fn(&mut Specials) -> &mut Option<u32>); 17] = [
439            ("tokenizer.ggml.bos_token_id", |s| &mut s.bos),
440            ("tokenizer.ggml.eos_token_id", |s| &mut s.eos),
441            ("tokenizer.ggml.eot_token_id", |s| &mut s.eot),
442            ("tokenizer.ggml.eom_token_id", |s| &mut s.eom),
443            ("tokenizer.ggml.unknown_token_id", |s| &mut s.unk),
444            ("tokenizer.ggml.seperator_token_id", |s| &mut s.sep),
445            ("tokenizer.ggml.padding_token_id", |s| &mut s.pad),
446            ("tokenizer.ggml.mask_token_id", |s| &mut s.mask),
447            ("tokenizer.ggml.fim_pre_token_id", |s| &mut s.fim_pre),
448            ("tokenizer.ggml.fim_suf_token_id", |s| &mut s.fim_suf),
449            ("tokenizer.ggml.fim_mid_token_id", |s| &mut s.fim_mid),
450            ("tokenizer.ggml.fim_pad_token_id", |s| &mut s.fim_pad),
451            ("tokenizer.ggml.fim_rep_token_id", |s| &mut s.fim_rep),
452            ("tokenizer.ggml.fim_sep_token_id", |s| &mut s.fim_sep),
453            ("tokenizer.ggml.prefix_token_id", |s| &mut s.fim_pre),
454            ("tokenizer.ggml.suffix_token_id", |s| &mut s.fim_suf),
455            ("tokenizer.ggml.middle_token_id", |s| &mut s.fim_mid),
456        ];
457        for (key, slot) in keys {
458            let Some(val) = meta.get(key) else { continue };
459            let MetaValue::U32(id) = *val else { return Err(VocabError::WrongType(key)) };
460            if (id as usize) < n_tokens {
461                *slot(&mut v.specials) = Some(id);
462            }
463        }
464        if let Some(b) = get_bool("tokenizer.ggml.add_bos_token")? {
465            v.add_bos = b;
466        }
467        if let Some(b) = get_bool("tokenizer.ggml.add_eos_token")? {
468            v.add_eos = b;
469        }
470        if v.kind == VocabKind::Bpe(PreType::Gemma4) && !v.add_bos {
471            v.add_bos = true;
472        }
473        v.lowercase = !lowercase_cleared;
474        if let Some(b) = get_bool("tokenizer.ggml.normalizer.lowercase")? {
475            v.lowercase = b;
476        }
477
478        v.detect_specials(&map_hashes);
479        v.build_special_cache();
480        v.piece_cache = (0..n_tokens as u32).map(|id| v.piece_uncached(id)).collect();
481        v.apply_name_attrs(meta);
482        Ok(v)
483    }
484
485    /// The text-matched special tokens, the end-of-generation set, and the attribute fixes llama.cpp applies after
486    /// reading the vocabulary. A slot takes the first matching token in the order llama.cpp's `token_to_id` map
487    /// iterates ([`cxx::unordered_map_order`] over `map_hashes`, the hashes of the texts as the map was keyed).
488    fn detect_specials(&mut self, map_hashes: &[u64]) {
489        type Slot = fn(&mut Specials) -> &mut Option<u32>;
490        let rules: [(Slot, &[&str]); 8] = [
491            (
492                |s| &mut s.eot,
493                &[
494                    "<|eot_id|>",
495                    "<|im_end|>",
496                    "<|end|>",
497                    "<end_of_turn>",
498                    "<|endoftext|>",
499                    "<|end_of_text|>",
500                    "<EOT>",
501                    "_<EOT>",
502                    "[EOT]",
503                    "<|end▁of▁sentence|>",
504                    "<end_of_utterance>",
505                ],
506            ),
507            (|s| &mut s.eom, &["<|eom_id|>"]),
508            (
509                |s| &mut s.fim_pre,
510                &[
511                    "<|fim_prefix|>",
512                    "<fim-prefix>",
513                    "<fim_prefix>",
514                    "<|fim▁begin|>",
515                    "<PRE>",
516                    "▁<PRE>",
517                    "<|code_prefix|>",
518                    "<|prefix|>",
519                    "<|plamo:fim_prefix|>",
520                ],
521            ),
522            (
523                |s| &mut s.fim_suf,
524                &[
525                    "<|fim_suffix|>",
526                    "<fim-suffix>",
527                    "<fim_suffix>",
528                    "<|fim▁hole|>",
529                    "<SUF>",
530                    "▁<SUF>",
531                    "<|code_suffix|>",
532                    "<|suffix|>",
533                    "<|plamo:fim_suffix|>",
534                ],
535            ),
536            (
537                |s| &mut s.fim_mid,
538                &[
539                    "<|fim_middle|>",
540                    "<fim-middle>",
541                    "<fim_middle>",
542                    "<|fim▁end|>",
543                    "<MID>",
544                    "▁<MID>",
545                    "<|code_middle|>",
546                    "<|middle|>",
547                    "<|plamo:fim_middle|>",
548                ],
549            ),
550            (|s| &mut s.fim_pad, &["<|fim_pad|>", "<fim-pad>", "<fim_pad>", "<PAD>", "[PAD]"]),
551            (|s| &mut s.fim_rep, &["<|fim_repo|>", "<|repo_name|>", "<fim-repo>", "<REPO>", "<reponame>"]),
552            (|s| &mut s.fim_sep, &["<|file_sep|>", "<|plamo:file_separator|>"]),
553        ];
554        let mut rank = vec![0u32; map_hashes.len()];
555        for (pos, id) in cxx::unordered_map_order(map_hashes).into_iter().enumerate() {
556            rank[id as usize] = pos as u32;
557        }
558        for (slot, texts) in rules {
559            if slot(&mut self.specials).is_some() {
560                continue;
561            }
562            let found = texts
563                .iter()
564                .filter_map(|t| self.token_to_id.get(t.as_bytes()).copied())
565                .min_by_key(|&id| rank[id as usize]);
566            if let Some(id) = found {
567                *slot(&mut self.specials) = Some(id);
568                self.tokens[id as usize].attr |= attr::CONTROL;
569            }
570        }
571        for t in &mut self.tokens {
572            if t.attr & attr::CONTROL != 0 && t.attr & attr::UNUSED == 0 && find(&t.text, b"unused", 0).is_some() {
573                t.attr |= attr::UNUSED;
574            }
575        }
576        let mut eog = BTreeSet::new();
577        for id in [self.specials.fim_pad, self.specials.fim_rep, self.specials.fim_sep].into_iter().flatten() {
578            eog.insert(id);
579        }
580        const EOG_TEXTS: [&str; 23] = [
581            "<|eot_id|>",
582            "<|im_end|>",
583            "<|end|>",
584            "<|return|>",
585            "<|call|>",
586            "<|flush|>",
587            "<|calls|>",
588            "<end_of_turn>",
589            "<|endoftext|>",
590            "</s>",
591            "<|eom_id|>",
592            "<EOT>",
593            "_<EOT>",
594            "[EOT]",
595            "[EOS]",
596            "<|end_of_text|>",
597            "<end_of_utterance>",
598            "<eos>",
599            "<turn|>",
600            "<|tool_response>",
601            "<|end▁of▁sentence|>",
602            "[e~[",
603            "<|ifm|im_end|>",
604        ];
605        for text in EOG_TEXTS {
606            if let Some(&id) = self.token_to_id.get(text.as_bytes()) {
607                eog.insert(id);
608                self.tokens[id as usize].attr |= attr::CONTROL;
609            }
610        }
611        for text in ["<|channel|>", "<|message|>", "<|start|>", "<|constrain|>"] {
612            if let Some(&id) = self.token_to_id.get(text.as_bytes()) {
613                self.tokens[id as usize].attr = attr::USER_DEFINED;
614            }
615        }
616        for id in [self.specials.eos, self.specials.eot, self.specials.eom].into_iter().flatten() {
617            eog.insert(id);
618        }
619        let find_eog = |tokens: &[TokenData], eog: &BTreeSet<u32>, text: &[u8]| {
620            eog.iter().copied().find(|&id| tokens.get(id as usize).is_some_and(|t| t.text == text))
621        };
622        if let Some(end_id) = find_eog(&self.tokens, &eog, b"<|end|>") {
623            let has = |t: &[u8]| find_eog(&self.tokens, &eog, t).is_some();
624            let has_call = has(b"<|call|>") || has(b"<|calls|>");
625            if (has(b"<|return|>") && has_call) || (has_call && has(b"<|flush|>")) {
626                eog.remove(&end_id);
627                self.tokens[end_id as usize].attr = attr::USER_DEFINED;
628            }
629        }
630        let has_marker = find_eog(&self.tokens, &eog, b"<|tool_response>").is_some()
631            || find_eog(&self.tokens, &eog, b"<|plamo:eos|>").is_some();
632        if let (true, Some(s_id)) = (has_marker, find_eog(&self.tokens, &eog, b"</s>")) {
633            eog.remove(&s_id);
634            self.tokens[s_id as usize].attr = attr::NORMAL;
635        }
636        self.eog = eog;
637    }
638
639    fn build_special_cache(&mut self) {
640        let mut cache: Vec<u32> = (0..self.tokens.len() as u32)
641            .filter(|&id| self.tokens[id as usize].attr & (attr::CONTROL | attr::USER_DEFINED | attr::UNKNOWN) != 0)
642            .collect();
643        let tokens = &self.tokens;
644        cxx::sort(&mut cache, &|&a: &u32, &b: &u32| tokens[a as usize].text.len() > tokens[b as usize].text.len());
645        self.special_cache = cache;
646    }
647
648    /// The per-token attributes llama.cpp sets by model, pre-tokenizer or architecture name.
649    fn apply_name_attrs(&mut self, meta: &BTreeMap<String, MetaValue>) {
650        let name = meta_str(meta, "general.name").unwrap_or("").to_ascii_lowercase();
651        let pre = meta_str(meta, "tokenizer.ggml.pre").unwrap_or("");
652        let arch = meta_str(meta, "general.architecture").unwrap_or("");
653        let any = |s: &str, subs: &[&str]| subs.iter().any(|x| s.contains(x));
654        let set = |v: &mut Vocab, text: &[u8], a: u32, on: bool| {
655            if let Some(&id) = v.token_to_id.get(text) {
656                let t = &mut v.tokens[id as usize];
657                t.attr = if on { t.attr | a } else { t.attr & !a };
658            }
659        };
660        if any(pre, &["jina-v2-de", "jina-v2-es", "jina-v2-code", "mmbert"]) || any(arch, &["nomic-bert-moe", "jina-bert-v3"]) {
661            set(self, b"<mask>", attr::LSTRIP, true);
662        } else if any(&name, &["phi-3", "phi3"]) {
663            for id in self.special_cache.clone() {
664                self.tokens[id as usize].attr |= attr::RSTRIP;
665            }
666            set(self, b"</s>", attr::RSTRIP, true);
667            for t in [b"<unk>".as_slice(), b"<s>", b"<|endoftext|>"] {
668                set(self, t, attr::RSTRIP, false);
669            }
670        } else if any(&name, &["modern-bert"]) {
671            set(self, b"[MASK]", attr::LSTRIP, true);
672        }
673    }
674
675    pub fn n_tokens(&self) -> usize {
676        self.tokens.len()
677    }
678
679    pub fn token(&self, id: u32) -> &TokenData {
680        &self.tokens[id as usize]
681    }
682
683    pub fn text_to_token(&self, text: &[u8]) -> Option<u32> {
684        self.token_to_id.get(text).copied()
685    }
686
687    /// Whether `id` ends generation.
688    pub fn is_eog(&self, id: u32) -> bool {
689        self.eog.contains(&id)
690    }
691
692    /// The end-of-generation tokens, ascending.
693    pub fn eog(&self) -> impl Iterator<Item = u32> + '_ {
694        self.eog.iter().copied()
695    }
696
697    /// The special tokens in the order fragments are split on them.
698    pub fn special_order(&self) -> &[u32] {
699        &self.special_cache
700    }
701
702    fn bpe_rank(&self, first: &[u8], second: &[u8]) -> Option<u32> {
703        self.bpe_ranks.get(&pair_key(first, second)).copied()
704    }
705
706    /// `byte_to_token`: the token standing for one byte.
707    pub fn byte_to_token(&self, b: u8) -> Result<u32, TokenizeError> {
708        const HEX: &[u8; 16] = b"0123456789ABCDEF";
709        let found = match self.kind {
710            VocabKind::Spm => {
711                let name = [b'<', b'0', b'x', HEX[(b >> 4) as usize], HEX[(b & 15) as usize], b'>'];
712                self.text_to_token(&name).or_else(|| {
713                    let raw: &[u8] = if b == 0 { b"" } else { std::slice::from_ref(&b) };
714                    self.text_to_token(raw)
715                })
716            }
717            VocabKind::Bpe(_) => {
718                let mut s = Vec::new();
719                unicode::byte_to_utf8(b, &mut s);
720                self.text_to_token(&s)
721            }
722        };
723        found.ok_or(TokenizeError::MissingByteToken(b))
724    }
725
726    /// `token_to_byte`: the byte a byte token stands for (`<0xXX>`, the hex read as `strtol` reads it).
727    fn token_to_byte(&self, id: u32) -> u8 {
728        let t = &self.tokens[id as usize].text;
729        let hex = t.get(3..t.len().min(5)).unwrap_or(&[]);
730        let mut v: u32 = 0;
731        for &c in hex {
732            match (c as char).to_digit(16) {
733                Some(d) => v = v * 16 + d,
734                None => break,
735            }
736        }
737        v as u8
738    }
739
740    /// `tokenizer_st_partition`: split the raw fragments of `text` at every special token, longest first.
741    fn partition(&self, text: &[u8], parse_special: bool) -> Vec<Fragment> {
742        let mut frags = vec![Fragment::Raw { offset: 0, len: text.len() }];
743        for &id in &self.special_cache {
744            let data = &self.tokens[id as usize];
745            if !parse_special && data.attr & (attr::CONTROL | attr::UNKNOWN) != 0 {
746                continue;
747            }
748            let needle = &data.text;
749            let mut next = Vec::with_capacity(frags.len());
750            for f in frags {
751                let Fragment::Raw { offset, len } = f else {
752                    next.push(f);
753                    continue;
754                };
755                let (mut base, mut blen) = (offset, len);
756                loop {
757                    let Some(m) = find(&text[..base + blen], needle, base) else {
758                        next.push(Fragment::Raw { offset: base, len: blen });
759                        break;
760                    };
761                    if m > base {
762                        let mut left = m - base;
763                        if data.attr & attr::LSTRIP != 0 {
764                            while left > 0 && c_isspace(text[base + left - 1]) {
765                                left -= 1;
766                            }
767                        }
768                        if left > 0 {
769                            next.push(Fragment::Raw { offset: base, len: left });
770                        }
771                    }
772                    next.push(Fragment::Token(id));
773                    if m + needle.len() < base + blen {
774                        let mut r_off = m + needle.len();
775                        let mut r_len = blen - ((m - base) + needle.len());
776                        if data.attr & attr::RSTRIP != 0 {
777                            while r_len > 0 && c_isspace(text[r_off]) {
778                                r_off += 1;
779                                r_len -= 1;
780                            }
781                        }
782                        if r_len == 0 {
783                            break;
784                        }
785                        base = r_off;
786                        blen = r_len;
787                    } else {
788                        break;
789                    }
790                }
791            }
792            frags = next;
793        }
794        frags
795    }
796
797    /// `llama_tokenize`: the token ids of `text`. `add_special` adds the vocabulary's beginning- and end-of-sequence
798    /// tokens as it asks; `parse_special` lets control tokens written in the text match as themselves.
799    pub fn tokenize(&self, text: &[u8], add_special: bool, parse_special: bool) -> Result<Vec<u32>, TokenizeError> {
800        let mut out = Vec::new();
801        let frags = if text.is_empty() { Vec::new() } else { self.partition(text, parse_special) };
802        match self.kind {
803            VocabKind::Spm => {
804                let mut prev_special = true;
805                if add_special && self.add_bos {
806                    out.push(self.specials.bos.ok_or(TokenizeError::MissingSpecial("beginning-of-sequence"))?);
807                    prev_special = true;
808                }
809                for f in &frags {
810                    match *f {
811                        Fragment::Raw { offset, len } => {
812                            let mut t = Vec::with_capacity(len + 1);
813                            if self.add_space_prefix && prev_special {
814                                t.push(b' ');
815                            }
816                            t.extend_from_slice(&text[offset..offset + len]);
817                            let t = replace_all(&t, b" ", ESCAPED_SPACE);
818                            self.spm(&t, &mut out)?;
819                            prev_special = false;
820                        }
821                        Fragment::Token(id) => {
822                            out.push(id);
823                            prev_special = true;
824                        }
825                    }
826                }
827                if add_special && self.add_eos {
828                    out.push(self.specials.eos.ok_or(TokenizeError::MissingSpecial("end-of-sequence"))?);
829                }
830            }
831            VocabKind::Bpe(_) => {
832                if add_special && self.add_bos {
833                    out.push(self.specials.bos.ok_or(TokenizeError::MissingSpecial("beginning-of-sequence"))?);
834                }
835                for f in &frags {
836                    match *f {
837                        Fragment::Raw { offset, len } => {
838                            let raw = &text[offset..offset + len];
839                            let t = if self.escape_whitespaces { replace_all(raw, b" ", ESCAPED_SPACE) } else { raw.to_vec() };
840                            match self.session {
841                                BpeSession::Plain => self.bpe(&t, &mut out)?,
842                                BpeSession::HybridDna => self.bpe_dna(&t, &mut out)?,
843                                BpeSession::Whitespace => self.bpe_whitespace(&t, &mut out)?,
844                            }
845                        }
846                        Fragment::Token(id) => out.push(id),
847                    }
848                }
849                if add_special && self.add_eos {
850                    out.push(self.specials.eos.ok_or(TokenizeError::MissingSpecial("end-of-sequence"))?);
851                }
852            }
853        }
854        Ok(out)
855    }
856
857    /// The SentencePiece merge loop over one escaped fragment (`llm_tokenizer_spm_session`).
858    fn spm(&self, text: &[u8], out: &mut Vec<u32>) -> Result<(), TokenizeError> {
859        #[derive(Clone, Copy)]
860        struct Sym {
861            prev: isize,
862            next: isize,
863            start: usize,
864            n: usize,
865        }
866        #[derive(PartialEq)]
867        struct Bigram {
868            score: f32,
869            left: usize,
870            right: usize,
871            size: usize,
872        }
873        impl Eq for Bigram {}
874        impl PartialOrd for Bigram {
875            fn partial_cmp(&self, o: &Self) -> Option<std::cmp::Ordering> {
876                Some(self.cmp(o))
877            }
878        }
879        impl Ord for Bigram {
880            fn cmp(&self, o: &Self) -> std::cmp::Ordering {
881                let less = |l: &Bigram, r: &Bigram| l.score < r.score || (l.score == r.score && l.left > r.left);
882                if less(self, o) {
883                    std::cmp::Ordering::Less
884                } else if less(o, self) {
885                    std::cmp::Ordering::Greater
886                } else {
887                    std::cmp::Ordering::Equal
888                }
889            }
890        }
891        let mut syms: Vec<Sym> = Vec::new();
892        let mut offs = 0;
893        while offs < text.len() {
894            let n = unicode::len_utf8(text[offs]).min(text.len() - offs);
895            let idx = syms.len() as isize;
896            offs += n;
897            syms.push(Sym { prev: idx - 1, next: if offs == text.len() { -1 } else { idx + 1 }, start: offs - n, n });
898        }
899        let mut queue = BinaryHeap::new();
900        let try_add = |syms: &[Sym], queue: &mut BinaryHeap<Bigram>, left: isize, right: isize| {
901            if left < 0 || right < 0 {
902                return;
903            }
904            let (l, r) = (syms[left as usize], syms[right as usize]);
905            let piece = &text[l.start..l.start + l.n + r.n];
906            if let Some(id) = self.text_to_token(piece) {
907                queue.push(Bigram { score: self.tokens[id as usize].score, left: left as usize, right: right as usize, size: piece.len() });
908            }
909        };
910        for i in 1..syms.len() {
911            try_add(&syms, &mut queue, i as isize - 1, i as isize);
912        }
913        while let Some(b) = queue.pop() {
914            let (l, r) = (syms[b.left], syms[b.right]);
915            if l.n == 0 || r.n == 0 || l.n + r.n != b.size {
916                continue;
917            }
918            syms[b.left].n += r.n;
919            syms[b.right].n = 0;
920            syms[b.left].next = r.next;
921            if r.next >= 0 {
922                syms[r.next as usize].prev = b.left as isize;
923            }
924            let prev = syms[b.left].prev;
925            let next = syms[b.left].next;
926            try_add(&syms, &mut queue, prev, b.left as isize);
927            try_add(&syms, &mut queue, b.left as isize, next);
928        }
929        let mut i: isize = if syms.is_empty() { -1 } else { 0 };
930        while i != -1 {
931            let s = syms[i as usize];
932            let piece = &text[s.start..s.start + s.n];
933            // A merged symbol is always a token, so a symbol that is not one is a single unmerged character.
934            match self.text_to_token(piece) {
935                Some(id) => out.push(id),
936                None => {
937                    for &b in piece {
938                        out.push(self.byte_to_token(b)?);
939                    }
940                }
941            }
942            i = s.next;
943        }
944        Ok(())
945    }
946
947    /// The byte-pair merge loop over one fragment (`llm_tokenizer_bpe_session::tokenize`).
948    fn bpe(&self, text: &[u8], out: &mut Vec<u32>) -> Result<(), TokenizeError> {
949        let VocabKind::Bpe(pre) = self.kind else { unreachable!("a byte-pair session runs a byte-pair vocabulary") };
950        let splitter = self.splitter.as_ref().expect("a byte-pair vocabulary has a splitter");
951        let mut words = splitter.split(text)?;
952        if pre == PreType::Mmbert {
953            let mut split = Vec::new();
954            for mut w in words {
955                if w.iter().all(|&c| c == b'\n') {
956                    split.push(w);
957                    continue;
958                }
959                if !w.starts_with(ESCAPED_SPACE) {
960                    let mut p = ESCAPED_SPACE.to_vec();
961                    p.extend_from_slice(&w);
962                    w = p;
963                }
964                let mut start = 0;
965                while start < w.len() {
966                    let end = find(&w, ESCAPED_SPACE, start + ESCAPED_SPACE.len()).unwrap_or(w.len());
967                    split.push(w[start..end].to_vec());
968                    start = end;
969                }
970            }
971            words = split;
972        }
973        let byte_encode = pre.byte_encode();
974        for word in &words {
975            for piece in self.bpe_word(word, pre) {
976                match self.text_to_token(piece) {
977                    Some(id) => out.push(id),
978                    None => {
979                        for &b in piece {
980                            let id = if byte_encode {
981                                self.text_to_token(std::slice::from_ref(&b))
982                            } else {
983                                const HEX: &[u8; 16] = b"0123456789ABCDEF";
984                                self.text_to_token(&[b'<', b'0', b'x', HEX[(b >> 4) as usize], HEX[(b & 15) as usize], b'>'])
985                            };
986                            if let Some(id) = id {
987                                out.push(id);
988                            }
989                        }
990                    }
991                }
992            }
993        }
994        Ok(())
995    }
996
997    /// The pieces one word merges into.
998    fn bpe_word<'w>(&self, word: &'w [u8], pre: PreType) -> Vec<&'w [u8]> {
999        #[derive(Clone, Copy)]
1000        struct Sym {
1001            prev: isize,
1002            next: isize,
1003            start: usize,
1004            n: usize,
1005        }
1006        #[derive(PartialEq, Eq)]
1007        struct Bigram {
1008            rank: u32,
1009            left: usize,
1010            right: usize,
1011            text: Vec<u8>,
1012        }
1013        impl PartialOrd for Bigram {
1014            fn partial_cmp(&self, o: &Self) -> Option<std::cmp::Ordering> {
1015                Some(self.cmp(o))
1016            }
1017        }
1018        impl Ord for Bigram {
1019            fn cmp(&self, o: &Self) -> std::cmp::Ordering {
1020                (o.rank, o.left).cmp(&(self.rank, self.left))
1021            }
1022        }
1023        let mut syms: Vec<Sym> = Vec::new();
1024        let mut offset = 0;
1025        if self.ignore_merges && self.text_to_token(word).is_some() {
1026            syms.push(Sym { prev: -1, next: -1, start: 0, n: word.len() });
1027            offset = word.len();
1028        } else if matches!(pre, PreType::Gemma4 | PreType::Mmbert)
1029            && word.iter().all(|&c| c == b'\n')
1030            && self.text_to_token(word).is_some()
1031        {
1032            syms.push(Sym { prev: -1, next: -1, start: 0, n: word.len() });
1033            offset = word.len();
1034        }
1035        while offset < word.len() {
1036            let n = (word.len() - offset).min(unicode::len_utf8(word[offset]));
1037            let idx = syms.len() as isize;
1038            offset += n;
1039            syms.push(Sym { prev: idx - 1, next: if offset == word.len() { -1 } else { idx + 1 }, start: offset - n, n });
1040        }
1041        let mut queue: BinaryHeap<Bigram> = BinaryHeap::new();
1042        let add = |syms: &[Sym], queue: &mut BinaryHeap<Bigram>, left: isize, right: isize| {
1043            if left < 0 || right < 0 {
1044                return;
1045            }
1046            let (l, r) = (syms[left as usize], syms[right as usize]);
1047            let (lt, rt) = (&word[l.start..l.start + l.n], &word[r.start..r.start + r.n]);
1048            if let Some(rank) = self.bpe_rank(lt, rt) {
1049                let mut text = lt.to_vec();
1050                text.extend_from_slice(rt);
1051                queue.push(Bigram { rank, left: left as usize, right: right as usize, text });
1052            }
1053        };
1054        for i in 1..syms.len() {
1055            add(&syms, &mut queue, i as isize - 1, i as isize);
1056        }
1057        while let Some(b) = queue.pop() {
1058            let (l, r) = (syms[b.left], syms[b.right]);
1059            if l.n == 0 || r.n == 0 {
1060                continue;
1061            }
1062            let joined_now = &word[l.start..l.start + l.n].iter().chain(&word[r.start..r.start + r.n]).copied().collect::<Vec<u8>>();
1063            if *joined_now != b.text {
1064                continue;
1065            }
1066            syms[b.left].n += r.n;
1067            syms[b.right].n = 0;
1068            syms[b.left].next = r.next;
1069            if r.next >= 0 {
1070                syms[r.next as usize].prev = b.left as isize;
1071            }
1072            let prev = syms[b.left].prev;
1073            let next = syms[b.left].next;
1074            add(&syms, &mut queue, prev, b.left as isize);
1075            add(&syms, &mut queue, b.left as isize, next);
1076        }
1077        syms.iter().filter(|s| s.n > 0).map(|s| &word[s.start..s.start + s.n]).collect()
1078    }
1079
1080    /// The `hybriddna` session: `<dna>…</dna>` spans become k-mer tokens, the rest merges as byte pairs.
1081    fn bpe_dna(&self, text: &[u8], out: &mut Vec<u32>) -> Result<(), TokenizeError> {
1082        let (Some(open), Some(close), Some(oov)) =
1083            (self.text_to_token(b"<dna>"), self.text_to_token(b"</dna>"), self.text_to_token(b"<oov>"))
1084        else {
1085            return self.bpe(text, out);
1086        };
1087        const K: usize = 6;
1088        let mut pos = 0;
1089        while pos < text.len() {
1090            let Some(start) = find(text, b"<dna>", pos) else {
1091                self.bpe(&text[pos..], out)?;
1092                break;
1093            };
1094            if start > pos {
1095                self.bpe(&text[pos..start], out)?;
1096            }
1097            out.push(open);
1098            let content = start + 5;
1099            let end = find(text, b"</dna>", content);
1100            let seq: Vec<u8> = text[content..end.unwrap_or(text.len())].iter().map(|c| c.to_ascii_uppercase()).collect();
1101            let kmer = |k: &[u8]| {
1102                let mut t = k.to_vec();
1103                t.extend_from_slice(DNA_KMER_MARKER);
1104                self.text_to_token(&t).unwrap_or(oov)
1105            };
1106            let mut i = 0;
1107            while i + K <= seq.len() {
1108                out.push(kmer(&seq[i..i + K]));
1109                i += K;
1110            }
1111            if i < seq.len() {
1112                let mut tail = seq[i..].to_vec();
1113                tail.resize(K, b'A');
1114                out.push(kmer(&tail));
1115            }
1116            let Some(end) = end else { break };
1117            out.push(close);
1118            pos = end + 6;
1119        }
1120        Ok(())
1121    }
1122
1123    /// The `whitespace` session: whitespace-separated segments, lowercased when the normalizer says so.
1124    fn bpe_whitespace(&self, text: &[u8], out: &mut Vec<u32>) -> Result<(), TokenizeError> {
1125        let mut seg = Vec::new();
1126        for c in unicode::cpts_from_utf8(text) {
1127            if unicode::flags(c).is_whitespace() {
1128                if !seg.is_empty() {
1129                    self.bpe(&seg, out)?;
1130                    seg.clear();
1131                }
1132            } else {
1133                unicode::push_utf8(if self.lowercase { unicode::to_lower(c) } else { c }, &mut seg)?;
1134            }
1135        }
1136        if !seg.is_empty() {
1137            self.bpe(&seg, out)?;
1138        }
1139        Ok(())
1140    }
1141
1142    /// The text of a token as `token_to_piece` renders it with `special` on and no strip (the cached piece).
1143    fn piece_uncached(&self, id: u32) -> Vec<u8> {
1144        let t = &self.tokens[id as usize];
1145        let a = t.attr;
1146        if a & (attr::UNKNOWN | attr::CONTROL | attr::USER_DEFINED) != 0 {
1147            return t.text.clone();
1148        }
1149        if a & attr::NORMAL != 0 {
1150            return match self.kind {
1151                VocabKind::Spm => replace_all(&t.text, ESCAPED_SPACE, b" "),
1152                VocabKind::Bpe(_) if self.escape_whitespaces => replace_all(&t.text, ESCAPED_SPACE, b" "),
1153                VocabKind::Bpe(_) => decode_text(&t.text),
1154            };
1155        }
1156        if a & attr::BYTE != 0 {
1157            return vec![self.token_to_byte(id)];
1158        }
1159        Vec::new()
1160    }
1161
1162    /// `llama_token_to_piece`: the text of `id`, up to `lstrip` leading spaces removed; a control or unknown token
1163    /// renders only when `special` is on.
1164    pub fn token_to_piece(&self, id: u32, lstrip: usize, special: bool) -> &[u8] {
1165        if !special && self.tokens[id as usize].attr & (attr::UNKNOWN | attr::CONTROL) != 0 {
1166            return &[];
1167        }
1168        let p = &self.piece_cache[id as usize];
1169        let strip = p.iter().take(lstrip).take_while(|&&c| c == b' ').count();
1170        &p[strip..]
1171    }
1172
1173    /// `llama_detokenize`: the text of `tokens`. `remove_special` drops the leading BOS and trailing EOS the
1174    /// vocabulary adds; `unparse_special` renders control tokens.
1175    pub fn detokenize(&self, tokens: &[u32], remove_special: bool, unparse_special: bool) -> Vec<u8> {
1176        let mut toks = tokens;
1177        let mut remove_space = self.add_space_prefix;
1178        if remove_special && self.add_bos && !toks.is_empty() && Some(toks[0]) == self.specials.bos {
1179            remove_space = false;
1180            toks = &toks[1..];
1181        }
1182        if remove_special && self.add_eos && !toks.is_empty() && Some(toks[toks.len() - 1]) == self.specials.eos {
1183            toks = &toks[..toks.len() - 1];
1184        }
1185        let mut text = Vec::new();
1186        for &t in toks {
1187            text.extend_from_slice(self.token_to_piece(t, remove_space as usize, unparse_special));
1188            remove_space = false;
1189        }
1190        if self.clean_spaces {
1191            clean_spaces(&mut text);
1192        }
1193        text
1194    }
1195}
1196
1197/// `llama_decode_text`: a byte-level token back to its bytes; a codepoint outside the byte alphabet is written as
1198/// llama.cpp writes it, `[UNK_BYTE_0x…<token>]`.
1199fn decode_text(text: &[u8]) -> Vec<u8> {
1200    let mut out = Vec::with_capacity(text.len());
1201    for c in unicode::cpts_from_utf8(text) {
1202        match unicode::utf8_to_byte(c) {
1203            Some(b) => out.push(b),
1204            None => {
1205                let mut u = Vec::new();
1206                if unicode::push_utf8(c, &mut u).is_err() {
1207                    continue;
1208                }
1209                out.extend_from_slice(b"[UNK_BYTE_0x");
1210                for b in &u {
1211                    out.extend_from_slice(format!("{b:02x}").as_bytes());
1212                }
1213                out.extend_from_slice(text);
1214                out.push(b']');
1215            }
1216        }
1217    }
1218    out
1219}
1220
1221/// The three `clean_up_tokenization_spaces` passes of `llama_detokenize`, run in place over the buffer exactly as
1222/// llama.cpp runs them (a pass reads characters an earlier step of the same pass rewrote).
1223fn clean_spaces(text: &mut Vec<u8>) {
1224    let mut total = text.len();
1225    let total1 = total;
1226    total = if total > 0 { 1 } else { 0 };
1227    for i in 1..total1 {
1228        let x = text[i];
1229        if text[i - 1] == b' ' && matches!(x, b'?' | b'!' | b'.' | b',') {
1230            total -= 1;
1231        }
1232        text[total] = x;
1233        total += 1;
1234    }
1235    let total2 = total;
1236    total = if total > 0 { 1 } else { 0 };
1237    let mut i = 1;
1238    while i < total2 {
1239        let x = text[i];
1240        if x == b'\'' && i + 1 < total2 && text[i - 1] == b' ' && text[i + 1] == b' ' {
1241            total -= 1;
1242            i += 1;
1243            text[i] = 0;
1244        }
1245        text[total] = x;
1246        total += 1;
1247        i += 1;
1248    }
1249    let total3 = total;
1250    total = if total > 0 { 1 } else { 0 };
1251    for i in 1..total3 {
1252        let x = text[i];
1253        if text[i - 1] == b' ' && x == b'\'' && i + 1 < total3 {
1254            let x1 = text[i + 1];
1255            if x1 == b't' || x1 == b'd' {
1256            } else if x1 == b's' || x1 == b'm' {
1257                total -= 1;
1258            } else if i + 2 < total3 {
1259                let x2 = text[i + 2];
1260                if (x1 == b'r' && x2 == b'e') || (x1 == b'v' && x2 == b'e') {
1261                    total -= 1;
1262                }
1263            }
1264        }
1265        text[total] = x;
1266        total += 1;
1267    }
1268    text.truncate(total);
1269}