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 sp_prepend: bool,
67 sp_prepend_first: bool,
73 metaspace: bool,
76 nfc: bool,
79 byte_to_char: [char; 256],
81 char_to_byte: HashMap<char, u8>,
83 pub bos_token_id: Option<u32>,
85 pub eos_token_id: Option<u32>,
86 pub pad_token_id: Option<u32>,
87 pub im_start_id: Option<u32>,
89 pub im_end_id: Option<u32>,
90 pub chat_template: Option<String>,
93 pub extra_eos: HashSet<u32>,
95 pub add_bos: bool,
97}
98
99impl std::fmt::Debug for Tokenizer {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 f.debug_struct("Tokenizer")
102 .field("vocab", &self.vocab.len())
103 .field("merges", &self.ranks.len())
104 .field("added", &self.added.len())
105 .finish()
106 }
107}
108
109fn bytes_to_unicode() -> ([char; 256], HashMap<char, u8>) {
112 let mut b2c = ['\0'; 256];
113 let mut c2b = HashMap::with_capacity(256);
114 let mut n = 0u32;
115 for b in 0..=255u16 {
116 let printable =
117 (0x21..=0x7E).contains(&b) || (0xA1..=0xAC).contains(&b) || (0xAE..=0xFF).contains(&b);
118 let c = if printable {
119 char::from_u32(b as u32).unwrap()
120 } else {
121 let c = char::from_u32(256 + n).unwrap();
122 n += 1;
123 c
124 };
125 b2c[b as usize] = c;
126 c2b.insert(c, b as u8);
127 }
128 (b2c, c2b)
129}
130
131#[derive(Deserialize)]
133struct HfTokenizerJson {
134 model: HfModel,
135 #[serde(default)]
136 added_tokens: Vec<HfAddedToken>,
137 #[serde(default)]
138 pre_tokenizer: Option<serde_json::Value>,
139 #[serde(default)]
140 normalizer: Option<serde_json::Value>,
141 #[serde(default)]
142 post_processor: Option<serde_json::Value>,
143}
144
145#[derive(Deserialize)]
146struct HfModel {
147 vocab: HashMap<String, u32>,
148 #[serde(default)]
149 merges: Vec<HfMerge>,
150 #[serde(default)]
151 byte_fallback: bool,
152}
153
154#[derive(Deserialize)]
157#[serde(untagged)]
158enum HfMerge {
159 Pair([String; 2]),
160 Text(String),
161}
162
163#[derive(Deserialize)]
164struct HfAddedToken {
165 id: u32,
166 content: String,
167 special: bool,
168}
169
170fn collect_split_patterns(pt: &serde_json::Value, out: &mut Vec<String>) {
179 if pt.get("type").and_then(|t| t.as_str()) == Some("Split") {
180 if let Some(r) = pt
181 .get("pattern")
182 .and_then(|p| p.get("Regex"))
183 .and_then(|r| r.as_str())
184 {
185 out.push(r.to_string());
186 }
187 return;
188 }
189 if let Some(list) = pt.get("pretokenizers").and_then(|l| l.as_array()) {
190 for p in list {
191 collect_split_patterns(p, out);
192 }
193 }
194}
195
196fn find_prepend_scheme(pt: &serde_json::Value) -> Option<String> {
199 if pt.get("type").and_then(|t| t.as_str()) == Some("Metaspace") {
200 return pt
201 .get("prepend_scheme")
202 .and_then(|p| p.as_str())
203 .map(String::from);
204 }
205 if let Some(list) = pt.get("pretokenizers").and_then(|l| l.as_array()) {
206 return list.iter().find_map(find_prepend_scheme);
207 }
208 None
209}
210
211pub(crate) fn strip_generation_tags(tpl: &str) -> std::borrow::Cow<'_, str> {
227 if !tpl.contains("generation") {
228 return std::borrow::Cow::Borrowed(tpl);
229 }
230 let mut out = String::with_capacity(tpl.len());
231 let mut rest = tpl;
232 let mut touched = false;
233 while let Some(open) = rest.find("{%") {
234 let Some(close_rel) = rest[open..].find("%}") else {
235 break;
236 };
237 let close = open + close_rel + 2;
238 let tag = &rest[open..close];
239 let inner = tag[2..tag.len() - 2].trim();
240 let lead = inner.starts_with('-');
241 let trail = inner.ends_with('-');
242 let name = inner.trim_matches('-').trim();
243 out.push_str(&rest[..open]);
244 if name == "generation" || name == "endgeneration" {
245 out.push_str(if lead { "{%-" } else { "{%" });
246 out.push_str(" set _generation_span = true ");
247 out.push_str(if trail { "-%}" } else { "%}" });
248 touched = true;
249 } else {
250 out.push_str(tag);
251 }
252 rest = &rest[close..];
253 }
254 if !touched {
255 return std::borrow::Cow::Borrowed(tpl);
256 }
257 out.push_str(rest);
258 std::borrow::Cow::Owned(out)
259}
260
261fn close_direct_think(rendered: String, enable_thinking: Option<bool>) -> String {
270 if enable_thinking != Some(false) || rendered.contains("</think>") {
271 return rendered;
272 }
273 if let Some(pos) = rendered.rfind("<|assistant|>") {
274 let marker_end = pos + "<|assistant|>".len();
275 let after = &rendered[marker_end..];
276 let mut out = String::with_capacity(rendered.len() + 24);
277 out.push_str(&rendered[..marker_end]);
278 if let Some(rest) = after.strip_prefix("<think>") {
279 out.push_str("<think></think>\n\n");
280 out.push_str(rest);
281 } else {
282 out.push_str(after);
283 if !out.ends_with('\n') {
284 out.push('\n');
285 }
286 out.push_str("<think>\n\n</think>\n\n");
287 }
288 return out;
289 }
290 if let Some(pos) = rendered.rfind("\nassistant") {
294 let mut insert_at = pos + 1 + "assistant".len();
295 if let Some(idx) = rendered[insert_at..].find('\n') {
296 insert_at += idx + 1;
297 }
298 let mut out = String::with_capacity(rendered.len() + 24);
299 out.push_str(&rendered[..insert_at]);
300 if !out.ends_with('\n') {
301 out.push('\n');
302 }
303 out.push_str("<think>\n\n</think>\n\n");
304 out.push_str(&rendered[insert_at..]);
305 return out;
306 }
307 rendered
308}
309
310impl Tokenizer {
311 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, TokenizerError> {
313 let data = std::fs::read_to_string(path.as_ref())
314 .map_err(|e| TokenizerError::Io(e.to_string()))?;
315 Self::from_json(&data)
316 }
317
318 pub fn from_bytes(bytes: &[u8]) -> Result<Self, TokenizerError> {
320 let s = std::str::from_utf8(bytes)
321 .map_err(|e| TokenizerError::Parse(format!("vocab is not UTF-8: {e}")))?;
322 Self::from_json(s)
323 }
324
325 pub fn from_json(json: &str) -> Result<Self, TokenizerError> {
327 let hf: HfTokenizerJson =
328 serde_json::from_str(json).map_err(|e| TokenizerError::Parse(e.to_string()))?;
329
330 let mut vocab = hf.model.vocab;
331 let mut ranks = HashMap::new();
332 for (rank, m) in hf.model.merges.into_iter().enumerate() {
333 let (a, b) = match m {
334 HfMerge::Pair([a, b]) => (a, b),
335 HfMerge::Text(s) => {
336 let mut it = s.splitn(2, ' ');
337 match (it.next(), it.next()) {
338 (Some(a), Some(b)) => (a.to_string(), b.to_string()),
339 _ => continue,
340 }
341 }
342 };
343 ranks.insert((a, b), rank as u32);
344 }
345
346 let mut saw_gemma_bos = false;
351 let add_bos_detected = hf
352 .post_processor
353 .as_ref()
354 .map(|p| {
355 let pp = p.to_string();
356 pp.contains("\"<s>\"") || pp.contains("\"<bos>\"")
357 })
358 .unwrap_or(false);
359 let nfc = hf
360 .normalizer
361 .as_ref()
362 .map(|n| n.to_string().contains("NFC"))
363 .unwrap_or(false);
364 let metaspace = hf.model.byte_fallback
365 || hf
366 .normalizer
367 .as_ref()
368 .map(|n| n.to_string().contains("\u{2581}") || n.to_string().contains("▁"))
369 .unwrap_or(false);
370 let sp_prepend = hf
371 .normalizer
372 .as_ref()
373 .map(|n| n.to_string().contains("Prepend"))
374 .unwrap_or(false);
375 let (sp_prepend, sp_prepend_first) = if sp_prepend {
379 (true, false)
380 } else {
381 match hf.pre_tokenizer.as_ref().and_then(find_prepend_scheme) {
382 Some(s) if s == "always" => (true, false),
383 Some(s) if s == "first" => (false, true),
384 _ => (false, false),
385 }
386 };
387 let split_res = if metaspace {
388 Vec::new()
389 } else {
390 let mut pats = Vec::new();
391 if let Some(pt) = hf.pre_tokenizer.as_ref() {
392 collect_split_patterns(pt, &mut pats);
393 }
394 if pats.is_empty() {
395 pats.push(DEFAULT_SPLIT.to_string());
396 }
397 pats.iter()
398 .map(|p| {
399 fancy_regex::Regex::new(p)
400 .map_err(|e| TokenizerError::Parse(format!("pre-tokenizer regex: {e}")))
401 })
402 .collect::<Result<Vec<_>, _>>()?
403 };
404
405 let mut bos_token_id = None;
407 let mut eos_token_id = None;
408 let mut pad_token_id = None;
409 let mut im_start_id = None;
410 let mut im_end_id = None;
411 let mut special_ids = HashSet::new();
412 let mut added_ids = HashSet::new();
413 let mut added = Vec::new();
414
415 for at in &hf.added_tokens {
416 vocab.insert(at.content.clone(), at.id);
417 added.push((at.content.clone(), at.id));
418 added_ids.insert(at.id);
419 if at.special && !TOOL_MARKUP_TOKENS.contains(&at.content.as_str()) {
420 special_ids.insert(at.id);
421 }
422 match at.content.as_str() {
423 "<|endoftext|>" | "</s>" | "[EOS]" => eos_token_id = Some(at.id),
424 "<|im_start|>" => im_start_id = Some(at.id),
425 "<|im_end|>" => im_end_id = Some(at.id),
426 "<s>" | "[BOS]" => bos_token_id = Some(at.id),
427 "<bos>" => {
431 bos_token_id = Some(at.id);
432 saw_gemma_bos = true;
433 }
434 "<pad>" => pad_token_id = Some(at.id),
435 _ => {}
436 }
437 }
438
439 const DSV41_SPECIALS: &[&str] = &[
444 "<|begin▁of▁sentence|>",
445 "<|end▁of▁sentence|>",
446 "<|User|>",
447 "<|Assistant|>",
448 "<|System|>",
449 "<|latest_reminder|>",
450 "<|deepseek_image|>",
451 "<|action|>",
452 "<|query|>",
453 "<|authority|>",
454 "<|domain|>",
455 "<|title|>",
456 "<|read_url|>",
457 "<think>",
458 "</think>",
459 "|DSML|",
460 ];
461 for token in DSV41_SPECIALS {
462 if let Some(&id) = vocab.get(*token) {
463 if !added.iter().any(|(content, _)| content.as_str() == *token) {
464 added.push(((*token).to_string(), id));
465 }
466 added_ids.insert(id);
467 match *token {
468 "<|begin▁of▁sentence|>" => bos_token_id = Some(id),
469 "<|end▁of▁sentence|>" => eos_token_id = Some(id),
470 _ => {}
471 }
472 }
473 }
474 added.sort_by_key(|(c, _)| std::cmp::Reverse(c.len()));
475
476 let gemma_family = saw_gemma_bos
483 || vocab.contains_key("<start_of_turn>")
484 || added.iter().any(|(c, _)| c == "<start_of_turn>");
485
486 if let Some(pp) = hf.post_processor.as_ref() {
490 let pp = pp.to_string();
491 for name in ["<bos>", "<s>"] {
492 if pp.contains(&format!("\"{name}\"")) {
493 if let Some(&id) = vocab.get(name) {
494 bos_token_id = Some(id);
495 }
496 break;
497 }
498 }
499 }
500
501 let max_id = vocab.values().copied().max().unwrap_or(0) as usize;
503 let mut id_to_token = vec![String::new(); max_id + 1];
504 for (token, &id) in &vocab {
505 if (id as usize) < id_to_token.len() {
506 id_to_token[id as usize] = token.clone();
507 }
508 }
509
510 let (byte_to_char, char_to_byte) = bytes_to_unicode();
511
512 tracing::info!(
513 "Tokenizer loaded: {} vocab, {} merges, {} added, eos={:?}",
514 vocab.len(),
515 ranks.len(),
516 added.len(),
517 eos_token_id
518 );
519
520 Ok(Self {
521 vocab,
522 id_to_token,
523 ranks,
524 sp_ids: OnceLock::new(),
525 added,
526 added_ids,
527 special_ids,
528 split_res,
529 metaspace,
530 sp_prepend,
531 sp_prepend_first,
532 nfc,
533 byte_to_char,
534 char_to_byte,
535 bos_token_id,
536 eos_token_id,
537 pad_token_id,
538 im_start_id,
539 im_end_id,
540 chat_template: None,
541 extra_eos: HashSet::new(),
542 add_bos: add_bos_detected || gemma_family,
543 })
544 }
545
546 pub fn byte_level() -> Self {
548 let mut vocab = HashMap::new();
549 let mut id_to_token = Vec::with_capacity(256);
550 for i in 0..256u32 {
551 let tok = format!("<0x{:02X}>", i);
552 vocab.insert(tok.clone(), i);
553 id_to_token.push(tok);
554 }
555 let (byte_to_char, char_to_byte) = bytes_to_unicode();
556 Self {
557 vocab,
558 id_to_token,
559 ranks: HashMap::new(),
560 sp_ids: OnceLock::new(),
561 added: Vec::new(),
562 added_ids: HashSet::new(),
563 special_ids: HashSet::new(),
564 split_res: Vec::new(),
565 metaspace: false,
566 sp_prepend: false,
567 sp_prepend_first: false,
568 nfc: false,
569 byte_to_char,
570 char_to_byte,
571 bos_token_id: None,
572 eos_token_id: None,
573 pad_token_id: None,
574 im_start_id: None,
575 im_end_id: None,
576 chat_template: None,
577 extra_eos: HashSet::new(),
578 add_bos: false,
579 }
580 }
581
582 pub fn encode(&self, text: &str) -> Vec<u32> {
584 let mut ids = Vec::new();
585 let mut rest = text;
587 let mut head = true;
592 'outer: while !rest.is_empty() {
593 let mut best: Option<(usize, usize, u32)> = None; for (content, id) in &self.added {
595 if let Some(pos) = rest.find(content.as_str()) {
596 let better = match best {
597 None => true,
598 Some((bp, bl, _)) => pos < bp || (pos == bp && content.len() > bl),
599 };
600 if better {
601 best = Some((pos, content.len(), *id));
602 }
603 if pos == 0 {
604 break; }
606 }
607 }
608 match best {
609 Some((pos, len, id)) => {
610 self.encode_segment_at(&rest[..pos], head, &mut ids);
611 ids.push(id);
612 rest = &rest[pos + len..];
613 head = false;
614 }
615 None => {
616 self.encode_segment_at(rest, head, &mut ids);
617 break 'outer;
618 }
619 }
620 }
621 ids
622 }
623
624 pub fn encode_plain(&self, text: &str) -> Vec<u32> {
630 let mut ids = Vec::new();
631 self.encode_segment_at(text, true, &mut ids);
632 ids
633 }
634
635 fn encode_segment_at(&self, segment: &str, head: bool, out: &mut Vec<u32>) {
639 if segment.is_empty() {
640 return;
641 }
642 let norm: String = if self.nfc {
643 segment.nfc().collect()
644 } else {
645 segment.to_string()
646 };
647 if self.metaspace {
648 let sp = if self.sp_prepend {
652 format!("\u{2581}{}", norm).replace(' ', "\u{2581}")
655 } else {
656 let replaced = norm.replace(' ', "\u{2581}");
660 if self.sp_prepend_first && head && !replaced.starts_with('\u{2581}') {
661 format!("\u{2581}{replaced}")
662 } else {
663 replaced
664 }
665 };
666 self.bpe_piece_sp(&sp, out);
667 return;
668 }
669 if !self.split_res.is_empty() {
670 let mut pieces: Vec<(usize, usize)> = vec![(0, norm.len())];
673 for re in &self.split_res {
674 let mut next: Vec<(usize, usize)> = Vec::with_capacity(pieces.len() * 2);
675 for (ps, pe) in pieces {
676 let seg = &norm[ps..pe];
677 let mut last = 0usize;
678 for m in re.find_iter(seg) {
679 let m = match m {
680 Ok(m) => m,
681 Err(e) => {
682 tracing::error!("pre-tokenizer regex failed: {e}");
683 break;
684 }
685 };
686 if m.start() > last {
687 next.push((ps + last, ps + m.start()));
688 }
689 if m.end() > m.start() {
690 next.push((ps + m.start(), ps + m.end()));
691 }
692 last = m.end();
693 }
694 if last < seg.len() {
695 next.push((ps + last, pe));
696 }
697 }
698 pieces = next;
699 }
700 for (ps, pe) in pieces {
701 self.bpe_piece(&norm[ps..pe], out);
702 }
703 } else {
704 {
705 for b in norm.bytes() {
707 let tok = format!("<0x{:02X}>", b);
708 if let Some(&id) = self.vocab.get(&tok) {
709 out.push(id);
710 }
711 }
712 }
713 }
714 }
715
716 fn sp_merges(&self) -> Option<&SpMerges> {
718 self.sp_ids
719 .get_or_init(|| SpMerges::build(&self.vocab, &self.ranks))
720 .as_ref()
721 }
722
723 fn bpe_piece_sp(&self, piece: &str, out: &mut Vec<u32>) {
732 if piece.is_empty() {
733 return;
734 }
735 if !SP_STRINGS.with(|f| f.get()) {
736 if let Some(m) = self.sp_merges() {
737 m.encode(&self.vocab, piece, out);
738 return;
739 }
740 }
741 self.bpe_piece_sp_strings(piece, out);
742 }
743
744 #[doc(hidden)]
747 pub fn encode_reference(&self, text: &str) -> Vec<u32> {
748 SP_STRINGS.with(|f| f.set(true));
749 let ids = self.encode(text);
750 SP_STRINGS.with(|f| f.set(false));
751 ids
752 }
753
754 fn bpe_piece_sp_strings(&self, piece: &str, out: &mut Vec<u32>) {
755 let mut sym: Vec<String> = piece.chars().map(|c| c.to_string()).collect();
756 loop {
757 let mut best: Option<(u32, usize)> = None;
758 for i in 0..sym.len().saturating_sub(1) {
759 if let Some(&r) = self.ranks.get(&(sym[i].clone(), sym[i + 1].clone())) {
760 if best.map(|(br, _)| r < br).unwrap_or(true) {
761 best = Some((r, i));
762 }
763 }
764 }
765 let Some((_, i)) = best else { break };
766 let merged = format!("{}{}", sym[i], sym[i + 1]);
767 let (left, right) = (sym[i].clone(), sym[i + 1].clone());
768 let mut j = 0;
769 while j + 1 < sym.len() {
770 if sym[j] == left && sym[j + 1] == right {
771 sym[j] = merged.clone();
772 sym.remove(j + 1);
773 }
774 j += 1;
775 }
776 }
777 for t in &sym {
778 if let Some(&id) = self.vocab.get(t) {
779 out.push(id);
780 } else {
781 let mut ok = true;
782 for byte in t.bytes() {
783 let tok = format!("<0x{:02X}>", byte);
784 match self.vocab.get(&tok) {
785 Some(&id) => out.push(id),
786 None => {
787 ok = false;
788 break;
789 }
790 }
791 }
792 if !ok {
793 tracing::error!("tokenizer: no id for SP symbol {t:?} — dropped");
794 }
795 }
796 }
797 }
798
799 fn bpe_piece(&self, piece: &str, out: &mut Vec<u32>) {
801 if piece.is_empty() {
802 return;
803 }
804 let mapped: Vec<String> = piece
805 .bytes()
806 .map(|b| self.byte_to_char[b as usize].to_string())
807 .collect();
808 let mut sym = mapped;
809
810 loop {
812 let mut best: Option<(u32, usize)> = None;
813 for i in 0..sym.len().saturating_sub(1) {
814 if let Some(&r) = self.ranks.get(&(sym[i].clone(), sym[i + 1].clone())) {
815 if best.map(|(br, _)| r < br).unwrap_or(true) {
816 best = Some((r, i));
817 }
818 }
819 }
820 let Some((_, i)) = best else { break };
821 let merged = format!("{}{}", sym[i], sym[i + 1]);
822 let (left, right) = (sym[i].clone(), sym[i + 1].clone());
824 let mut j = 0;
825 while j + 1 < sym.len() {
826 if sym[j] == left && sym[j + 1] == right {
827 sym[j] = merged.clone();
828 sym.remove(j + 1);
829 }
830 j += 1;
831 }
832 }
833
834 for s in &sym {
835 if let Some(&id) = self.vocab.get(s) {
836 out.push(id);
837 } else {
838 let mut ok = true;
840 for ch in s.chars() {
841 let Some(&b) = self.char_to_byte.get(&ch) else {
842 ok = false;
843 break;
844 };
845 let tok = format!("<0x{:02X}>", b);
846 if let Some(&id) = self.vocab.get(&tok) {
847 out.push(id);
848 } else {
849 ok = false;
850 break;
851 }
852 }
853 if !ok {
854 tracing::error!("tokenizer: no id for symbol {s:?} — dropped");
855 }
856 }
857 }
858 }
859
860 pub fn decode(&self, ids: &[u32]) -> String {
863 let mut bytes: Vec<u8> = Vec::new();
864 for &id in ids {
865 if self.special_ids.contains(&id) {
866 continue;
867 }
868 let idx = id as usize;
869 if idx >= self.id_to_token.len() {
870 continue;
871 }
872 let tok = &self.id_to_token[idx];
873 if self.added_ids.contains(&id) {
874 if self.metaspace && tok.contains('\u{2581}') {
877 bytes.extend_from_slice(tok.replace('\u{2581}', " ").as_bytes());
878 } else {
879 bytes.extend_from_slice(tok.as_bytes());
880 }
881 continue;
882 }
883 if tok.starts_with("<0x") && tok.ends_with('>') && tok.len() == 6 {
885 if let Ok(b) = u8::from_str_radix(&tok[3..5], 16) {
886 bytes.push(b);
887 continue;
888 }
889 }
890 if self.metaspace {
891 for ch in tok.chars() {
893 if ch == '\u{2581}' {
894 bytes.push(b' ');
895 } else {
896 let mut buf = [0u8; 4];
897 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
898 }
899 }
900 continue;
901 }
902 for ch in tok.chars() {
903 match self.char_to_byte.get(&ch) {
904 Some(&b) => bytes.push(b),
905 None => {
908 let mut buf = [0u8; 4];
909 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
910 }
911 }
912 }
913 }
914 let text = String::from_utf8_lossy(&bytes).into_owned();
915 if self.metaspace && (self.sp_prepend || self.sp_prepend_first) {
916 if let Some(stripped) = text.strip_prefix(' ') {
918 return stripped.to_string();
919 }
920 }
921 text
922 }
923
924 pub fn decode_token(&self, id: u32) -> String {
927 if self.special_ids.contains(&id) {
928 return String::new();
929 }
930 let idx = id as usize;
931 if idx >= self.id_to_token.len() {
932 return String::new();
933 }
934 let tok = &self.id_to_token[idx];
935 if self.added_ids.contains(&id) {
936 if self.metaspace && tok.contains('\u{2581}') {
937 return tok.replace('\u{2581}', " ");
938 }
939 return tok.clone();
940 }
941 if tok.starts_with("<0x") && tok.ends_with('>') && tok.len() == 6 {
942 if let Ok(b) = u8::from_str_radix(&tok[3..5], 16) {
943 return String::from_utf8_lossy(&[b]).into_owned();
944 }
945 }
946 if self.metaspace {
947 return tok.replace('\u{2581}', " ");
948 }
949 let mut bytes = Vec::new();
950 for ch in tok.chars() {
951 match self.char_to_byte.get(&ch) {
952 Some(&b) => bytes.push(b),
953 None => {
954 let mut buf = [0u8; 4];
955 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
956 }
957 }
958 }
959 String::from_utf8_lossy(&bytes).into_owned()
960 }
961
962 pub fn decode_token_for_hash(&self, id: u32) -> String {
965 let idx = id as usize;
966 if idx >= self.id_to_token.len() {
967 return String::new();
968 }
969 if self.special_ids.contains(&id) {
970 return self.id_to_token[idx].clone();
971 }
972 self.decode_token(id)
973 }
974
975 pub fn decode_for_protocol(&self, ids: &[u32]) -> String {
978 let mut out = String::new();
979 for &id in ids {
980 let idx = id as usize;
981 if self.special_ids.contains(&id) {
982 if let Some(token) = self.id_to_token.get(idx) {
983 out.push_str(token);
984 }
985 } else {
986 out.push_str(&self.decode_token(id));
987 }
988 }
989 out
990 }
991
992 pub fn raw_token_for_hash(&self, id: u32) -> String {
994 self.id_to_token
995 .get(id as usize)
996 .cloned()
997 .unwrap_or_default()
998 }
999
1000 pub fn apply_chat_template(&self, messages: &[(String, String)]) -> Vec<u32> {
1004 self.apply_chat_template_opts(messages, None)
1005 }
1006
1007 pub fn apply_chat_template_json(
1024 &self,
1025 messages: &[serde_json::Value],
1026 tools: Option<&[serde_json::Value]>,
1027 enable_thinking: Option<bool>,
1028 ) -> Vec<u32> {
1029 self.apply_chat_template_json_with_reasoning_effort(messages, tools, enable_thinking, None)
1030 }
1031
1032 pub fn apply_chat_template_json_with_reasoning_effort(
1035 &self,
1036 messages: &[serde_json::Value],
1037 tools: Option<&[serde_json::Value]>,
1038 enable_thinking: Option<bool>,
1039 reasoning_effort: Option<&str>,
1040 ) -> Vec<u32> {
1041 match self.try_apply_chat_template_json_with_reasoning_effort(
1042 messages,
1043 tools,
1044 enable_thinking,
1045 reasoning_effort,
1046 ) {
1047 Ok(ids) => ids,
1048 Err(e) => {
1049 tracing::error!("chat template render failed ({e}); ChatML fallback");
1050 self.chatml_json_fallback(messages, enable_thinking)
1051 }
1052 }
1053 }
1054
1055 pub fn try_apply_chat_template_json(
1066 &self,
1067 messages: &[serde_json::Value],
1068 tools: Option<&[serde_json::Value]>,
1069 enable_thinking: Option<bool>,
1070 ) -> Result<Vec<u32>, String> {
1071 self.try_apply_chat_template_json_with_reasoning_effort(
1072 messages,
1073 tools,
1074 enable_thinking,
1075 None,
1076 )
1077 }
1078
1079 pub fn try_apply_chat_template_json_with_reasoning_effort(
1083 &self,
1084 messages: &[serde_json::Value],
1085 tools: Option<&[serde_json::Value]>,
1086 enable_thinking: Option<bool>,
1087 reasoning_effort: Option<&str>,
1088 ) -> Result<Vec<u32>, String> {
1089 if let Some(tpl) = &self.chat_template {
1090 return self
1091 .render_template_json(tpl, messages, tools, enable_thinking, reasoning_effort)
1092 .map(|text| self.with_bos(self.encode(&text)))
1093 .map_err(|e| format!("{e:#}"));
1094 }
1095 Ok(self.chatml_json_fallback(messages, enable_thinking))
1096 }
1097
1098 fn chatml_json_fallback(
1099 &self,
1100 messages: &[serde_json::Value],
1101 enable_thinking: Option<bool>,
1102 ) -> Vec<u32> {
1103 let pairs: Vec<(String, String)> = messages
1104 .iter()
1105 .map(|m| {
1106 (
1107 m.get("role")
1108 .and_then(|v| v.as_str())
1109 .unwrap_or("user")
1110 .to_string(),
1111 m.get("content")
1112 .and_then(|v| v.as_str())
1113 .unwrap_or("")
1114 .to_string(),
1115 )
1116 })
1117 .collect();
1118 self.with_bos(self.chatml_fallback_opts(&pairs, enable_thinking))
1119 }
1120
1121 pub fn render_chat_json(
1123 &self,
1124 messages: &[serde_json::Value],
1125 tools: Option<&[serde_json::Value]>,
1126 enable_thinking: Option<bool>,
1127 ) -> Option<String> {
1128 self.render_chat_json_with_reasoning_effort(messages, tools, enable_thinking, None)
1129 }
1130
1131 pub fn render_chat_json_with_reasoning_effort(
1134 &self,
1135 messages: &[serde_json::Value],
1136 tools: Option<&[serde_json::Value]>,
1137 enable_thinking: Option<bool>,
1138 reasoning_effort: Option<&str>,
1139 ) -> Option<String> {
1140 let tpl = self.chat_template.as_ref()?;
1141 match self.render_template_json(tpl, messages, tools, enable_thinking, reasoning_effort) {
1142 Ok(t) => Some(t),
1143 Err(e) => {
1144 tracing::error!("chat template render (json): {e:#}");
1145 eprintln!("chat template render (json): {e:#}");
1146 None
1147 }
1148 }
1149 }
1150
1151 fn render_template_json(
1152 &self,
1153 tpl: &str,
1154 messages: &[serde_json::Value],
1155 tools: Option<&[serde_json::Value]>,
1156 enable_thinking: Option<bool>,
1157 reasoning_effort: Option<&str>,
1158 ) -> Result<String, minijinja::Error> {
1159 let mut env = crate::chat_template::environment();
1160 let tpl_src = strip_generation_tags(tpl);
1161 env.add_template("chat", &tpl_src)?;
1162 let msgs: Vec<minijinja::Value> = messages
1163 .iter()
1164 .map(minijinja::Value::from_serialize)
1165 .collect();
1166 let tools_v: Option<Vec<minijinja::Value>> =
1167 tools.map(|ts| ts.iter().map(minijinja::Value::from_serialize).collect());
1168 let tpl = env.get_template("chat")?;
1169 let rendered = match (tools_v, enable_thinking, reasoning_effort) {
1176 (Some(ts), Some(v), Some(effort)) => tpl.render(minijinja::context! {
1177 messages => msgs, tools => ts, add_generation_prompt => true, enable_thinking => v,
1178 reasoning_effort => effort,
1179 tool_call_format => "json",
1180 })?,
1181 (Some(ts), Some(v), None) => tpl.render(minijinja::context! {
1182 messages => msgs, tools => ts, add_generation_prompt => true, enable_thinking => v,
1183 reasoning_effort => if !v { Some("low") } else { None::<&str> },
1188 tool_call_format => "json",
1189 })?,
1190 (Some(ts), None, Some(effort)) => tpl.render(minijinja::context! {
1191 messages => msgs, tools => ts, add_generation_prompt => true,
1192 reasoning_effort => effort,
1193 tool_call_format => "json",
1194 })?,
1195 (Some(ts), None, None) => tpl.render(minijinja::context! {
1196 messages => msgs, tools => ts, add_generation_prompt => true,
1197 tool_call_format => "json",
1198 })?,
1199 (None, Some(v), Some(effort)) => tpl.render(minijinja::context! {
1200 messages => msgs, add_generation_prompt => true, enable_thinking => v,
1201 reasoning_effort => effort,
1202 tool_call_format => "json",
1203 })?,
1204 (None, Some(v), None) => tpl.render(minijinja::context! {
1205 messages => msgs, add_generation_prompt => true, enable_thinking => v,
1206 reasoning_effort => if !v { Some("low") } else { None::<&str> },
1208 tool_call_format => "json",
1209 })?,
1210 (None, None, Some(effort)) => tpl.render(minijinja::context! {
1211 messages => msgs, add_generation_prompt => true,
1212 reasoning_effort => effort,
1213 tool_call_format => "json",
1214 })?,
1215 (None, None, None) => tpl.render(minijinja::context! {
1216 messages => msgs, add_generation_prompt => true,
1217 tool_call_format => "json",
1218 })?,
1219 };
1220 Ok(close_direct_think(rendered, enable_thinking))
1221 }
1222
1223 pub fn apply_chat_template_opts(
1224 &self,
1225 messages: &[(String, String)],
1226 enable_thinking: Option<bool>,
1227 ) -> Vec<u32> {
1228 if let Some(tpl) = &self.chat_template {
1229 match self.render_template(tpl, messages, enable_thinking) {
1230 Ok(text) => return self.with_bos(self.encode(&text)),
1231 Err(e) => {
1232 tracing::error!("chat template render failed ({e}); ChatML fallback");
1233 }
1234 }
1235 }
1236 self.with_bos(self.chatml_fallback_opts(messages, enable_thinking))
1237 }
1238
1239 pub fn with_bos(&self, mut ids: Vec<u32>) -> Vec<u32> {
1241 if self.add_bos {
1242 if let Some(b) = self.bos_token_id {
1243 if ids.first() != Some(&b) {
1244 ids.insert(0, b);
1245 }
1246 }
1247 }
1248 ids
1249 }
1250
1251 pub fn render_chat(&self, messages: &[(String, String)]) -> Option<String> {
1253 self.render_chat_opts(messages, None)
1254 }
1255
1256 pub fn render_chat_opts(
1258 &self,
1259 messages: &[(String, String)],
1260 enable_thinking: Option<bool>,
1261 ) -> Option<String> {
1262 let tpl = self.chat_template.as_ref()?;
1263 match self.render_template(tpl, messages, enable_thinking) {
1264 Ok(t) => Some(t),
1265 Err(e) => {
1266 tracing::error!("chat template render: {e:#}");
1267 None
1268 }
1269 }
1270 }
1271
1272 fn render_template(
1273 &self,
1274 tpl: &str,
1275 messages: &[(String, String)],
1276 enable_thinking: Option<bool>,
1277 ) -> Result<String, minijinja::Error> {
1278 let mut env = crate::chat_template::environment();
1279 let tpl_src = strip_generation_tags(tpl);
1280 env.add_template("chat", &tpl_src)?;
1281 let msgs: Vec<minijinja::Value> = messages
1282 .iter()
1283 .map(|(role, content)| {
1284 minijinja::context! { role => role, content => content }
1285 })
1286 .collect();
1287 let rendered = match enable_thinking {
1290 Some(v) => env.get_template("chat")?.render(minijinja::context! {
1291 messages => msgs,
1292 add_generation_prompt => true,
1293 enable_thinking => v,
1294 reasoning_effort => if !v { Some("low") } else { None::<&str> },
1297 })?,
1298 None => env.get_template("chat")?.render(minijinja::context! {
1299 messages => msgs,
1300 add_generation_prompt => true,
1301 })?,
1302 };
1303 Ok(close_direct_think(rendered, enable_thinking))
1304 }
1305
1306 fn chatml_fallback(&self, messages: &[(String, String)]) -> Vec<u32> {
1308 self.chatml_fallback_opts(messages, None)
1309 }
1310
1311 fn chatml_fallback_opts(
1313 &self,
1314 messages: &[(String, String)],
1315 enable_thinking: Option<bool>,
1316 ) -> Vec<u32> {
1317 let mut tokens = Vec::new();
1318
1319 for (role, content) in messages {
1320 if let Some(start_id) = self.im_start_id {
1322 tokens.push(start_id);
1323 }
1324 tokens.extend(self.encode(&format!("{}\n{}", role, content)));
1325 if let Some(end_id) = self.im_end_id {
1326 tokens.push(end_id);
1327 }
1328 tokens.extend(self.encode("\n"));
1329 }
1330
1331 if let Some(start_id) = self.im_start_id {
1333 tokens.push(start_id);
1334 }
1335 tokens.extend(self.encode("assistant\n"));
1336 if enable_thinking == Some(false) {
1337 tokens.extend(self.encode("<think>\n\n</think>\n\n"));
1338 }
1339
1340 tokens
1341 }
1342
1343 pub fn vocab_size(&self) -> usize {
1345 self.id_to_token.len()
1346 }
1347
1348 pub fn token_to_id(&self, token: &str) -> Option<u32> {
1352 self.vocab.get(token).copied()
1353 }
1354
1355 pub fn convert_tokens_to_ids(&self, token: &str) -> Option<u32> {
1358 self.token_to_id(token)
1359 }
1360
1361 pub fn is_eos(&self, id: u32) -> bool {
1363 self.eos_token_id == Some(id) || self.im_end_id == Some(id) || self.extra_eos.contains(&id)
1364 }
1365}
1366
1367#[derive(Debug, thiserror::Error)]
1368pub enum TokenizerError {
1369 #[error("IO error: {0}")]
1370 Io(String),
1371 #[error("Parse error: {0}")]
1372 Parse(String),
1373}
1374
1375struct SpMerges {
1377 pairs: HashMap<u64, (u32, u32)>,
1378 bytes: [u32; 256],
1380}
1381
1382const SP_NONE: u32 = u32::MAX;
1383
1384thread_local! {
1385 static SP_STRINGS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1387}
1388
1389impl SpMerges {
1390 fn build(vocab: &HashMap<String, u32>, ranks: &HashMap<(String, String), u32>) -> Option<Self> {
1391 if ranks.is_empty() {
1392 return None;
1393 }
1394 let mut pairs = HashMap::with_capacity(ranks.len());
1395 let mut cat = String::new();
1396 for ((a, b), &r) in ranks {
1397 let (&ia, &ib) = (vocab.get(a)?, vocab.get(b)?);
1398 cat.clear();
1399 cat.push_str(a);
1400 cat.push_str(b);
1401 let &m = vocab.get(&cat)?;
1402 pairs.insert(((ia as u64) << 32) | ib as u64, (r, m));
1403 }
1404 let mut bytes = [SP_NONE; 256];
1405 for (b, slot) in bytes.iter_mut().enumerate() {
1406 if let Some(&id) = vocab.get(&format!("<0x{b:02X}>")) {
1407 *slot = id;
1408 }
1409 }
1410 Some(SpMerges { pairs, bytes })
1411 }
1412
1413 #[inline]
1414 fn rank(&self, a: u32, b: u32) -> Option<(u32, u32)> {
1415 if a == SP_NONE || b == SP_NONE {
1416 return None;
1417 }
1418 self.pairs.get(&(((a as u64) << 32) | b as u64)).copied()
1419 }
1420
1421 fn encode(&self, vocab: &HashMap<String, u32>, piece: &str, out: &mut Vec<u32>) {
1426 use std::cmp::Reverse;
1427 use std::collections::BinaryHeap;
1428 let spans: Vec<(usize, usize)> = piece
1429 .char_indices()
1430 .map(|(i, c)| (i, i + c.len_utf8()))
1431 .collect();
1432 let n = spans.len();
1433 let mut buf = [0u8; 4];
1434 let mut id: Vec<u32> = piece
1435 .chars()
1436 .map(|c| *vocab.get(c.encode_utf8(&mut buf) as &str).unwrap_or(&SP_NONE))
1437 .collect();
1438 let mut next: Vec<usize> = (1..=n).collect();
1439 let mut prev: Vec<usize> = (0..n).map(|i| i.wrapping_sub(1)).collect();
1440 let mut alive = vec![true; n];
1441 let mut heap: BinaryHeap<Reverse<(u32, usize)>> = BinaryHeap::with_capacity(n);
1442 for i in 0..n.saturating_sub(1) {
1443 if let Some((r, _)) = self.rank(id[i], id[i + 1]) {
1444 heap.push(Reverse((r, i)));
1445 }
1446 }
1447 let mut batch: Vec<usize> = Vec::new();
1448 let mut merged: Vec<usize> = Vec::new();
1449 while let Some(&Reverse((r, _))) = heap.peek() {
1450 batch.clear();
1451 while let Some(&Reverse((r2, pos))) = heap.peek() {
1452 if r2 != r {
1453 break;
1454 }
1455 heap.pop();
1456 batch.push(pos);
1457 }
1458 batch.sort_unstable();
1459 batch.dedup();
1460 merged.clear();
1461 for &pos in &batch {
1462 if !alive[pos] || next[pos] >= n {
1463 continue;
1464 }
1465 let j = next[pos];
1466 match self.rank(id[pos], id[j]) {
1467 Some((rr, m)) if rr == r => {
1468 id[pos] = m;
1469 alive[j] = false;
1470 next[pos] = next[j];
1471 if next[j] < n {
1472 prev[next[j]] = pos;
1473 }
1474 merged.push(pos);
1475 }
1476 _ => {}
1477 }
1478 }
1479 for &pos in &merged {
1480 if !alive[pos] {
1481 continue;
1482 }
1483 let pv = prev[pos];
1484 if pv < n && alive[pv] {
1485 if let Some((r2, _)) = self.rank(id[pv], id[pos]) {
1486 heap.push(Reverse((r2, pv)));
1487 }
1488 }
1489 let nx = next[pos];
1490 if nx < n {
1491 if let Some((r2, _)) = self.rank(id[pos], id[nx]) {
1492 heap.push(Reverse((r2, pos)));
1493 }
1494 }
1495 }
1496 }
1497 let mut i = 0usize;
1498 while i < n {
1499 if id[i] != SP_NONE {
1500 out.push(id[i]);
1501 } else {
1502 let (a, b) = spans[i];
1504 let mut ok = true;
1505 for &byte in piece[a..b].as_bytes() {
1506 match self.bytes[byte as usize] {
1507 SP_NONE => {
1508 ok = false;
1509 break;
1510 }
1511 t => out.push(t),
1512 }
1513 }
1514 if !ok {
1515 tracing::error!("tokenizer: no id for SP symbol {:?} — dropped", &piece[a..b]);
1516 }
1517 }
1518 i = next[i];
1519 }
1520 }
1521}
1522
1523#[cfg(test)]
1524mod tests {
1525 use super::*;
1526
1527 #[test]
1528 fn byte_unicode_bijection() {
1529 let (b2c, c2b) = bytes_to_unicode();
1530 for b in 0..=255u8 {
1531 assert_eq!(c2b[&b2c[b as usize]], b);
1532 }
1533 assert_eq!(b2c[b' ' as usize], 'Ġ');
1535 assert_eq!(b2c[b'\n' as usize], 'Ċ');
1536 }
1537
1538 #[test]
1539 fn byte_level_roundtrip_utf8() {
1540 let tok = Tokenizer::byte_level();
1541 let text = "hello 🌍 hi\n";
1542 let ids = tok.encode(text);
1543 assert_eq!(ids.len(), text.len()); assert_eq!(tok.decode(&ids), text);
1545 }
1546
1547 fn mini_json() -> String {
1550 let vocab: Vec<(&str, u32)> = vec![
1552 ("h", 0),
1553 ("e", 1),
1554 ("l", 2),
1555 ("o", 3),
1556 ("Ġ", 4),
1557 ("w", 5),
1558 ("r", 6),
1559 ("d", 7),
1560 ("he", 8),
1561 ("Ġw", 9),
1562 ];
1563 let vocab_json: String = vocab
1564 .iter()
1565 .map(|(t, i)| format!("\"{t}\": {i}"))
1566 .collect::<Vec<_>>()
1567 .join(", ");
1568 format!(
1569 r#"{{
1570 "model": {{
1571 "type": "BPE",
1572 "vocab": {{ {vocab_json} }},
1573 "merges": [["h", "e"], ["Ġ", "w"]]
1574 }},
1575 "added_tokens": [
1576 {{"id": 10, "content": "<|eot|>", "special": true}}
1577 ]
1578 }}"#
1579 )
1580 }
1581
1582 #[test]
1588 fn sp_heap_bpe_matches_the_string_form() {
1589 let toks = [
1590 "a", "b", "c", "▁", "bc", "abc", "ab", "aa", "aab", "ca", "▁a", "aabc",
1591 "aaa", "<0x64>", "<0xC3>", "▁▁",
1592 ];
1593 let vocab: Vec<String> = toks
1594 .iter()
1595 .enumerate()
1596 .map(|(i, t)| format!("\"{t}\": {i}"))
1597 .collect();
1598 let merges = [
1599 ("aab", "c"),
1600 ("b", "c"),
1601 ("a", "bc"),
1602 ("a", "b"),
1603 ("ab", "c"),
1604 ("a", "a"),
1605 ("aa", "b"),
1606 ("c", "a"),
1607 ("▁", "a"),
1608 ("aa", "a"),
1609 ("▁", "▁"),
1610 ];
1611 let merges: Vec<String> = merges
1612 .iter()
1613 .map(|(a, b)| format!("[\"{a}\", \"{b}\"]"))
1614 .collect();
1615 let json = format!(
1616 r#"{{"model": {{"type": "BPE", "byte_fallback": true, "vocab": {{ {} }}, "merges": [{}]}},
1617 "normalizer": {{"type": "Replace", "pattern": {{"String": " "}}, "content": "\u2581"}},
1618 "added_tokens": []}}"#,
1619 vocab.join(", "),
1620 merges.join(", ")
1621 );
1622 let t = Tokenizer::from_json(&json).unwrap();
1623 assert!(t.metaspace, "a byte_fallback vocabulary takes the SentencePiece path");
1624 assert!(t.sp_merges().is_some());
1625 let alphabet = ['a', 'b', 'c', '\u{2581}', 'd', 'é', 'a', 'a', 'b'];
1626 let mut state = 0x2545_f491_4f6c_dd1du64;
1627 for len in 0..40 {
1628 for _ in 0..60 {
1629 let piece: String = (0..len)
1630 .map(|_| {
1631 state ^= state << 13;
1632 state ^= state >> 7;
1633 state ^= state << 17;
1634 alphabet[(state % alphabet.len() as u64) as usize]
1635 })
1636 .collect();
1637 let (mut fast, mut slow) = (Vec::new(), Vec::new());
1638 t.sp_merges().unwrap().encode(&t.vocab, &piece, &mut fast);
1639 t.bpe_piece_sp_strings(&piece, &mut slow);
1640 assert_eq!(fast, slow, "{piece:?}");
1641 }
1642 }
1643 }
1644
1645 #[test]
1649 fn tool_markup_decodes_even_when_special() {
1650 let json = r#"{
1651 "model": {"type": "BPE", "vocab": {"h": 0, "e": 1, "l": 2, "o": 3}, "merges": []},
1652 "added_tokens": [
1653 {"id": 10, "content": "<|im_end|>", "special": true},
1654 {"id": 11, "content": "<function", "special": true},
1655 {"id": 12, "content": "</function>", "special": true},
1656 {"id": 13, "content": "<param", "special": true},
1657 {"id": 14, "content": "</param>", "special": true},
1658 {"id": 15, "content": "<tool_call>", "special": true}
1659 ]
1660 }"#;
1661 let t = Tokenizer::from_json(json).unwrap();
1662 let ids = [11, 0, 1, 13, 2, 14, 12, 15, 10];
1663 assert_eq!(
1664 t.decode(&ids),
1665 "<functionhe<paraml</param></function><tool_call>"
1666 );
1667 let streamed: String = ids.iter().map(|&i| t.decode_token(i)).collect();
1668 assert_eq!(streamed, t.decode(&ids), "streaming must agree with decode");
1669 assert!(
1670 !t.decode(&[10]).contains("im_end"),
1671 "control tokens stay hidden"
1672 );
1673 }
1674
1675 #[test]
1678 fn real_tokenizer_parity_when_available() {
1679 let Ok(path) = std::env::var("CMF_TOK_PARITY") else {
1680 return;
1681 };
1682 let t = Tokenizer::from_file(&path).expect("load");
1683 for (text, want) in [
1684 (
1685 "The capital of France is",
1686 vec![671u32, 6102, 294, 8760, 344],
1687 ),
1688 ("2 + 2 =", vec![20, 940, 223, 20, 438]),
1689 ] {
1690 let got = t.encode(text);
1691 assert_eq!(got, want, "«{text}»");
1692 }
1693 }
1694
1695 #[test]
1700 fn granite_42_chat_template_when_available() {
1701 let Ok(path) = std::env::var("CMF_GRANITE_CHAT_TEMPLATE") else {
1702 return;
1703 };
1704 let mut tok = Tokenizer::byte_level();
1705 tok.chat_template = Some(std::fs::read_to_string(path).expect("read Granite template"));
1706 let messages = vec![("user".to_string(), "Hello".to_string())];
1707
1708 let thinking = tok
1709 .render_chat_opts(&messages, Some(true))
1710 .expect("render Granite thinking prompt");
1711 assert_eq!(
1712 thinking,
1713 "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\n<think>\n"
1714 );
1715
1716 let direct = tok
1717 .render_chat_opts(&messages, Some(false))
1718 .expect("render Granite direct prompt");
1719 assert_eq!(
1720 direct,
1721 "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\n<think></think>"
1722 );
1723 }
1724
1725 #[test]
1726 fn json_template_passes_explicit_reasoning_effort_without_overriding_it() {
1727 let mut tok = Tokenizer::byte_level();
1728 tok.chat_template = Some(
1729 "{% if reasoning_effort is defined %}<effort>{{ reasoning_effort }}</effort>{% else %}<default-effort>{% endif %}|{{ enable_thinking }}|{{ messages[0].reasoning_content }}"
1730 .to_string(),
1731 );
1732 let messages = vec![serde_json::json!({
1733 "role": "assistant",
1734 "content": "answer",
1735 "reasoning_content": "prior chain"
1736 })];
1737 let explicit = tok
1738 .render_chat_json_with_reasoning_effort(&messages, None, Some(false), Some("high"))
1739 .expect("render explicit effort");
1740 assert_eq!(explicit, "<effort>high</effort>|False|prior chain");
1741
1742 let native_default = tok
1743 .render_chat_json_with_reasoning_effort(&messages, None, None, None)
1744 .expect("render native default");
1745 assert_eq!(native_default, "<default-effort>||prior chain");
1746 }
1747
1748 #[test]
1749 fn fallible_json_template_render_does_not_use_chatml_on_jinja_error() {
1750 let mut tok = Tokenizer::byte_level();
1751 tok.chat_template = Some("{% for message in messages %}".to_string());
1752 let messages = vec![serde_json::json!({"role": "user", "content": "hello"})];
1753 let error = tok
1754 .try_apply_chat_template_json_with_reasoning_effort(
1755 &messages,
1756 None,
1757 None,
1758 Some("high"),
1759 )
1760 .expect_err("unclosed Jinja must fail instead of producing ChatML");
1761 assert!(!error.is_empty());
1762 }
1763
1764 #[test]
1770 fn minimax_m3_chat_template_when_available() {
1771 let Ok(path) = std::env::var("CMF_MINIMAX_M3_CHAT_TEMPLATE") else {
1772 return;
1773 };
1774 let mut tok = Tokenizer::byte_level();
1775 tok.chat_template = Some(std::fs::read_to_string(path).expect("read MiniMax M3 template"));
1776 let messages = vec![
1777 serde_json::json!({"role": "system", "content": "Keep answers concise."}),
1778 serde_json::json!({"role": "user", "content": "What is 2 + 2?"}),
1779 ];
1780 let tools = vec![serde_json::json!({
1781 "type": "function",
1782 "function": {
1783 "name": "get_weather",
1784 "description": "Return the weather for a city.",
1785 "parameters": {
1786 "type": "object",
1787 "properties": {"city": {"type": "string"}},
1788 "required": ["city"]
1789 }
1790 }
1791 })];
1792 let rendered = tok
1793 .render_chat_json(&messages, Some(&tools), Some(false))
1794 .expect("render MiniMax M3 native prompt");
1795 assert!(rendered.starts_with("]~!b[]~b]system\n"), "{rendered:?}");
1796 assert!(rendered.contains("<effort>low</effort>"), "{rendered:?}");
1797 assert!(
1798 rendered.contains("]<]minimax[>[<tool_call>"),
1799 "{rendered:?}"
1800 );
1801 assert!(rendered.contains("get_weather"), "{rendered:?}");
1802 assert!(rendered.ends_with("]~b]ai\n"), "{rendered:?}");
1803 }
1804
1805 #[test]
1809 fn glm_direct_template_keeps_assistant_special_token_intact() {
1810 let mut tok = Tokenizer::byte_level();
1811 tok.chat_template = Some(
1812 "[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>"
1813 .to_string(),
1814 );
1815 let messages = vec![("user".to_string(), "2+2?".to_string())];
1816 let rendered = tok
1817 .render_chat_opts(&messages, Some(false))
1818 .expect("render GLM direct prompt");
1819 assert!(rendered.contains("Reasoning Effort: Low"));
1820 assert!(rendered.contains("<|assistant|><think></think>"));
1821 assert!(!rendered.contains("<|assistant\n"));
1822
1823 let thinking = tok
1826 .render_chat_opts(&messages, Some(true))
1827 .expect("render GLM thinking prompt");
1828 assert!(thinking.contains("Reasoning Effort: Max"));
1829
1830 let json_messages = vec![serde_json::json!({
1831 "role": "user",
1832 "content": "2+2?"
1833 })];
1834 let rendered_json = tok
1835 .render_chat_json(&json_messages, None, Some(false))
1836 .expect("render GLM JSON direct prompt");
1837 assert!(rendered_json.contains("Reasoning Effort: Low"));
1838 assert!(rendered_json.contains("<|assistant|><think></think>"));
1839 assert!(!rendered_json.contains("<|assistant\n"));
1840 }
1841
1842 #[test]
1848 fn every_split_in_a_sequence_is_applied() {
1849 let pt = serde_json::json!({
1850 "type": "Sequence",
1851 "pretokenizers": [
1852 {"type": "Split", "behavior": "Isolated",
1853 "pattern": {"Regex": r"\p{N}{1,3}"}},
1854 {"type": "Split", "behavior": "Isolated",
1855 "pattern": {"Regex": r" ?[\p{L}]+"}},
1856 {"type": "ByteLevel", "add_prefix_space": false, "use_regex": false}
1857 ]
1858 });
1859 let mut pats = Vec::new();
1860 collect_split_patterns(&pt, &mut pats);
1861 assert_eq!(
1862 pats.len(),
1863 2,
1864 "both Split stages must be collected: {pats:?}"
1865 );
1866 assert!(pats[0].contains("p{N}"), "digit rule first");
1867 assert!(pats[1].contains("p{L}"), "word rule second");
1868
1869 let re: Vec<fancy_regex::Regex> = pats
1873 .iter()
1874 .map(|p| fancy_regex::Regex::new(p).unwrap())
1875 .collect();
1876 let norm = "ab cd12";
1877 let mut pieces: Vec<(usize, usize)> = vec![(0, norm.len())];
1878 for r in &re {
1879 let mut next = Vec::new();
1880 for (ps, pe) in pieces {
1881 let seg = &norm[ps..pe];
1882 let mut last = 0;
1883 for m in r.find_iter(seg).flatten() {
1884 if m.start() > last {
1885 next.push((ps + last, ps + m.start()));
1886 }
1887 if m.end() > m.start() {
1888 next.push((ps + m.start(), ps + m.end()));
1889 }
1890 last = m.end();
1891 }
1892 if last < seg.len() {
1893 next.push((ps + last, pe));
1894 }
1895 }
1896 pieces = next;
1897 }
1898 let got: Vec<&str> = pieces.iter().map(|(a, b)| &norm[*a..*b]).collect();
1899 assert_eq!(
1900 got,
1901 vec!["ab", " cd", "12"],
1902 "staged split produced {got:?}"
1903 );
1904 }
1905
1906 #[test]
1907 fn full_pipeline_merges_and_added_tokens() {
1908 let tok = Tokenizer::from_json(&mini_json()).unwrap();
1909 let ids = tok.encode("hello world");
1911 assert_eq!(ids, vec![8, 2, 2, 3, 9, 3, 6, 2, 7]);
1912 assert_eq!(tok.decode(&ids), "hello world");
1913 let ids2 = tok.encode("he<|eot|>he");
1915 assert_eq!(ids2, vec![8, 10, 8]);
1916 assert_eq!(tok.decode(&ids2), "hehe");
1917 }
1918
1919 #[test]
1920 fn non_ascii_is_never_silently_dropped() {
1921 let tok = Tokenizer::from_json(&mini_json()).unwrap();
1922 let ids = tok.encode("hello");
1925 assert!(!ids.is_empty());
1926 }
1927}
1928
1929#[cfg(test)]
1930mod generation_tag_tests {
1931 use super::strip_generation_tags;
1932
1933 #[test]
1938 fn a_generation_block_becomes_a_no_op_keeping_its_whitespace_control() {
1939 let tpl = "a{%- generation -%}b{%- endgeneration -%}c";
1940 let out = strip_generation_tags(tpl);
1941 assert!(!out.contains("{%- generation"));
1942 assert!(!out.contains("endgeneration"));
1943 assert_eq!(out.matches("{%-").count(), 2);
1945 assert_eq!(out.matches("-%}").count(), 2);
1946 assert!(out.starts_with('a') && out.ends_with('c'));
1947 }
1948
1949 #[test]
1952 fn each_side_keeps_its_own_dash() {
1953 let out = strip_generation_tags("{% generation %}x{%- endgeneration %}");
1954 assert!(out.starts_with("{% set"), "no dash added on the left");
1955 assert!(out.contains("{%- set"), "the right tag keeps its dash");
1956 assert!(!out.contains("-%}"), "no trailing dash invented");
1957 }
1958
1959 #[test]
1962 fn everything_else_is_left_alone() {
1963 let plain = "{%- if x -%}{{ y }}{%- endif -%}";
1964 assert_eq!(strip_generation_tags(plain), plain);
1965 let prose = "{{ 'the generation of tokens' }}";
1967 assert_eq!(strip_generation_tags(prose), prose);
1968 }
1969}