1use serde::Deserialize;
13use std::collections::{HashMap, HashSet};
14use std::path::Path;
15use std::sync::OnceLock;
16use unicode_normalization::UnicodeNormalization;
17
18const DEFAULT_SPLIT: &str = r"(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+";
21
22pub const TOOL_MARKUP_TOKENS: &[&str] = &[
34 "<tool_call>",
35 "</tool_call>",
36 "<function",
37 "</function>",
38 "<param",
39 "</param>",
40];
41
42pub struct Tokenizer {
44 vocab: HashMap<String, u32>,
46 id_to_token: Vec<String>,
48 ranks: HashMap<(String, String), u32>,
50 sp_ids: OnceLock<Option<SpMerges>>,
54 added: Vec<(String, u32)>,
56 added_ids: HashSet<u32>,
58 special_ids: HashSet<u32>,
60 split_res: Vec<fancy_regex::Regex>,
64 split_drop_gap: Vec<bool>,
70 eow_suffix: Option<String>,
75 lowercase: bool,
78 collapse_ws: bool,
81 sp_prepend: bool,
84 sp_prepend_first: bool,
90 metaspace: bool,
93 nfc: bool,
96 byte_to_char: [char; 256],
98 char_to_byte: HashMap<char, u8>,
100 pub bos_token_id: Option<u32>,
102 pub eos_token_id: Option<u32>,
103 pub pad_token_id: Option<u32>,
104 pub im_start_id: Option<u32>,
106 pub im_end_id: Option<u32>,
107 pub chat_template: Option<String>,
110 pub extra_eos: HashSet<u32>,
112 pub add_bos: bool,
114}
115
116impl std::fmt::Debug for Tokenizer {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 f.debug_struct("Tokenizer")
119 .field("vocab", &self.vocab.len())
120 .field("merges", &self.ranks.len())
121 .field("added", &self.added.len())
122 .finish()
123 }
124}
125
126fn bytes_to_unicode() -> ([char; 256], HashMap<char, u8>) {
129 let mut b2c = ['\0'; 256];
130 let mut c2b = HashMap::with_capacity(256);
131 let mut n = 0u32;
132 for b in 0..=255u16 {
133 let printable =
134 (0x21..=0x7E).contains(&b) || (0xA1..=0xAC).contains(&b) || (0xAE..=0xFF).contains(&b);
135 let c = if printable {
136 char::from_u32(b as u32).unwrap()
137 } else {
138 let c = char::from_u32(256 + n).unwrap();
139 n += 1;
140 c
141 };
142 b2c[b as usize] = c;
143 c2b.insert(c, b as u8);
144 }
145 (b2c, c2b)
146}
147
148#[derive(Deserialize)]
150struct HfTokenizerJson {
151 model: HfModel,
152 #[serde(default)]
153 added_tokens: Vec<HfAddedToken>,
154 #[serde(default)]
155 pre_tokenizer: Option<serde_json::Value>,
156 #[serde(default)]
157 normalizer: Option<serde_json::Value>,
158 #[serde(default)]
159 post_processor: Option<serde_json::Value>,
160}
161
162#[derive(Deserialize)]
163struct HfModel {
164 vocab: HashMap<String, u32>,
165 #[serde(default)]
166 merges: Vec<HfMerge>,
167 #[serde(default)]
168 byte_fallback: bool,
169 #[serde(default)]
171 end_of_word_suffix: Option<String>,
172}
173
174#[derive(Deserialize)]
177#[serde(untagged)]
178enum HfMerge {
179 Pair([String; 2]),
180 Text(String),
181}
182
183#[derive(Deserialize)]
184struct HfAddedToken {
185 id: u32,
186 content: String,
187 special: bool,
188}
189
190fn collect_split_patterns(pt: &serde_json::Value, out: &mut Vec<String>) {
199 if pt.get("type").and_then(|t| t.as_str()) == Some("Split") {
200 if let Some(r) = pt
201 .get("pattern")
202 .and_then(|p| p.get("Regex"))
203 .and_then(|r| r.as_str())
204 {
205 out.push(r.to_string());
206 }
207 return;
208 }
209 if let Some(list) = pt.get("pretokenizers").and_then(|l| l.as_array()) {
210 for p in list {
211 collect_split_patterns(p, out);
212 }
213 }
214}
215
216fn collect_split_drop_gaps(pt: &serde_json::Value, out: &mut Vec<bool>) {
220 if pt.get("type").and_then(|t| t.as_str()) == Some("Split") {
221 let behavior = pt.get("behavior").and_then(|b| b.as_str()).unwrap_or("");
222 let invert = pt.get("invert").and_then(|i| i.as_bool()).unwrap_or(false);
223 out.push(behavior == "Removed" && invert);
224 return;
225 }
226 if let Some(list) = pt.get("pretokenizers").and_then(|l| l.as_array()) {
227 for p in list {
228 collect_split_drop_gaps(p, out);
229 }
230 }
231}
232
233fn find_prepend_scheme(pt: &serde_json::Value) -> Option<String> {
236 if pt.get("type").and_then(|t| t.as_str()) == Some("Metaspace") {
237 return pt
238 .get("prepend_scheme")
239 .and_then(|p| p.as_str())
240 .map(String::from);
241 }
242 if let Some(list) = pt.get("pretokenizers").and_then(|l| l.as_array()) {
243 return list.iter().find_map(find_prepend_scheme);
244 }
245 None
246}
247
248pub(crate) fn strip_generation_tags(tpl: &str) -> std::borrow::Cow<'_, str> {
264 if !tpl.contains("generation") {
265 return std::borrow::Cow::Borrowed(tpl);
266 }
267 let mut out = String::with_capacity(tpl.len());
268 let mut rest = tpl;
269 let mut touched = false;
270 while let Some(open) = rest.find("{%") {
271 let Some(close_rel) = rest[open..].find("%}") else {
272 break;
273 };
274 let close = open + close_rel + 2;
275 let tag = &rest[open..close];
276 let inner = tag[2..tag.len() - 2].trim();
277 let lead = inner.starts_with('-');
278 let trail = inner.ends_with('-');
279 let name = inner.trim_matches('-').trim();
280 out.push_str(&rest[..open]);
281 if name == "generation" || name == "endgeneration" {
282 out.push_str(if lead { "{%-" } else { "{%" });
283 out.push_str(" set _generation_span = true ");
284 out.push_str(if trail { "-%}" } else { "%}" });
285 touched = true;
286 } else {
287 out.push_str(tag);
288 }
289 rest = &rest[close..];
290 }
291 if !touched {
292 return std::borrow::Cow::Borrowed(tpl);
293 }
294 out.push_str(rest);
295 std::borrow::Cow::Owned(out)
296}
297
298fn close_direct_think(rendered: String, enable_thinking: Option<bool>) -> String {
307 if enable_thinking != Some(false) || rendered.contains("</think>") {
308 return rendered;
309 }
310 if let Some(pos) = rendered.rfind("<|assistant|>") {
311 let marker_end = pos + "<|assistant|>".len();
312 let after = &rendered[marker_end..];
313 let mut out = String::with_capacity(rendered.len() + 24);
314 out.push_str(&rendered[..marker_end]);
315 if let Some(rest) = after.strip_prefix("<think>") {
316 out.push_str("<think></think>\n\n");
317 out.push_str(rest);
318 } else {
319 out.push_str(after);
320 if !out.ends_with('\n') {
321 out.push('\n');
322 }
323 out.push_str("<think>\n\n</think>\n\n");
324 }
325 return out;
326 }
327 if let Some(pos) = rendered.rfind("\nassistant") {
331 let mut insert_at = pos + 1 + "assistant".len();
332 if let Some(idx) = rendered[insert_at..].find('\n') {
333 insert_at += idx + 1;
334 }
335 let mut out = String::with_capacity(rendered.len() + 24);
336 out.push_str(&rendered[..insert_at]);
337 if !out.ends_with('\n') {
338 out.push('\n');
339 }
340 out.push_str("<think>\n\n</think>\n\n");
341 out.push_str(&rendered[insert_at..]);
342 return out;
343 }
344 rendered
345}
346
347impl Tokenizer {
348 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, TokenizerError> {
350 let data = std::fs::read_to_string(path.as_ref())
351 .map_err(|e| TokenizerError::Io(e.to_string()))?;
352 Self::from_json(&data)
353 }
354
355 pub fn from_bytes(bytes: &[u8]) -> Result<Self, TokenizerError> {
357 let s = std::str::from_utf8(bytes)
358 .map_err(|e| TokenizerError::Parse(format!("vocab is not UTF-8: {e}")))?;
359 Self::from_json(s)
360 }
361
362 pub fn from_json(json: &str) -> Result<Self, TokenizerError> {
364 let hf: HfTokenizerJson =
365 serde_json::from_str(json).map_err(|e| TokenizerError::Parse(e.to_string()))?;
366
367 let mut vocab = hf.model.vocab;
368 let mut ranks = HashMap::new();
369 for (rank, m) in hf.model.merges.into_iter().enumerate() {
370 let (a, b) = match m {
371 HfMerge::Pair([a, b]) => (a, b),
372 HfMerge::Text(s) => {
373 let mut it = s.splitn(2, ' ');
374 match (it.next(), it.next()) {
375 (Some(a), Some(b)) => (a.to_string(), b.to_string()),
376 _ => continue,
377 }
378 }
379 };
380 ranks.insert((a, b), rank as u32);
381 }
382
383 let mut saw_gemma_bos = false;
388 let add_bos_detected = hf
389 .post_processor
390 .as_ref()
391 .map(|p| {
392 let pp = p.to_string();
393 pp.contains("\"<s>\"") || pp.contains("\"<bos>\"")
394 })
395 .unwrap_or(false);
396 let nfc = hf
397 .normalizer
398 .as_ref()
399 .map(|n| n.to_string().contains("NFC"))
400 .unwrap_or(false);
401 let metaspace = hf.model.byte_fallback
402 || hf
403 .normalizer
404 .as_ref()
405 .map(|n| n.to_string().contains("\u{2581}") || n.to_string().contains("▁"))
406 .unwrap_or(false);
407 let sp_prepend = hf
408 .normalizer
409 .as_ref()
410 .map(|n| n.to_string().contains("Prepend"))
411 .unwrap_or(false);
412 let (sp_prepend, sp_prepend_first) = if sp_prepend {
416 (true, false)
417 } else {
418 match hf.pre_tokenizer.as_ref().and_then(find_prepend_scheme) {
419 Some(s) if s == "always" => (true, false),
420 Some(s) if s == "first" => (false, true),
421 _ => (false, false),
422 }
423 };
424 let (split_res, split_drop_gap) = if metaspace {
425 (Vec::new(), Vec::new())
426 } else {
427 let mut pats = Vec::new();
428 if let Some(pt) = hf.pre_tokenizer.as_ref() {
429 collect_split_patterns(pt, &mut pats);
430 }
431 if pats.is_empty() {
432 pats.push(DEFAULT_SPLIT.to_string());
433 }
434 let mut split_drop_gap = Vec::new();
435 if let Some(pt) = hf.pre_tokenizer.as_ref() {
436 collect_split_drop_gaps(pt, &mut split_drop_gap);
437 }
438 while split_drop_gap.len() < pats.len() {
439 split_drop_gap.push(false);
440 }
441 split_drop_gap.truncate(pats.len());
442 (
443 pats.iter()
444 .map(|p| {
445 fancy_regex::Regex::new(p)
446 .map_err(|e| TokenizerError::Parse(format!("pre-tokenizer regex: {e}")))
447 })
448 .collect::<Result<Vec<_>, _>>()?,
449 split_drop_gap,
450 )
451 };
452 let norm_s = hf
454 .normalizer
455 .as_ref()
456 .map(|n| n.to_string())
457 .unwrap_or_default();
458 let collapse_ws = norm_s.contains("\"Replace\"") && norm_s.contains("\\\\s+");
459 let lowercase = norm_s.contains("\"Lowercase\"");
460
461 let mut bos_token_id = None;
463 let mut eos_token_id = None;
464 let mut pad_token_id = None;
465 let mut im_start_id = None;
466 let mut im_end_id = None;
467 let mut special_ids = HashSet::new();
468 let mut added_ids = HashSet::new();
469 let mut added = Vec::new();
470
471 for at in &hf.added_tokens {
472 vocab.insert(at.content.clone(), at.id);
473 added.push((at.content.clone(), at.id));
474 added_ids.insert(at.id);
475 if at.special && !TOOL_MARKUP_TOKENS.contains(&at.content.as_str()) {
476 special_ids.insert(at.id);
477 }
478 match at.content.as_str() {
479 "<|endoftext|>" | "</s>" | "[EOS]" => eos_token_id = Some(at.id),
480 "<|im_start|>" => im_start_id = Some(at.id),
481 "<|im_end|>" => im_end_id = Some(at.id),
482 "<s>" | "[BOS]" | "<|startoftext|>" => bos_token_id = Some(at.id),
483 "<bos>" => {
487 bos_token_id = Some(at.id);
488 saw_gemma_bos = true;
489 }
490 "<pad>" => pad_token_id = Some(at.id),
491 _ => {}
492 }
493 }
494
495 const DSV41_SPECIALS: &[&str] = &[
500 "<|begin▁of▁sentence|>",
501 "<|end▁of▁sentence|>",
502 "<|User|>",
503 "<|Assistant|>",
504 "<|System|>",
505 "<|latest_reminder|>",
506 "<|deepseek_image|>",
507 "<|action|>",
508 "<|query|>",
509 "<|authority|>",
510 "<|domain|>",
511 "<|title|>",
512 "<|read_url|>",
513 "<think>",
514 "</think>",
515 "|DSML|",
516 ];
517 for token in DSV41_SPECIALS {
518 if let Some(&id) = vocab.get(*token) {
519 if !added.iter().any(|(content, _)| content.as_str() == *token) {
520 added.push(((*token).to_string(), id));
521 }
522 added_ids.insert(id);
523 match *token {
524 "<|begin▁of▁sentence|>" => bos_token_id = Some(id),
525 "<|end▁of▁sentence|>" => eos_token_id = Some(id),
526 _ => {}
527 }
528 }
529 }
530 added.sort_by_key(|(c, _)| std::cmp::Reverse(c.len()));
531
532 let gemma_family = saw_gemma_bos
539 || vocab.contains_key("<start_of_turn>")
540 || added.iter().any(|(c, _)| c == "<start_of_turn>");
541
542 if let Some(pp) = hf.post_processor.as_ref() {
546 let pp = pp.to_string();
547 for name in ["<bos>", "<s>"] {
548 if pp.contains(&format!("\"{name}\"")) {
549 if let Some(&id) = vocab.get(name) {
550 bos_token_id = Some(id);
551 }
552 break;
553 }
554 }
555 }
556
557 let max_id = vocab.values().copied().max().unwrap_or(0) as usize;
559 let mut id_to_token = vec![String::new(); max_id + 1];
560 for (token, &id) in &vocab {
561 if (id as usize) < id_to_token.len() {
562 id_to_token[id as usize] = token.clone();
563 }
564 }
565
566 let (byte_to_char, char_to_byte) = bytes_to_unicode();
567
568 tracing::info!(
569 "Tokenizer loaded: {} vocab, {} merges, {} added, eos={:?}",
570 vocab.len(),
571 ranks.len(),
572 added.len(),
573 eos_token_id
574 );
575
576 Ok(Self {
577 vocab,
578 id_to_token,
579 ranks,
580 sp_ids: OnceLock::new(),
581 added,
582 added_ids,
583 special_ids,
584 split_res,
585 split_drop_gap,
586 eow_suffix: hf.model.end_of_word_suffix,
587 lowercase,
588 collapse_ws,
589 metaspace,
590 sp_prepend,
591 sp_prepend_first,
592 nfc,
593 byte_to_char,
594 char_to_byte,
595 bos_token_id,
596 eos_token_id,
597 pad_token_id,
598 im_start_id,
599 im_end_id,
600 chat_template: None,
601 extra_eos: HashSet::new(),
602 add_bos: add_bos_detected || gemma_family,
603 })
604 }
605
606 pub fn byte_level() -> Self {
608 let mut vocab = HashMap::new();
609 let mut id_to_token = Vec::with_capacity(256);
610 for i in 0..256u32 {
611 let tok = format!("<0x{:02X}>", i);
612 vocab.insert(tok.clone(), i);
613 id_to_token.push(tok);
614 }
615 let (byte_to_char, char_to_byte) = bytes_to_unicode();
616 Self {
617 vocab,
618 id_to_token,
619 ranks: HashMap::new(),
620 sp_ids: OnceLock::new(),
621 added: Vec::new(),
622 added_ids: HashSet::new(),
623 special_ids: HashSet::new(),
624 split_res: Vec::new(),
625 split_drop_gap: Vec::new(),
626 eow_suffix: None,
627 lowercase: false,
628 collapse_ws: false,
629 metaspace: false,
630 sp_prepend: false,
631 sp_prepend_first: false,
632 nfc: false,
633 byte_to_char,
634 char_to_byte,
635 bos_token_id: None,
636 eos_token_id: None,
637 pad_token_id: None,
638 im_start_id: None,
639 im_end_id: None,
640 chat_template: None,
641 extra_eos: HashSet::new(),
642 add_bos: false,
643 }
644 }
645
646 pub fn encode(&self, text: &str) -> Vec<u32> {
648 let mut ids = Vec::new();
649 let mut rest = text;
651 let mut head = true;
656 'outer: while !rest.is_empty() {
657 let mut best: Option<(usize, usize, u32)> = None; for (content, id) in &self.added {
659 if let Some(pos) = rest.find(content.as_str()) {
660 let better = match best {
661 None => true,
662 Some((bp, bl, _)) => pos < bp || (pos == bp && content.len() > bl),
663 };
664 if better {
665 best = Some((pos, content.len(), *id));
666 }
667 if pos == 0 {
668 break; }
670 }
671 }
672 match best {
673 Some((pos, len, id)) => {
674 self.encode_segment_at(&rest[..pos], head, &mut ids);
675 ids.push(id);
676 rest = &rest[pos + len..];
677 head = false;
678 }
679 None => {
680 self.encode_segment_at(rest, head, &mut ids);
681 break 'outer;
682 }
683 }
684 }
685 ids
686 }
687
688 pub fn encode_plain(&self, text: &str) -> Vec<u32> {
694 let mut ids = Vec::new();
695 self.encode_segment_at(text, true, &mut ids);
696 ids
697 }
698
699 fn encode_segment_at(&self, segment: &str, head: bool, out: &mut Vec<u32>) {
704 if segment.is_empty() {
705 return;
706 }
707 let mut norm: String = if self.nfc {
708 segment.nfc().collect()
709 } else {
710 segment.to_string()
711 };
712 if self.collapse_ws {
713 let mut collapsed = String::with_capacity(norm.len());
715 let mut in_ws = false;
716 for ch in norm.chars() {
717 if ch.is_whitespace() {
718 if !in_ws {
719 collapsed.push(' ');
720 in_ws = true;
721 }
722 } else {
723 collapsed.push(ch);
724 in_ws = false;
725 }
726 }
727 norm = collapsed;
728 }
729 if self.lowercase {
730 norm = norm.to_lowercase();
731 }
732 if self.metaspace {
733 let sp = if self.sp_prepend {
737 format!("\u{2581}{}", norm).replace(' ', "\u{2581}")
740 } else {
741 let replaced = norm.replace(' ', "\u{2581}");
745 if self.sp_prepend_first && head && !replaced.starts_with('\u{2581}') {
746 format!("\u{2581}{replaced}")
747 } else {
748 replaced
749 }
750 };
751 self.bpe_piece_sp(&sp, out);
752 return;
753 }
754 if !self.split_res.is_empty() {
755 let mut pieces: Vec<(usize, usize)> = vec![(0, norm.len())];
760 for (stage, re) in self.split_res.iter().enumerate() {
761 let drop = self.split_drop_gap.get(stage).copied().unwrap_or(false);
762 let mut next: Vec<(usize, usize)> = Vec::with_capacity(pieces.len() * 2);
763 for (ps, pe) in pieces {
764 let seg = &norm[ps..pe];
765 let mut last = 0usize;
766 for m in re.find_iter(seg) {
767 let m = match m {
768 Ok(m) => m,
769 Err(e) => {
770 tracing::error!("pre-tokenizer regex failed: {e}");
771 break;
772 }
773 };
774 if m.start() > last && !drop {
775 next.push((ps + last, ps + m.start()));
776 }
777 if m.end() > m.start() {
778 next.push((ps + m.start(), ps + m.end()));
779 }
780 last = m.end();
781 }
782 if last < seg.len() && !drop {
783 next.push((ps + last, pe));
784 }
785 }
786 pieces = next;
787 }
788 for (ps, pe) in pieces {
789 self.bpe_piece(&norm[ps..pe], out);
790 }
791 } else {
792 {
793 for b in norm.bytes() {
795 let tok = format!("<0x{:02X}>", b);
796 if let Some(&id) = self.vocab.get(&tok) {
797 out.push(id);
798 }
799 }
800 }
801 }
802 }
803
804 fn sp_merges(&self) -> Option<&SpMerges> {
806 self.sp_ids
807 .get_or_init(|| SpMerges::build(&self.vocab, &self.ranks))
808 .as_ref()
809 }
810
811 fn bpe_piece_sp(&self, piece: &str, out: &mut Vec<u32>) {
820 if piece.is_empty() {
821 return;
822 }
823 if !SP_STRINGS.with(|f| f.get()) {
824 if let Some(m) = self.sp_merges() {
825 m.encode(&self.vocab, piece, out);
826 return;
827 }
828 }
829 self.bpe_piece_sp_strings(piece, out);
830 }
831
832 #[doc(hidden)]
835 pub fn encode_reference(&self, text: &str) -> Vec<u32> {
836 SP_STRINGS.with(|f| f.set(true));
837 let ids = self.encode(text);
838 SP_STRINGS.with(|f| f.set(false));
839 ids
840 }
841
842 fn bpe_piece_sp_strings(&self, piece: &str, out: &mut Vec<u32>) {
843 let mut sym: Vec<String> = piece.chars().map(|c| c.to_string()).collect();
844 loop {
845 let mut best: Option<(u32, usize)> = None;
846 for i in 0..sym.len().saturating_sub(1) {
847 if let Some(&r) = self.ranks.get(&(sym[i].clone(), sym[i + 1].clone())) {
848 if best.map(|(br, _)| r < br).unwrap_or(true) {
849 best = Some((r, i));
850 }
851 }
852 }
853 let Some((_, i)) = best else { break };
854 let merged = format!("{}{}", sym[i], sym[i + 1]);
855 let (left, right) = (sym[i].clone(), sym[i + 1].clone());
856 let mut j = 0;
857 while j + 1 < sym.len() {
858 if sym[j] == left && sym[j + 1] == right {
859 sym[j] = merged.clone();
860 sym.remove(j + 1);
861 }
862 j += 1;
863 }
864 }
865 for t in &sym {
866 if let Some(&id) = self.vocab.get(t) {
867 out.push(id);
868 } else {
869 let mut ok = true;
870 for byte in t.bytes() {
871 let tok = format!("<0x{:02X}>", byte);
872 match self.vocab.get(&tok) {
873 Some(&id) => out.push(id),
874 None => {
875 ok = false;
876 break;
877 }
878 }
879 }
880 if !ok {
881 tracing::error!("tokenizer: no id for SP symbol {t:?} — dropped");
882 }
883 }
884 }
885 }
886
887 fn bpe_piece(&self, piece: &str, out: &mut Vec<u32>) {
889 if piece.is_empty() {
890 return;
891 }
892 let mapped: Vec<String> = piece
893 .bytes()
894 .map(|b| self.byte_to_char[b as usize].to_string())
895 .collect();
896 let mut sym = mapped;
897 if let Some(sfx) = &self.eow_suffix {
901 if let Some(last) = sym.last_mut() {
902 last.push_str(sfx);
903 }
904 }
905
906 loop {
908 let mut best: Option<(u32, usize)> = None;
909 for i in 0..sym.len().saturating_sub(1) {
910 if let Some(&r) = self.ranks.get(&(sym[i].clone(), sym[i + 1].clone())) {
911 if best.map(|(br, _)| r < br).unwrap_or(true) {
912 best = Some((r, i));
913 }
914 }
915 }
916 let Some((_, i)) = best else { break };
917 let merged = format!("{}{}", sym[i], sym[i + 1]);
918 let (left, right) = (sym[i].clone(), sym[i + 1].clone());
920 let mut j = 0;
921 while j + 1 < sym.len() {
922 if sym[j] == left && sym[j + 1] == right {
923 sym[j] = merged.clone();
924 sym.remove(j + 1);
925 }
926 j += 1;
927 }
928 }
929
930 for s in &sym {
931 if let Some(&id) = self.vocab.get(s) {
932 out.push(id);
933 } else {
934 let mut ok = true;
936 for ch in s.chars() {
937 let Some(&b) = self.char_to_byte.get(&ch) else {
938 ok = false;
939 break;
940 };
941 let tok = format!("<0x{:02X}>", b);
942 if let Some(&id) = self.vocab.get(&tok) {
943 out.push(id);
944 } else {
945 ok = false;
946 break;
947 }
948 }
949 if !ok {
950 tracing::error!("tokenizer: no id for symbol {s:?} — dropped");
951 }
952 }
953 }
954 }
955
956 pub fn decode(&self, ids: &[u32]) -> String {
959 let mut bytes: Vec<u8> = Vec::new();
960 for &id in ids {
961 if self.special_ids.contains(&id) {
962 continue;
963 }
964 let idx = id as usize;
965 if idx >= self.id_to_token.len() {
966 continue;
967 }
968 let tok = &self.id_to_token[idx];
969 if self.added_ids.contains(&id) {
970 if self.metaspace && tok.contains('\u{2581}') {
973 bytes.extend_from_slice(tok.replace('\u{2581}', " ").as_bytes());
974 } else {
975 bytes.extend_from_slice(tok.as_bytes());
976 }
977 continue;
978 }
979 if tok.starts_with("<0x") && tok.ends_with('>') && tok.len() == 6 {
981 if let Ok(b) = u8::from_str_radix(&tok[3..5], 16) {
982 bytes.push(b);
983 continue;
984 }
985 }
986 if self.metaspace {
987 for ch in tok.chars() {
989 if ch == '\u{2581}' {
990 bytes.push(b' ');
991 } else {
992 let mut buf = [0u8; 4];
993 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
994 }
995 }
996 continue;
997 }
998 for ch in tok.chars() {
999 match self.char_to_byte.get(&ch) {
1000 Some(&b) => bytes.push(b),
1001 None => {
1004 let mut buf = [0u8; 4];
1005 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
1006 }
1007 }
1008 }
1009 }
1010 let text = String::from_utf8_lossy(&bytes).into_owned();
1011 if self.metaspace && (self.sp_prepend || self.sp_prepend_first) {
1012 if let Some(stripped) = text.strip_prefix(' ') {
1014 return stripped.to_string();
1015 }
1016 }
1017 text
1018 }
1019
1020 pub fn decode_token(&self, id: u32) -> String {
1023 if self.special_ids.contains(&id) {
1024 return String::new();
1025 }
1026 let idx = id as usize;
1027 if idx >= self.id_to_token.len() {
1028 return String::new();
1029 }
1030 let tok = &self.id_to_token[idx];
1031 if self.added_ids.contains(&id) {
1032 if self.metaspace && tok.contains('\u{2581}') {
1033 return tok.replace('\u{2581}', " ");
1034 }
1035 return tok.clone();
1036 }
1037 if tok.starts_with("<0x") && tok.ends_with('>') && tok.len() == 6 {
1038 if let Ok(b) = u8::from_str_radix(&tok[3..5], 16) {
1039 return String::from_utf8_lossy(&[b]).into_owned();
1040 }
1041 }
1042 if self.metaspace {
1043 return tok.replace('\u{2581}', " ");
1044 }
1045 let mut bytes = Vec::new();
1046 for ch in tok.chars() {
1047 match self.char_to_byte.get(&ch) {
1048 Some(&b) => bytes.push(b),
1049 None => {
1050 let mut buf = [0u8; 4];
1051 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
1052 }
1053 }
1054 }
1055 String::from_utf8_lossy(&bytes).into_owned()
1056 }
1057
1058 pub fn token_bytes(&self, id: u32) -> Vec<u8> {
1061 let idx = id as usize;
1062 let Some(tok) = self.id_to_token.get(idx) else {
1063 return Vec::new();
1064 };
1065 if self.added_ids.contains(&id) {
1066 return tok.as_bytes().to_vec();
1067 }
1068 if tok.starts_with("<0x") && tok.ends_with('>') && tok.len() == 6 {
1069 if let Ok(b) = u8::from_str_radix(&tok[3..5], 16) {
1070 return vec![b];
1071 }
1072 }
1073 if self.metaspace {
1074 return tok.replace('\u{2581}', " ").into_bytes();
1075 }
1076 let mut bytes = Vec::new();
1077 for ch in tok.chars() {
1078 match self.char_to_byte.get(&ch) {
1079 Some(&b) => bytes.push(b),
1080 None => {
1081 let mut buf = [0u8; 4];
1082 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
1083 }
1084 }
1085 }
1086 bytes
1087 }
1088
1089 pub fn special_texts(&self) -> Vec<String> {
1091 self.added
1092 .iter()
1093 .filter(|(_, id)| self.special_ids.contains(id))
1094 .map(|(text, _)| text.clone())
1095 .collect()
1096 }
1097
1098 pub fn decode_token_for_hash(&self, id: u32) -> String {
1101 let idx = id as usize;
1102 if idx >= self.id_to_token.len() {
1103 return String::new();
1104 }
1105 if self.special_ids.contains(&id) {
1106 return self.id_to_token[idx].clone();
1107 }
1108 self.decode_token(id)
1109 }
1110
1111 pub fn decode_for_protocol(&self, ids: &[u32]) -> String {
1114 let mut out = String::new();
1115 for &id in ids {
1116 let idx = id as usize;
1117 if self.special_ids.contains(&id) {
1118 if let Some(token) = self.id_to_token.get(idx) {
1119 out.push_str(token);
1120 }
1121 } else {
1122 out.push_str(&self.decode_token(id));
1123 }
1124 }
1125 out
1126 }
1127
1128 pub fn raw_token_for_hash(&self, id: u32) -> String {
1130 self.id_to_token
1131 .get(id as usize)
1132 .cloned()
1133 .unwrap_or_default()
1134 }
1135
1136 pub fn apply_chat_template(&self, messages: &[(String, String)]) -> Vec<u32> {
1140 self.apply_chat_template_opts(messages, None)
1141 }
1142
1143 pub fn apply_chat_template_json(
1160 &self,
1161 messages: &[serde_json::Value],
1162 tools: Option<&[serde_json::Value]>,
1163 enable_thinking: Option<bool>,
1164 ) -> Vec<u32> {
1165 self.apply_chat_template_json_with_reasoning_effort(messages, tools, enable_thinking, None)
1166 }
1167
1168 pub fn apply_chat_template_json_with_reasoning_effort(
1171 &self,
1172 messages: &[serde_json::Value],
1173 tools: Option<&[serde_json::Value]>,
1174 enable_thinking: Option<bool>,
1175 reasoning_effort: Option<&str>,
1176 ) -> Vec<u32> {
1177 match self.try_apply_chat_template_json_with_reasoning_effort(
1178 messages,
1179 tools,
1180 enable_thinking,
1181 reasoning_effort,
1182 ) {
1183 Ok(ids) => ids,
1184 Err(e) => {
1185 tracing::error!("chat template render failed ({e}); ChatML fallback");
1186 self.chatml_json_fallback(messages, enable_thinking)
1187 }
1188 }
1189 }
1190
1191 pub fn try_apply_chat_template_json(
1202 &self,
1203 messages: &[serde_json::Value],
1204 tools: Option<&[serde_json::Value]>,
1205 enable_thinking: Option<bool>,
1206 ) -> Result<Vec<u32>, String> {
1207 self.try_apply_chat_template_json_with_reasoning_effort(
1208 messages,
1209 tools,
1210 enable_thinking,
1211 None,
1212 )
1213 }
1214
1215 pub fn try_apply_chat_template_json_with_reasoning_effort(
1219 &self,
1220 messages: &[serde_json::Value],
1221 tools: Option<&[serde_json::Value]>,
1222 enable_thinking: Option<bool>,
1223 reasoning_effort: Option<&str>,
1224 ) -> Result<Vec<u32>, String> {
1225 if let Some(tpl) = &self.chat_template {
1226 return self
1227 .render_template_json(tpl, messages, tools, enable_thinking, reasoning_effort)
1228 .map(|text| self.with_bos(self.encode(&text)))
1229 .map_err(|e| format!("{e:#}"));
1230 }
1231 Ok(self.chatml_json_fallback(messages, enable_thinking))
1232 }
1233
1234 fn chatml_json_fallback(
1235 &self,
1236 messages: &[serde_json::Value],
1237 enable_thinking: Option<bool>,
1238 ) -> Vec<u32> {
1239 let pairs: Vec<(String, String)> = messages
1240 .iter()
1241 .map(|m| {
1242 (
1243 m.get("role")
1244 .and_then(|v| v.as_str())
1245 .unwrap_or("user")
1246 .to_string(),
1247 m.get("content")
1248 .and_then(|v| v.as_str())
1249 .unwrap_or("")
1250 .to_string(),
1251 )
1252 })
1253 .collect();
1254 self.with_bos(self.chatml_fallback_opts(&pairs, enable_thinking))
1255 }
1256
1257 pub fn render_chat_json(
1259 &self,
1260 messages: &[serde_json::Value],
1261 tools: Option<&[serde_json::Value]>,
1262 enable_thinking: Option<bool>,
1263 ) -> Option<String> {
1264 self.render_chat_json_with_reasoning_effort(messages, tools, enable_thinking, None)
1265 }
1266
1267 pub fn render_chat_json_with_reasoning_effort(
1270 &self,
1271 messages: &[serde_json::Value],
1272 tools: Option<&[serde_json::Value]>,
1273 enable_thinking: Option<bool>,
1274 reasoning_effort: Option<&str>,
1275 ) -> Option<String> {
1276 let tpl = self.chat_template.as_ref()?;
1277 match self.render_template_json(tpl, messages, tools, enable_thinking, reasoning_effort) {
1278 Ok(t) => Some(t),
1279 Err(e) => {
1280 tracing::error!("chat template render (json): {e:#}");
1281 eprintln!("chat template render (json): {e:#}");
1282 None
1283 }
1284 }
1285 }
1286
1287 fn render_template_json(
1288 &self,
1289 tpl: &str,
1290 messages: &[serde_json::Value],
1291 tools: Option<&[serde_json::Value]>,
1292 enable_thinking: Option<bool>,
1293 reasoning_effort: Option<&str>,
1294 ) -> Result<String, minijinja::Error> {
1295 let mut env = crate::chat_template::environment();
1296 self.add_special_globals(&mut env);
1297 let tpl_src = strip_generation_tags(tpl);
1298 env.add_template("chat", &tpl_src)?;
1299 let msgs: Vec<minijinja::Value> = messages
1300 .iter()
1301 .map(minijinja::Value::from_serialize)
1302 .collect();
1303 let tools_v: Option<Vec<minijinja::Value>> =
1304 tools.map(|ts| ts.iter().map(minijinja::Value::from_serialize).collect());
1305 let tpl = env.get_template("chat")?;
1306 let rendered = match (tools_v, enable_thinking, reasoning_effort) {
1313 (Some(ts), Some(v), Some(effort)) => tpl.render(minijinja::context! {
1314 messages => msgs, tools => ts, add_generation_prompt => true, enable_thinking => v,
1315 reasoning_effort => effort,
1316 tool_call_format => "json",
1317 })?,
1318 (Some(ts), Some(v), None) => tpl.render(minijinja::context! {
1319 messages => msgs, tools => ts, add_generation_prompt => true, enable_thinking => v,
1320 reasoning_effort => if !v { Some("low") } else { None::<&str> },
1325 tool_call_format => "json",
1326 })?,
1327 (Some(ts), None, Some(effort)) => tpl.render(minijinja::context! {
1328 messages => msgs, tools => ts, add_generation_prompt => true,
1329 reasoning_effort => effort,
1330 tool_call_format => "json",
1331 })?,
1332 (Some(ts), None, None) => tpl.render(minijinja::context! {
1333 messages => msgs, tools => ts, add_generation_prompt => true,
1334 tool_call_format => "json",
1335 })?,
1336 (None, Some(v), Some(effort)) => tpl.render(minijinja::context! {
1337 messages => msgs, add_generation_prompt => true, enable_thinking => v,
1338 reasoning_effort => effort,
1339 tool_call_format => "json",
1340 })?,
1341 (None, Some(v), None) => tpl.render(minijinja::context! {
1342 messages => msgs, add_generation_prompt => true, enable_thinking => v,
1343 reasoning_effort => if !v { Some("low") } else { None::<&str> },
1345 tool_call_format => "json",
1346 })?,
1347 (None, None, Some(effort)) => tpl.render(minijinja::context! {
1348 messages => msgs, add_generation_prompt => true,
1349 reasoning_effort => effort,
1350 tool_call_format => "json",
1351 })?,
1352 (None, None, None) => tpl.render(minijinja::context! {
1353 messages => msgs, add_generation_prompt => true,
1354 tool_call_format => "json",
1355 })?,
1356 };
1357 Ok(close_direct_think(rendered, enable_thinking))
1358 }
1359
1360 pub fn apply_chat_template_opts(
1361 &self,
1362 messages: &[(String, String)],
1363 enable_thinking: Option<bool>,
1364 ) -> Vec<u32> {
1365 if let Some(tpl) = &self.chat_template {
1366 match self.render_template(tpl, messages, enable_thinking) {
1367 Ok(text) => return self.with_bos(self.encode(&text)),
1368 Err(e) => {
1369 tracing::error!("chat template render failed ({e}); ChatML fallback");
1370 }
1371 }
1372 }
1373 self.with_bos(self.chatml_fallback_opts(messages, enable_thinking))
1374 }
1375
1376 pub fn with_bos(&self, mut ids: Vec<u32>) -> Vec<u32> {
1378 if self.add_bos {
1379 if let Some(b) = self.bos_token_id {
1380 if ids.first() != Some(&b) {
1381 ids.insert(0, b);
1382 }
1383 }
1384 }
1385 ids
1386 }
1387
1388 pub fn render_chat(&self, messages: &[(String, String)]) -> Option<String> {
1390 self.render_chat_opts(messages, None)
1391 }
1392
1393 pub fn render_chat_opts(
1395 &self,
1396 messages: &[(String, String)],
1397 enable_thinking: Option<bool>,
1398 ) -> Option<String> {
1399 let tpl = self.chat_template.as_ref()?;
1400 match self.render_template(tpl, messages, enable_thinking) {
1401 Ok(t) => Some(t),
1402 Err(e) => {
1403 tracing::error!("chat template render: {e:#}");
1404 None
1405 }
1406 }
1407 }
1408
1409 fn render_template(
1410 &self,
1411 tpl: &str,
1412 messages: &[(String, String)],
1413 enable_thinking: Option<bool>,
1414 ) -> Result<String, minijinja::Error> {
1415 let mut env = crate::chat_template::environment();
1416 self.add_special_globals(&mut env);
1417 let tpl_src = strip_generation_tags(tpl);
1418 env.add_template("chat", &tpl_src)?;
1419 let msgs: Vec<minijinja::Value> = messages
1420 .iter()
1421 .map(|(role, content)| {
1422 minijinja::context! { role => role, content => content }
1423 })
1424 .collect();
1425 let rendered = match enable_thinking {
1428 Some(v) => env.get_template("chat")?.render(minijinja::context! {
1429 messages => msgs,
1430 add_generation_prompt => true,
1431 enable_thinking => v,
1432 reasoning_effort => if !v { Some("low") } else { None::<&str> },
1435 })?,
1436 None => env.get_template("chat")?.render(minijinja::context! {
1437 messages => msgs,
1438 add_generation_prompt => true,
1439 })?,
1440 };
1441 Ok(close_direct_think(rendered, enable_thinking))
1442 }
1443
1444 fn add_special_globals(&self, env: &mut minijinja::Environment<'_>) {
1450 let bos = self.bos_token_id.and_then(|id| {
1451 self.added
1452 .iter()
1453 .find(|(_, i)| *i == id)
1454 .map(|(text, _)| text.clone())
1455 });
1456 if let Some(text) = bos {
1457 env.add_global("bos_token", text);
1458 }
1459 }
1460
1461 fn chatml_fallback(&self, messages: &[(String, String)]) -> Vec<u32> {
1463 self.chatml_fallback_opts(messages, None)
1464 }
1465
1466 fn chatml_fallback_opts(
1468 &self,
1469 messages: &[(String, String)],
1470 enable_thinking: Option<bool>,
1471 ) -> Vec<u32> {
1472 let mut tokens = Vec::new();
1473
1474 for (role, content) in messages {
1475 if let Some(start_id) = self.im_start_id {
1477 tokens.push(start_id);
1478 }
1479 tokens.extend(self.encode(&format!("{}\n{}", role, content)));
1480 if let Some(end_id) = self.im_end_id {
1481 tokens.push(end_id);
1482 }
1483 tokens.extend(self.encode("\n"));
1484 }
1485
1486 if let Some(start_id) = self.im_start_id {
1488 tokens.push(start_id);
1489 }
1490 tokens.extend(self.encode("assistant\n"));
1491 if enable_thinking == Some(false) {
1492 tokens.extend(self.encode("<think>\n\n</think>\n\n"));
1493 }
1494
1495 tokens
1496 }
1497
1498 pub fn vocab_size(&self) -> usize {
1500 self.id_to_token.len()
1501 }
1502
1503 pub fn token_to_id(&self, token: &str) -> Option<u32> {
1507 self.vocab.get(token).copied()
1508 }
1509
1510 pub fn convert_tokens_to_ids(&self, token: &str) -> Option<u32> {
1513 self.token_to_id(token)
1514 }
1515
1516 pub fn is_eos(&self, id: u32) -> bool {
1518 self.eos_token_id == Some(id) || self.im_end_id == Some(id) || self.extra_eos.contains(&id)
1519 }
1520}
1521
1522#[derive(Debug, thiserror::Error)]
1523pub enum TokenizerError {
1524 #[error("IO error: {0}")]
1525 Io(String),
1526 #[error("Parse error: {0}")]
1527 Parse(String),
1528}
1529
1530struct SpMerges {
1532 pairs: HashMap<u64, (u32, u32)>,
1533 bytes: [u32; 256],
1535}
1536
1537const SP_NONE: u32 = u32::MAX;
1538
1539thread_local! {
1540 static SP_STRINGS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1542}
1543
1544impl SpMerges {
1545 fn build(vocab: &HashMap<String, u32>, ranks: &HashMap<(String, String), u32>) -> Option<Self> {
1546 if ranks.is_empty() {
1547 return None;
1548 }
1549 let mut pairs = HashMap::with_capacity(ranks.len());
1550 let mut cat = String::new();
1551 for ((a, b), &r) in ranks {
1552 let (&ia, &ib) = (vocab.get(a)?, vocab.get(b)?);
1553 cat.clear();
1554 cat.push_str(a);
1555 cat.push_str(b);
1556 let &m = vocab.get(&cat)?;
1557 pairs.insert(((ia as u64) << 32) | ib as u64, (r, m));
1558 }
1559 let mut bytes = [SP_NONE; 256];
1560 for (b, slot) in bytes.iter_mut().enumerate() {
1561 if let Some(&id) = vocab.get(&format!("<0x{b:02X}>")) {
1562 *slot = id;
1563 }
1564 }
1565 Some(SpMerges { pairs, bytes })
1566 }
1567
1568 #[inline]
1569 fn rank(&self, a: u32, b: u32) -> Option<(u32, u32)> {
1570 if a == SP_NONE || b == SP_NONE {
1571 return None;
1572 }
1573 self.pairs.get(&(((a as u64) << 32) | b as u64)).copied()
1574 }
1575
1576 fn encode(&self, vocab: &HashMap<String, u32>, piece: &str, out: &mut Vec<u32>) {
1581 use std::cmp::Reverse;
1582 use std::collections::BinaryHeap;
1583 let spans: Vec<(usize, usize)> = piece
1584 .char_indices()
1585 .map(|(i, c)| (i, i + c.len_utf8()))
1586 .collect();
1587 let n = spans.len();
1588 let mut buf = [0u8; 4];
1589 let mut id: Vec<u32> = piece
1590 .chars()
1591 .map(|c| *vocab.get(c.encode_utf8(&mut buf) as &str).unwrap_or(&SP_NONE))
1592 .collect();
1593 let mut next: Vec<usize> = (1..=n).collect();
1594 let mut prev: Vec<usize> = (0..n).map(|i| i.wrapping_sub(1)).collect();
1595 let mut alive = vec![true; n];
1596 let mut heap: BinaryHeap<Reverse<(u32, usize)>> = BinaryHeap::with_capacity(n);
1597 for i in 0..n.saturating_sub(1) {
1598 if let Some((r, _)) = self.rank(id[i], id[i + 1]) {
1599 heap.push(Reverse((r, i)));
1600 }
1601 }
1602 let mut batch: Vec<usize> = Vec::new();
1603 let mut merged: Vec<usize> = Vec::new();
1604 while let Some(&Reverse((r, _))) = heap.peek() {
1605 batch.clear();
1606 while let Some(&Reverse((r2, pos))) = heap.peek() {
1607 if r2 != r {
1608 break;
1609 }
1610 heap.pop();
1611 batch.push(pos);
1612 }
1613 batch.sort_unstable();
1614 batch.dedup();
1615 merged.clear();
1616 for &pos in &batch {
1617 if !alive[pos] || next[pos] >= n {
1618 continue;
1619 }
1620 let j = next[pos];
1621 match self.rank(id[pos], id[j]) {
1622 Some((rr, m)) if rr == r => {
1623 id[pos] = m;
1624 alive[j] = false;
1625 next[pos] = next[j];
1626 if next[j] < n {
1627 prev[next[j]] = pos;
1628 }
1629 merged.push(pos);
1630 }
1631 _ => {}
1632 }
1633 }
1634 for &pos in &merged {
1635 if !alive[pos] {
1636 continue;
1637 }
1638 let pv = prev[pos];
1639 if pv < n && alive[pv] {
1640 if let Some((r2, _)) = self.rank(id[pv], id[pos]) {
1641 heap.push(Reverse((r2, pv)));
1642 }
1643 }
1644 let nx = next[pos];
1645 if nx < n {
1646 if let Some((r2, _)) = self.rank(id[pos], id[nx]) {
1647 heap.push(Reverse((r2, pos)));
1648 }
1649 }
1650 }
1651 }
1652 let mut i = 0usize;
1653 while i < n {
1654 if id[i] != SP_NONE {
1655 out.push(id[i]);
1656 } else {
1657 let (a, b) = spans[i];
1659 let mut ok = true;
1660 for &byte in piece[a..b].as_bytes() {
1661 match self.bytes[byte as usize] {
1662 SP_NONE => {
1663 ok = false;
1664 break;
1665 }
1666 t => out.push(t),
1667 }
1668 }
1669 if !ok {
1670 tracing::error!("tokenizer: no id for SP symbol {:?} — dropped", &piece[a..b]);
1671 }
1672 }
1673 i = next[i];
1674 }
1675 }
1676}
1677
1678#[cfg(test)]
1679mod tests {
1680 use super::*;
1681
1682 #[test]
1683 fn byte_unicode_bijection() {
1684 let (b2c, c2b) = bytes_to_unicode();
1685 for b in 0..=255u8 {
1686 assert_eq!(c2b[&b2c[b as usize]], b);
1687 }
1688 assert_eq!(b2c[b' ' as usize], 'Ġ');
1690 assert_eq!(b2c[b'\n' as usize], 'Ċ');
1691 }
1692
1693 #[test]
1694 fn byte_level_roundtrip_utf8() {
1695 let tok = Tokenizer::byte_level();
1696 let text = "hello 🌍 hi\n";
1697 let ids = tok.encode(text);
1698 assert_eq!(ids.len(), text.len()); assert_eq!(tok.decode(&ids), text);
1700 }
1701
1702 fn mini_json() -> String {
1705 let vocab: Vec<(&str, u32)> = vec![
1707 ("h", 0),
1708 ("e", 1),
1709 ("l", 2),
1710 ("o", 3),
1711 ("Ġ", 4),
1712 ("w", 5),
1713 ("r", 6),
1714 ("d", 7),
1715 ("he", 8),
1716 ("Ġw", 9),
1717 ];
1718 let vocab_json: String = vocab
1719 .iter()
1720 .map(|(t, i)| format!("\"{t}\": {i}"))
1721 .collect::<Vec<_>>()
1722 .join(", ");
1723 format!(
1724 r#"{{
1725 "model": {{
1726 "type": "BPE",
1727 "vocab": {{ {vocab_json} }},
1728 "merges": [["h", "e"], ["Ġ", "w"]]
1729 }},
1730 "added_tokens": [
1731 {{"id": 10, "content": "<|eot|>", "special": true}}
1732 ]
1733 }}"#
1734 )
1735 }
1736
1737 #[test]
1743 fn sp_heap_bpe_matches_the_string_form() {
1744 let toks = [
1745 "a", "b", "c", "▁", "bc", "abc", "ab", "aa", "aab", "ca", "▁a", "aabc",
1746 "aaa", "<0x64>", "<0xC3>", "▁▁",
1747 ];
1748 let vocab: Vec<String> = toks
1749 .iter()
1750 .enumerate()
1751 .map(|(i, t)| format!("\"{t}\": {i}"))
1752 .collect();
1753 let merges = [
1754 ("aab", "c"),
1755 ("b", "c"),
1756 ("a", "bc"),
1757 ("a", "b"),
1758 ("ab", "c"),
1759 ("a", "a"),
1760 ("aa", "b"),
1761 ("c", "a"),
1762 ("▁", "a"),
1763 ("aa", "a"),
1764 ("▁", "▁"),
1765 ];
1766 let merges: Vec<String> = merges
1767 .iter()
1768 .map(|(a, b)| format!("[\"{a}\", \"{b}\"]"))
1769 .collect();
1770 let json = format!(
1771 r#"{{"model": {{"type": "BPE", "byte_fallback": true, "vocab": {{ {} }}, "merges": [{}]}},
1772 "normalizer": {{"type": "Replace", "pattern": {{"String": " "}}, "content": "\u2581"}},
1773 "added_tokens": []}}"#,
1774 vocab.join(", "),
1775 merges.join(", ")
1776 );
1777 let t = Tokenizer::from_json(&json).unwrap();
1778 assert!(t.metaspace, "a byte_fallback vocabulary takes the SentencePiece path");
1779 assert!(t.sp_merges().is_some());
1780 let alphabet = ['a', 'b', 'c', '\u{2581}', 'd', 'é', 'a', 'a', 'b'];
1781 let mut state = 0x2545_f491_4f6c_dd1du64;
1782 for len in 0..40 {
1783 for _ in 0..60 {
1784 let piece: String = (0..len)
1785 .map(|_| {
1786 state ^= state << 13;
1787 state ^= state >> 7;
1788 state ^= state << 17;
1789 alphabet[(state % alphabet.len() as u64) as usize]
1790 })
1791 .collect();
1792 let (mut fast, mut slow) = (Vec::new(), Vec::new());
1793 t.sp_merges().unwrap().encode(&t.vocab, &piece, &mut fast);
1794 t.bpe_piece_sp_strings(&piece, &mut slow);
1795 assert_eq!(fast, slow, "{piece:?}");
1796 }
1797 }
1798 }
1799
1800 #[test]
1804 fn tool_markup_decodes_even_when_special() {
1805 let json = r#"{
1806 "model": {"type": "BPE", "vocab": {"h": 0, "e": 1, "l": 2, "o": 3}, "merges": []},
1807 "added_tokens": [
1808 {"id": 10, "content": "<|im_end|>", "special": true},
1809 {"id": 11, "content": "<function", "special": true},
1810 {"id": 12, "content": "</function>", "special": true},
1811 {"id": 13, "content": "<param", "special": true},
1812 {"id": 14, "content": "</param>", "special": true},
1813 {"id": 15, "content": "<tool_call>", "special": true}
1814 ]
1815 }"#;
1816 let t = Tokenizer::from_json(json).unwrap();
1817 let ids = [11, 0, 1, 13, 2, 14, 12, 15, 10];
1818 assert_eq!(
1819 t.decode(&ids),
1820 "<functionhe<paraml</param></function><tool_call>"
1821 );
1822 let streamed: String = ids.iter().map(|&i| t.decode_token(i)).collect();
1823 assert_eq!(streamed, t.decode(&ids), "streaming must agree with decode");
1824 assert!(
1825 !t.decode(&[10]).contains("im_end"),
1826 "control tokens stay hidden"
1827 );
1828 }
1829
1830 #[test]
1833 fn real_tokenizer_parity_when_available() {
1834 let Ok(path) = std::env::var("CMF_TOK_PARITY") else {
1835 return;
1836 };
1837 let t = Tokenizer::from_file(&path).expect("load");
1838 for (text, want) in [
1839 (
1840 "The capital of France is",
1841 vec![671u32, 6102, 294, 8760, 344],
1842 ),
1843 ("2 + 2 =", vec![20, 940, 223, 20, 438]),
1844 ] {
1845 let got = t.encode(text);
1846 assert_eq!(got, want, "«{text}»");
1847 }
1848 }
1849
1850 #[test]
1855 fn granite_42_chat_template_when_available() {
1856 let Ok(path) = std::env::var("CMF_GRANITE_CHAT_TEMPLATE") else {
1857 return;
1858 };
1859 let mut tok = Tokenizer::byte_level();
1860 tok.chat_template = Some(std::fs::read_to_string(path).expect("read Granite template"));
1861 let messages = vec![("user".to_string(), "Hello".to_string())];
1862
1863 let thinking = tok
1864 .render_chat_opts(&messages, Some(true))
1865 .expect("render Granite thinking prompt");
1866 assert_eq!(
1867 thinking,
1868 "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\n<think>\n"
1869 );
1870
1871 let direct = tok
1872 .render_chat_opts(&messages, Some(false))
1873 .expect("render Granite direct prompt");
1874 assert_eq!(
1875 direct,
1876 "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\n<think></think>"
1877 );
1878 }
1879
1880 #[test]
1881 fn json_template_passes_explicit_reasoning_effort_without_overriding_it() {
1882 let mut tok = Tokenizer::byte_level();
1883 tok.chat_template = Some(
1884 "{% if reasoning_effort is defined %}<effort>{{ reasoning_effort }}</effort>{% else %}<default-effort>{% endif %}|{{ enable_thinking }}|{{ messages[0].reasoning_content }}"
1885 .to_string(),
1886 );
1887 let messages = vec![serde_json::json!({
1888 "role": "assistant",
1889 "content": "answer",
1890 "reasoning_content": "prior chain"
1891 })];
1892 let explicit = tok
1893 .render_chat_json_with_reasoning_effort(&messages, None, Some(false), Some("high"))
1894 .expect("render explicit effort");
1895 assert_eq!(explicit, "<effort>high</effort>|False|prior chain");
1896
1897 let native_default = tok
1898 .render_chat_json_with_reasoning_effort(&messages, None, None, None)
1899 .expect("render native default");
1900 assert_eq!(native_default, "<default-effort>||prior chain");
1901 }
1902
1903 #[test]
1904 fn fallible_json_template_render_does_not_use_chatml_on_jinja_error() {
1905 let mut tok = Tokenizer::byte_level();
1906 tok.chat_template = Some("{% for message in messages %}".to_string());
1907 let messages = vec![serde_json::json!({"role": "user", "content": "hello"})];
1908 let error = tok
1909 .try_apply_chat_template_json_with_reasoning_effort(
1910 &messages,
1911 None,
1912 None,
1913 Some("high"),
1914 )
1915 .expect_err("unclosed Jinja must fail instead of producing ChatML");
1916 assert!(!error.is_empty());
1917 }
1918
1919 #[test]
1925 fn minimax_m3_chat_template_when_available() {
1926 let Ok(path) = std::env::var("CMF_MINIMAX_M3_CHAT_TEMPLATE") else {
1927 return;
1928 };
1929 let mut tok = Tokenizer::byte_level();
1930 tok.chat_template = Some(std::fs::read_to_string(path).expect("read MiniMax M3 template"));
1931 let messages = vec![
1932 serde_json::json!({"role": "system", "content": "Keep answers concise."}),
1933 serde_json::json!({"role": "user", "content": "What is 2 + 2?"}),
1934 ];
1935 let tools = vec![serde_json::json!({
1936 "type": "function",
1937 "function": {
1938 "name": "get_weather",
1939 "description": "Return the weather for a city.",
1940 "parameters": {
1941 "type": "object",
1942 "properties": {"city": {"type": "string"}},
1943 "required": ["city"]
1944 }
1945 }
1946 })];
1947 let rendered = tok
1948 .render_chat_json(&messages, Some(&tools), Some(false))
1949 .expect("render MiniMax M3 native prompt");
1950 assert!(rendered.starts_with("]~!b[]~b]system\n"), "{rendered:?}");
1951 assert!(rendered.contains("<effort>low</effort>"), "{rendered:?}");
1952 assert!(
1953 rendered.contains("]<]minimax[>[<tool_call>"),
1954 "{rendered:?}"
1955 );
1956 assert!(rendered.contains("get_weather"), "{rendered:?}");
1957 assert!(rendered.ends_with("]~b]ai\n"), "{rendered:?}");
1958 }
1959
1960 #[test]
1964 fn glm_direct_template_keeps_assistant_special_token_intact() {
1965 let mut tok = Tokenizer::byte_level();
1966 tok.chat_template = Some(
1967 "[gMASK]<sop>{%- set effort = reasoning_effort if reasoning_effort is defined and reasoning_effort in ['low', 'high'] else 'max' -%}<|system|>Reasoning Effort: {{ effort | capitalize }}{%- for m in messages -%}<|user|>{{ m.content }}{%- endfor -%}<|assistant|><think>"
1968 .to_string(),
1969 );
1970 let messages = vec![("user".to_string(), "2+2?".to_string())];
1971 let rendered = tok
1972 .render_chat_opts(&messages, Some(false))
1973 .expect("render GLM direct prompt");
1974 assert!(rendered.contains("Reasoning Effort: Low"));
1975 assert!(rendered.contains("<|assistant|><think></think>"));
1976 assert!(!rendered.contains("<|assistant\n"));
1977
1978 let thinking = tok
1981 .render_chat_opts(&messages, Some(true))
1982 .expect("render GLM thinking prompt");
1983 assert!(thinking.contains("Reasoning Effort: Max"));
1984
1985 let json_messages = vec![serde_json::json!({
1986 "role": "user",
1987 "content": "2+2?"
1988 })];
1989 let rendered_json = tok
1990 .render_chat_json(&json_messages, None, Some(false))
1991 .expect("render GLM JSON direct prompt");
1992 assert!(rendered_json.contains("Reasoning Effort: Low"));
1993 assert!(rendered_json.contains("<|assistant|><think></think>"));
1994 assert!(!rendered_json.contains("<|assistant\n"));
1995 }
1996
1997 #[test]
2003 fn every_split_in_a_sequence_is_applied() {
2004 let pt = serde_json::json!({
2005 "type": "Sequence",
2006 "pretokenizers": [
2007 {"type": "Split", "behavior": "Isolated",
2008 "pattern": {"Regex": r"\p{N}{1,3}"}},
2009 {"type": "Split", "behavior": "Isolated",
2010 "pattern": {"Regex": r" ?[\p{L}]+"}},
2011 {"type": "ByteLevel", "add_prefix_space": false, "use_regex": false}
2012 ]
2013 });
2014 let mut pats = Vec::new();
2015 collect_split_patterns(&pt, &mut pats);
2016 assert_eq!(
2017 pats.len(),
2018 2,
2019 "both Split stages must be collected: {pats:?}"
2020 );
2021 assert!(pats[0].contains("p{N}"), "digit rule first");
2022 assert!(pats[1].contains("p{L}"), "word rule second");
2023
2024 let re: Vec<fancy_regex::Regex> = pats
2028 .iter()
2029 .map(|p| fancy_regex::Regex::new(p).unwrap())
2030 .collect();
2031 let norm = "ab cd12";
2032 let mut pieces: Vec<(usize, usize)> = vec![(0, norm.len())];
2033 for r in &re {
2034 let mut next = Vec::new();
2035 for (ps, pe) in pieces {
2036 let seg = &norm[ps..pe];
2037 let mut last = 0;
2038 for m in r.find_iter(seg).flatten() {
2039 if m.start() > last {
2040 next.push((ps + last, ps + m.start()));
2041 }
2042 if m.end() > m.start() {
2043 next.push((ps + m.start(), ps + m.end()));
2044 }
2045 last = m.end();
2046 }
2047 if last < seg.len() {
2048 next.push((ps + last, pe));
2049 }
2050 }
2051 pieces = next;
2052 }
2053 let got: Vec<&str> = pieces.iter().map(|(a, b)| &norm[*a..*b]).collect();
2054 assert_eq!(
2055 got,
2056 vec!["ab", " cd", "12"],
2057 "staged split produced {got:?}"
2058 );
2059 }
2060
2061 #[test]
2062 fn full_pipeline_merges_and_added_tokens() {
2063 let tok = Tokenizer::from_json(&mini_json()).unwrap();
2064 let ids = tok.encode("hello world");
2066 assert_eq!(ids, vec![8, 2, 2, 3, 9, 3, 6, 2, 7]);
2067 assert_eq!(tok.decode(&ids), "hello world");
2068 let ids2 = tok.encode("he<|eot|>he");
2070 assert_eq!(ids2, vec![8, 10, 8]);
2071 assert_eq!(tok.decode(&ids2), "hehe");
2072 }
2073
2074 #[test]
2079 fn chat_template_sees_bos_token_once() {
2080 let mut tok = Tokenizer::from_json(&mini_json()).unwrap();
2081 tok.bos_token_id = Some(10);
2082 tok.chat_template = Some(
2083 "{{- bos_token -}}{%- for m in messages -%}{{ m.content }}{%- endfor -%}".to_string(),
2084 );
2085 let messages = vec![("user".to_string(), "he".to_string())];
2086 assert_eq!(tok.apply_chat_template(&messages), vec![10, 8]);
2087 let json = vec![serde_json::json!({"role": "user", "content": "he"})];
2088 assert_eq!(tok.apply_chat_template_json(&json, None, None), vec![10, 8]);
2089 tok.add_bos = true;
2090 assert_eq!(tok.apply_chat_template(&messages), vec![10, 8]);
2091 }
2092
2093 #[test]
2094 fn non_ascii_is_never_silently_dropped() {
2095 let tok = Tokenizer::from_json(&mini_json()).unwrap();
2096 let ids = tok.encode("hello");
2099 assert!(!ids.is_empty());
2100 }
2101}
2102
2103#[cfg(test)]
2104mod generation_tag_tests {
2105 use super::strip_generation_tags;
2106
2107 #[test]
2112 fn a_generation_block_becomes_a_no_op_keeping_its_whitespace_control() {
2113 let tpl = "a{%- generation -%}b{%- endgeneration -%}c";
2114 let out = strip_generation_tags(tpl);
2115 assert!(!out.contains("{%- generation"));
2116 assert!(!out.contains("endgeneration"));
2117 assert_eq!(out.matches("{%-").count(), 2);
2119 assert_eq!(out.matches("-%}").count(), 2);
2120 assert!(out.starts_with('a') && out.ends_with('c'));
2121 }
2122
2123 #[test]
2126 fn each_side_keeps_its_own_dash() {
2127 let out = strip_generation_tags("{% generation %}x{%- endgeneration %}");
2128 assert!(out.starts_with("{% set"), "no dash added on the left");
2129 assert!(out.contains("{%- set"), "the right tag keeps its dash");
2130 assert!(!out.contains("-%}"), "no trailing dash invented");
2131 }
2132
2133 #[test]
2136 fn everything_else_is_left_alone() {
2137 let plain = "{%- if x -%}{{ y }}{%- endif -%}";
2138 assert_eq!(strip_generation_tags(plain), plain);
2139 let prose = "{{ 'the generation of tokens' }}";
2141 assert_eq!(strip_generation_tags(prose), prose);
2142 }
2143}