1use serde::Deserialize;
13use std::collections::{HashMap, HashSet};
14use std::path::Path;
15use unicode_normalization::UnicodeNormalization;
16
17const 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+";
20
21pub struct Tokenizer {
23 vocab: HashMap<String, u32>,
25 id_to_token: Vec<String>,
27 ranks: HashMap<(String, String), u32>,
29 added: Vec<(String, u32)>,
31 added_ids: HashSet<u32>,
33 special_ids: HashSet<u32>,
35 split_res: Vec<fancy_regex::Regex>,
39 sp_prepend: bool,
42 sp_prepend_first: bool,
48 metaspace: bool,
51 nfc: bool,
54 byte_to_char: [char; 256],
56 char_to_byte: HashMap<char, u8>,
58 pub bos_token_id: Option<u32>,
60 pub eos_token_id: Option<u32>,
61 pub pad_token_id: Option<u32>,
62 pub im_start_id: Option<u32>,
64 pub im_end_id: Option<u32>,
65 pub chat_template: Option<String>,
68 pub extra_eos: HashSet<u32>,
70 pub add_bos: bool,
72}
73
74impl std::fmt::Debug for Tokenizer {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.debug_struct("Tokenizer")
77 .field("vocab", &self.vocab.len())
78 .field("merges", &self.ranks.len())
79 .field("added", &self.added.len())
80 .finish()
81 }
82}
83
84fn bytes_to_unicode() -> ([char; 256], HashMap<char, u8>) {
87 let mut b2c = ['\0'; 256];
88 let mut c2b = HashMap::with_capacity(256);
89 let mut n = 0u32;
90 for b in 0..=255u16 {
91 let printable =
92 (0x21..=0x7E).contains(&b) || (0xA1..=0xAC).contains(&b) || (0xAE..=0xFF).contains(&b);
93 let c = if printable {
94 char::from_u32(b as u32).unwrap()
95 } else {
96 let c = char::from_u32(256 + n).unwrap();
97 n += 1;
98 c
99 };
100 b2c[b as usize] = c;
101 c2b.insert(c, b as u8);
102 }
103 (b2c, c2b)
104}
105
106#[derive(Deserialize)]
108struct HfTokenizerJson {
109 model: HfModel,
110 #[serde(default)]
111 added_tokens: Vec<HfAddedToken>,
112 #[serde(default)]
113 pre_tokenizer: Option<serde_json::Value>,
114 #[serde(default)]
115 normalizer: Option<serde_json::Value>,
116 #[serde(default)]
117 post_processor: Option<serde_json::Value>,
118}
119
120#[derive(Deserialize)]
121struct HfModel {
122 vocab: HashMap<String, u32>,
123 #[serde(default)]
124 merges: Vec<HfMerge>,
125 #[serde(default)]
126 byte_fallback: bool,
127}
128
129#[derive(Deserialize)]
132#[serde(untagged)]
133enum HfMerge {
134 Pair([String; 2]),
135 Text(String),
136}
137
138#[derive(Deserialize)]
139struct HfAddedToken {
140 id: u32,
141 content: String,
142 special: bool,
143}
144
145fn collect_split_patterns(pt: &serde_json::Value, out: &mut Vec<String>) {
154 if pt.get("type").and_then(|t| t.as_str()) == Some("Split") {
155 if let Some(r) = pt
156 .get("pattern")
157 .and_then(|p| p.get("Regex"))
158 .and_then(|r| r.as_str())
159 {
160 out.push(r.to_string());
161 }
162 return;
163 }
164 if let Some(list) = pt.get("pretokenizers").and_then(|l| l.as_array()) {
165 for p in list {
166 collect_split_patterns(p, out);
167 }
168 }
169}
170
171fn find_prepend_scheme(pt: &serde_json::Value) -> Option<String> {
174 if pt.get("type").and_then(|t| t.as_str()) == Some("Metaspace") {
175 return pt
176 .get("prepend_scheme")
177 .and_then(|p| p.as_str())
178 .map(String::from);
179 }
180 if let Some(list) = pt.get("pretokenizers").and_then(|l| l.as_array()) {
181 return list.iter().find_map(find_prepend_scheme);
182 }
183 None
184}
185
186impl Tokenizer {
187 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, TokenizerError> {
189 let data = std::fs::read_to_string(path.as_ref())
190 .map_err(|e| TokenizerError::Io(e.to_string()))?;
191 Self::from_json(&data)
192 }
193
194 pub fn from_bytes(bytes: &[u8]) -> Result<Self, TokenizerError> {
196 let s = std::str::from_utf8(bytes)
197 .map_err(|e| TokenizerError::Parse(format!("vocab is not UTF-8: {e}")))?;
198 Self::from_json(s)
199 }
200
201 pub fn from_json(json: &str) -> Result<Self, TokenizerError> {
203 let hf: HfTokenizerJson =
204 serde_json::from_str(json).map_err(|e| TokenizerError::Parse(e.to_string()))?;
205
206 let mut vocab = hf.model.vocab;
207 let mut ranks = HashMap::new();
208 for (rank, m) in hf.model.merges.into_iter().enumerate() {
209 let (a, b) = match m {
210 HfMerge::Pair([a, b]) => (a, b),
211 HfMerge::Text(s) => {
212 let mut it = s.splitn(2, ' ');
213 match (it.next(), it.next()) {
214 (Some(a), Some(b)) => (a.to_string(), b.to_string()),
215 _ => continue,
216 }
217 }
218 };
219 ranks.insert((a, b), rank as u32);
220 }
221
222 let mut saw_gemma_bos = false;
227 let add_bos_detected = hf
228 .post_processor
229 .as_ref()
230 .map(|p| {
231 let pp = p.to_string();
232 pp.contains("\"<s>\"") || pp.contains("\"<bos>\"")
233 })
234 .unwrap_or(false);
235 let nfc = hf
236 .normalizer
237 .as_ref()
238 .map(|n| n.to_string().contains("NFC"))
239 .unwrap_or(false);
240 let metaspace = hf.model.byte_fallback
241 || hf
242 .normalizer
243 .as_ref()
244 .map(|n| n.to_string().contains("\u{2581}") || n.to_string().contains("▁"))
245 .unwrap_or(false);
246 let sp_prepend = hf
247 .normalizer
248 .as_ref()
249 .map(|n| n.to_string().contains("Prepend"))
250 .unwrap_or(false);
251 let (sp_prepend, sp_prepend_first) = if sp_prepend {
255 (true, false)
256 } else {
257 match hf.pre_tokenizer.as_ref().and_then(find_prepend_scheme) {
258 Some(s) if s == "always" => (true, false),
259 Some(s) if s == "first" => (false, true),
260 _ => (false, false),
261 }
262 };
263 let split_res = if metaspace {
264 Vec::new()
265 } else {
266 let mut pats = Vec::new();
267 if let Some(pt) = hf.pre_tokenizer.as_ref() {
268 collect_split_patterns(pt, &mut pats);
269 }
270 if pats.is_empty() {
271 pats.push(DEFAULT_SPLIT.to_string());
272 }
273 pats.iter()
274 .map(|p| {
275 fancy_regex::Regex::new(p)
276 .map_err(|e| TokenizerError::Parse(format!("pre-tokenizer regex: {e}")))
277 })
278 .collect::<Result<Vec<_>, _>>()?
279 };
280
281 let mut bos_token_id = None;
283 let mut eos_token_id = None;
284 let mut pad_token_id = None;
285 let mut im_start_id = None;
286 let mut im_end_id = None;
287 let mut special_ids = HashSet::new();
288 let mut added_ids = HashSet::new();
289 let mut added = Vec::new();
290
291 for at in &hf.added_tokens {
292 vocab.insert(at.content.clone(), at.id);
293 added.push((at.content.clone(), at.id));
294 added_ids.insert(at.id);
295 if at.special {
296 special_ids.insert(at.id);
297 }
298 match at.content.as_str() {
299 "<|endoftext|>" | "</s>" | "[EOS]" => eos_token_id = Some(at.id),
300 "<|im_start|>" => im_start_id = Some(at.id),
301 "<|im_end|>" => im_end_id = Some(at.id),
302 "<s>" | "[BOS]" => bos_token_id = Some(at.id),
303 "<bos>" => {
307 bos_token_id = Some(at.id);
308 saw_gemma_bos = true;
309 }
310 "<pad>" => pad_token_id = Some(at.id),
311 _ => {}
312 }
313 }
314 added.sort_by_key(|(c, _)| std::cmp::Reverse(c.len()));
315
316 let gemma_family = saw_gemma_bos
323 || vocab.contains_key("<start_of_turn>")
324 || added.iter().any(|(c, _)| c == "<start_of_turn>");
325
326 if let Some(pp) = hf.post_processor.as_ref() {
330 let pp = pp.to_string();
331 for name in ["<bos>", "<s>"] {
332 if pp.contains(&format!("\"{name}\"")) {
333 if let Some(&id) = vocab.get(name) {
334 bos_token_id = Some(id);
335 }
336 break;
337 }
338 }
339 }
340
341 let max_id = vocab.values().copied().max().unwrap_or(0) as usize;
343 let mut id_to_token = vec![String::new(); max_id + 1];
344 for (token, &id) in &vocab {
345 if (id as usize) < id_to_token.len() {
346 id_to_token[id as usize] = token.clone();
347 }
348 }
349
350 let (byte_to_char, char_to_byte) = bytes_to_unicode();
351
352 tracing::info!(
353 "Tokenizer loaded: {} vocab, {} merges, {} added, eos={:?}",
354 vocab.len(),
355 ranks.len(),
356 added.len(),
357 eos_token_id
358 );
359
360 Ok(Self {
361 vocab,
362 id_to_token,
363 ranks,
364 added,
365 added_ids,
366 special_ids,
367 split_res,
368 metaspace,
369 sp_prepend,
370 sp_prepend_first,
371 nfc,
372 byte_to_char,
373 char_to_byte,
374 bos_token_id,
375 eos_token_id,
376 pad_token_id,
377 im_start_id,
378 im_end_id,
379 chat_template: None,
380 extra_eos: HashSet::new(),
381 add_bos: add_bos_detected || gemma_family,
382 })
383 }
384
385 pub fn byte_level() -> Self {
387 let mut vocab = HashMap::new();
388 let mut id_to_token = Vec::with_capacity(256);
389 for i in 0..256u32 {
390 let tok = format!("<0x{:02X}>", i);
391 vocab.insert(tok.clone(), i);
392 id_to_token.push(tok);
393 }
394 let (byte_to_char, char_to_byte) = bytes_to_unicode();
395 Self {
396 vocab,
397 id_to_token,
398 ranks: HashMap::new(),
399 added: Vec::new(),
400 added_ids: HashSet::new(),
401 special_ids: HashSet::new(),
402 split_res: Vec::new(),
403 metaspace: false,
404 sp_prepend: false,
405 sp_prepend_first: false,
406 nfc: false,
407 byte_to_char,
408 char_to_byte,
409 bos_token_id: None,
410 eos_token_id: None,
411 pad_token_id: None,
412 im_start_id: None,
413 im_end_id: None,
414 chat_template: None,
415 extra_eos: HashSet::new(),
416 add_bos: false,
417 }
418 }
419
420 pub fn encode(&self, text: &str) -> Vec<u32> {
422 let mut ids = Vec::new();
423 let mut rest = text;
425 let mut head = true;
430 'outer: while !rest.is_empty() {
431 let mut best: Option<(usize, usize, u32)> = None; for (content, id) in &self.added {
433 if let Some(pos) = rest.find(content.as_str()) {
434 let better = match best {
435 None => true,
436 Some((bp, bl, _)) => pos < bp || (pos == bp && content.len() > bl),
437 };
438 if better {
439 best = Some((pos, content.len(), *id));
440 }
441 if pos == 0 {
442 break; }
444 }
445 }
446 match best {
447 Some((pos, len, id)) => {
448 self.encode_segment_at(&rest[..pos], head, &mut ids);
449 ids.push(id);
450 rest = &rest[pos + len..];
451 head = false;
452 }
453 None => {
454 self.encode_segment_at(rest, head, &mut ids);
455 break 'outer;
456 }
457 }
458 }
459 ids
460 }
461
462 fn encode_segment_at(&self, segment: &str, head: bool, out: &mut Vec<u32>) {
466 if segment.is_empty() {
467 return;
468 }
469 let norm: String = if self.nfc {
470 segment.nfc().collect()
471 } else {
472 segment.to_string()
473 };
474 if self.metaspace {
475 let sp = if self.sp_prepend {
479 format!("\u{2581}{}", norm).replace(' ', "\u{2581}")
482 } else {
483 let replaced = norm.replace(' ', "\u{2581}");
487 if self.sp_prepend_first && head && !replaced.starts_with('\u{2581}') {
488 format!("\u{2581}{replaced}")
489 } else {
490 replaced
491 }
492 };
493 self.bpe_piece_sp(&sp, out);
494 return;
495 }
496 if !self.split_res.is_empty() {
497 let mut pieces: Vec<(usize, usize)> = vec![(0, norm.len())];
500 for re in &self.split_res {
501 let mut next: Vec<(usize, usize)> = Vec::with_capacity(pieces.len() * 2);
502 for (ps, pe) in pieces {
503 let seg = &norm[ps..pe];
504 let mut last = 0usize;
505 for m in re.find_iter(seg) {
506 let m = match m {
507 Ok(m) => m,
508 Err(e) => {
509 tracing::error!("pre-tokenizer regex failed: {e}");
510 break;
511 }
512 };
513 if m.start() > last {
514 next.push((ps + last, ps + m.start()));
515 }
516 if m.end() > m.start() {
517 next.push((ps + m.start(), ps + m.end()));
518 }
519 last = m.end();
520 }
521 if last < seg.len() {
522 next.push((ps + last, pe));
523 }
524 }
525 pieces = next;
526 }
527 for (ps, pe) in pieces {
528 self.bpe_piece(&norm[ps..pe], out);
529 }
530 } else {
531 {
532 for b in norm.bytes() {
534 let tok = format!("<0x{:02X}>", b);
535 if let Some(&id) = self.vocab.get(&tok) {
536 out.push(id);
537 }
538 }
539 }
540 }
541 }
542
543 fn bpe_piece_sp(&self, piece: &str, out: &mut Vec<u32>) {
546 if piece.is_empty() {
547 return;
548 }
549 let mut sym: Vec<String> = piece.chars().map(|c| c.to_string()).collect();
550 loop {
551 let mut best: Option<(u32, usize)> = None;
552 for i in 0..sym.len().saturating_sub(1) {
553 if let Some(&r) = self.ranks.get(&(sym[i].clone(), sym[i + 1].clone())) {
554 if best.map(|(br, _)| r < br).unwrap_or(true) {
555 best = Some((r, i));
556 }
557 }
558 }
559 let Some((_, i)) = best else { break };
560 let merged = format!("{}{}", sym[i], sym[i + 1]);
561 let (left, right) = (sym[i].clone(), sym[i + 1].clone());
562 let mut j = 0;
563 while j + 1 < sym.len() {
564 if sym[j] == left && sym[j + 1] == right {
565 sym[j] = merged.clone();
566 sym.remove(j + 1);
567 }
568 j += 1;
569 }
570 }
571 for t in &sym {
572 if let Some(&id) = self.vocab.get(t) {
573 out.push(id);
574 } else {
575 let mut ok = true;
576 for byte in t.bytes() {
577 let tok = format!("<0x{:02X}>", byte);
578 match self.vocab.get(&tok) {
579 Some(&id) => out.push(id),
580 None => {
581 ok = false;
582 break;
583 }
584 }
585 }
586 if !ok {
587 tracing::error!("tokenizer: no id for SP symbol {t:?} — dropped");
588 }
589 }
590 }
591 }
592
593 fn bpe_piece(&self, piece: &str, out: &mut Vec<u32>) {
595 if piece.is_empty() {
596 return;
597 }
598 let mapped: Vec<String> = piece
599 .bytes()
600 .map(|b| self.byte_to_char[b as usize].to_string())
601 .collect();
602 let mut sym = mapped;
603
604 loop {
606 let mut best: Option<(u32, usize)> = None;
607 for i in 0..sym.len().saturating_sub(1) {
608 if let Some(&r) = self.ranks.get(&(sym[i].clone(), sym[i + 1].clone())) {
609 if best.map(|(br, _)| r < br).unwrap_or(true) {
610 best = Some((r, i));
611 }
612 }
613 }
614 let Some((_, i)) = best else { break };
615 let merged = format!("{}{}", sym[i], sym[i + 1]);
616 let (left, right) = (sym[i].clone(), sym[i + 1].clone());
618 let mut j = 0;
619 while j + 1 < sym.len() {
620 if sym[j] == left && sym[j + 1] == right {
621 sym[j] = merged.clone();
622 sym.remove(j + 1);
623 }
624 j += 1;
625 }
626 }
627
628 for s in &sym {
629 if let Some(&id) = self.vocab.get(s) {
630 out.push(id);
631 } else {
632 let mut ok = true;
634 for ch in s.chars() {
635 let Some(&b) = self.char_to_byte.get(&ch) else {
636 ok = false;
637 break;
638 };
639 let tok = format!("<0x{:02X}>", b);
640 if let Some(&id) = self.vocab.get(&tok) {
641 out.push(id);
642 } else {
643 ok = false;
644 break;
645 }
646 }
647 if !ok {
648 tracing::error!("tokenizer: no id for symbol {s:?} — dropped");
649 }
650 }
651 }
652 }
653
654 pub fn decode(&self, ids: &[u32]) -> String {
657 let mut bytes: Vec<u8> = Vec::new();
658 for &id in ids {
659 if self.special_ids.contains(&id) {
660 continue;
661 }
662 let idx = id as usize;
663 if idx >= self.id_to_token.len() {
664 continue;
665 }
666 let tok = &self.id_to_token[idx];
667 if self.added_ids.contains(&id) {
668 if self.metaspace && tok.contains('\u{2581}') {
671 bytes.extend_from_slice(tok.replace('\u{2581}', " ").as_bytes());
672 } else {
673 bytes.extend_from_slice(tok.as_bytes());
674 }
675 continue;
676 }
677 if tok.starts_with("<0x") && tok.ends_with('>') && tok.len() == 6 {
679 if let Ok(b) = u8::from_str_radix(&tok[3..5], 16) {
680 bytes.push(b);
681 continue;
682 }
683 }
684 if self.metaspace {
685 for ch in tok.chars() {
687 if ch == '\u{2581}' {
688 bytes.push(b' ');
689 } else {
690 let mut buf = [0u8; 4];
691 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
692 }
693 }
694 continue;
695 }
696 for ch in tok.chars() {
697 match self.char_to_byte.get(&ch) {
698 Some(&b) => bytes.push(b),
699 None => {
702 let mut buf = [0u8; 4];
703 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
704 }
705 }
706 }
707 }
708 let text = String::from_utf8_lossy(&bytes).into_owned();
709 if self.metaspace && (self.sp_prepend || self.sp_prepend_first) {
710 if let Some(stripped) = text.strip_prefix(' ') {
712 return stripped.to_string();
713 }
714 }
715 text
716 }
717
718 pub fn decode_token(&self, id: u32) -> String {
721 if self.special_ids.contains(&id) {
722 return String::new();
723 }
724 let idx = id as usize;
725 if idx >= self.id_to_token.len() {
726 return String::new();
727 }
728 let tok = &self.id_to_token[idx];
729 if self.added_ids.contains(&id) {
730 if self.metaspace && tok.contains('\u{2581}') {
731 return tok.replace('\u{2581}', " ");
732 }
733 return tok.clone();
734 }
735 if tok.starts_with("<0x") && tok.ends_with('>') && tok.len() == 6 {
736 if let Ok(b) = u8::from_str_radix(&tok[3..5], 16) {
737 return String::from_utf8_lossy(&[b]).into_owned();
738 }
739 }
740 if self.metaspace {
741 return tok.replace('\u{2581}', " ");
742 }
743 let mut bytes = Vec::new();
744 for ch in tok.chars() {
745 match self.char_to_byte.get(&ch) {
746 Some(&b) => bytes.push(b),
747 None => {
748 let mut buf = [0u8; 4];
749 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
750 }
751 }
752 }
753 String::from_utf8_lossy(&bytes).into_owned()
754 }
755
756 pub fn apply_chat_template(&self, messages: &[(String, String)]) -> Vec<u32> {
760 self.apply_chat_template_opts(messages, None)
761 }
762
763 pub fn apply_chat_template_json(
780 &self,
781 messages: &[serde_json::Value],
782 tools: Option<&[serde_json::Value]>,
783 enable_thinking: Option<bool>,
784 ) -> Vec<u32> {
785 if let Some(tpl) = &self.chat_template {
786 match self.render_template_json(tpl, messages, tools, enable_thinking) {
787 Ok(text) => return self.with_bos(self.encode(&text)),
788 Err(e) => {
789 tracing::error!("chat template render failed ({e}); ChatML fallback");
790 }
791 }
792 }
793 let pairs: Vec<(String, String)> = messages
794 .iter()
795 .map(|m| {
796 (
797 m.get("role")
798 .and_then(|v| v.as_str())
799 .unwrap_or("user")
800 .to_string(),
801 m.get("content")
802 .and_then(|v| v.as_str())
803 .unwrap_or("")
804 .to_string(),
805 )
806 })
807 .collect();
808 self.with_bos(self.chatml_fallback_opts(&pairs, enable_thinking))
809 }
810
811 pub fn render_chat_json(
813 &self,
814 messages: &[serde_json::Value],
815 tools: Option<&[serde_json::Value]>,
816 enable_thinking: Option<bool>,
817 ) -> Option<String> {
818 let tpl = self.chat_template.as_ref()?;
819 match self.render_template_json(tpl, messages, tools, enable_thinking) {
820 Ok(t) => Some(t),
821 Err(e) => {
822 tracing::error!("chat template render (json): {e:#}");
823 eprintln!("chat template render (json): {e:#}");
824 None
825 }
826 }
827 }
828
829 fn render_template_json(
830 &self,
831 tpl: &str,
832 messages: &[serde_json::Value],
833 tools: Option<&[serde_json::Value]>,
834 enable_thinking: Option<bool>,
835 ) -> Result<String, minijinja::Error> {
836 let mut env = minijinja::Environment::new();
837 env.set_trim_blocks(true);
838 env.set_lstrip_blocks(true);
839 env.set_unknown_method_callback(minijinja_contrib::pycompat::unknown_method_callback);
840 env.add_function("visible_text", |v: minijinja::Value| -> String {
846 if let Some(s) = v.as_str() {
847 return s.to_string();
848 }
849 if let Ok(iter) = v.try_iter() {
850 let mut out = Vec::new();
851 for item in iter {
852 if let Some(s) = item.as_str() {
853 out.push(s.to_string());
854 } else if let Ok(t) = item.get_attr("text") {
855 if let Some(s) = t.as_str() {
856 out.push(s.to_string());
857 }
858 }
859 }
860 return out.join("\n");
861 }
862 String::new()
863 });
864 env.add_template("chat", tpl)?;
865 let msgs: Vec<minijinja::Value> = messages
866 .iter()
867 .map(minijinja::Value::from_serialize)
868 .collect();
869 let tools_v: Option<Vec<minijinja::Value>> =
870 tools.map(|ts| ts.iter().map(minijinja::Value::from_serialize).collect());
871 let tpl = env.get_template("chat")?;
872 let rendered = match (tools_v, enable_thinking) {
879 (Some(ts), Some(v)) => tpl.render(minijinja::context! {
880 messages => msgs, tools => ts, add_generation_prompt => true, enable_thinking => v,
881 tool_call_format => "json",
882 })?,
883 (Some(ts), None) => tpl.render(minijinja::context! {
884 messages => msgs, tools => ts, add_generation_prompt => true,
885 tool_call_format => "json",
886 })?,
887 (None, Some(v)) => tpl.render(minijinja::context! {
888 messages => msgs, add_generation_prompt => true, enable_thinking => v,
889 tool_call_format => "json",
890 })?,
891 (None, None) => tpl.render(minijinja::context! {
892 messages => msgs, add_generation_prompt => true,
893 tool_call_format => "json",
894 })?,
895 };
896 Ok(rendered)
897 }
898
899 pub fn apply_chat_template_opts(
900 &self,
901 messages: &[(String, String)],
902 enable_thinking: Option<bool>,
903 ) -> Vec<u32> {
904 if let Some(tpl) = &self.chat_template {
905 match self.render_template(tpl, messages, enable_thinking) {
906 Ok(text) => return self.with_bos(self.encode(&text)),
907 Err(e) => {
908 tracing::error!("chat template render failed ({e}); ChatML fallback");
909 }
910 }
911 }
912 self.with_bos(self.chatml_fallback_opts(messages, enable_thinking))
913 }
914
915 pub fn with_bos(&self, mut ids: Vec<u32>) -> Vec<u32> {
917 if self.add_bos {
918 if let Some(b) = self.bos_token_id {
919 if ids.first() != Some(&b) {
920 ids.insert(0, b);
921 }
922 }
923 }
924 ids
925 }
926
927 pub fn render_chat(&self, messages: &[(String, String)]) -> Option<String> {
929 self.render_chat_opts(messages, None)
930 }
931
932 pub fn render_chat_opts(
934 &self,
935 messages: &[(String, String)],
936 enable_thinking: Option<bool>,
937 ) -> Option<String> {
938 let tpl = self.chat_template.as_ref()?;
939 match self.render_template(tpl, messages, enable_thinking) {
940 Ok(t) => Some(t),
941 Err(e) => {
942 tracing::error!("chat template render: {e:#}");
943 None
944 }
945 }
946 }
947
948 fn render_template(
949 &self,
950 tpl: &str,
951 messages: &[(String, String)],
952 enable_thinking: Option<bool>,
953 ) -> Result<String, minijinja::Error> {
954 let mut env = minijinja::Environment::new();
955 env.set_trim_blocks(true);
956 env.set_lstrip_blocks(true);
957 env.set_unknown_method_callback(minijinja_contrib::pycompat::unknown_method_callback);
959 env.add_template("chat", tpl)?;
960 let msgs: Vec<minijinja::Value> = messages
961 .iter()
962 .map(|(role, content)| {
963 minijinja::context! { role => role, content => content }
964 })
965 .collect();
966 let rendered = match enable_thinking {
969 Some(v) => env.get_template("chat")?.render(minijinja::context! {
970 messages => msgs,
971 add_generation_prompt => true,
972 enable_thinking => v,
973 })?,
974 None => env.get_template("chat")?.render(minijinja::context! {
975 messages => msgs,
976 add_generation_prompt => true,
977 })?,
978 };
979 if enable_thinking == Some(false) && !rendered.contains("</think>") {
983 if let Some(pos) = rendered.rfind("assistant") {
984 let mut insert_at = pos + "assistant".len();
985 if let Some(idx) = rendered[insert_at..].find('\n') {
986 insert_at += idx + 1;
987 }
988 let mut out = String::with_capacity(rendered.len() + 24);
989 out.push_str(&rendered[..insert_at]);
990 if !out.ends_with('\n') {
991 out.push('\n');
992 }
993 out.push_str("<think>\n\n</think>\n\n");
994 out.push_str(&rendered[insert_at..]);
995 return Ok(out);
996 }
997 }
998 Ok(rendered)
999 }
1000
1001 fn chatml_fallback(&self, messages: &[(String, String)]) -> Vec<u32> {
1003 self.chatml_fallback_opts(messages, None)
1004 }
1005
1006 fn chatml_fallback_opts(
1008 &self,
1009 messages: &[(String, String)],
1010 enable_thinking: Option<bool>,
1011 ) -> Vec<u32> {
1012 let mut tokens = Vec::new();
1013
1014 for (role, content) in messages {
1015 if let Some(start_id) = self.im_start_id {
1017 tokens.push(start_id);
1018 }
1019 tokens.extend(self.encode(&format!("{}\n{}", role, content)));
1020 if let Some(end_id) = self.im_end_id {
1021 tokens.push(end_id);
1022 }
1023 tokens.extend(self.encode("\n"));
1024 }
1025
1026 if let Some(start_id) = self.im_start_id {
1028 tokens.push(start_id);
1029 }
1030 tokens.extend(self.encode("assistant\n"));
1031 if enable_thinking == Some(false) {
1032 tokens.extend(self.encode("<think>\n\n</think>\n\n"));
1033 }
1034
1035 tokens
1036 }
1037
1038 pub fn vocab_size(&self) -> usize {
1040 self.id_to_token.len()
1041 }
1042
1043 pub fn is_eos(&self, id: u32) -> bool {
1045 self.eos_token_id == Some(id) || self.im_end_id == Some(id) || self.extra_eos.contains(&id)
1046 }
1047}
1048
1049#[derive(Debug, thiserror::Error)]
1050pub enum TokenizerError {
1051 #[error("IO error: {0}")]
1052 Io(String),
1053 #[error("Parse error: {0}")]
1054 Parse(String),
1055}
1056
1057#[cfg(test)]
1058mod tests {
1059 use super::*;
1060
1061 #[test]
1062 fn byte_unicode_bijection() {
1063 let (b2c, c2b) = bytes_to_unicode();
1064 for b in 0..=255u8 {
1065 assert_eq!(c2b[&b2c[b as usize]], b);
1066 }
1067 assert_eq!(b2c[b' ' as usize], 'Ġ');
1069 assert_eq!(b2c[b'\n' as usize], 'Ċ');
1070 }
1071
1072 #[test]
1073 fn byte_level_roundtrip_utf8() {
1074 let tok = Tokenizer::byte_level();
1075 let text = "hello 🌍 hi\n";
1076 let ids = tok.encode(text);
1077 assert_eq!(ids.len(), text.len()); assert_eq!(tok.decode(&ids), text);
1079 }
1080
1081 fn mini_json() -> String {
1084 let vocab: Vec<(&str, u32)> = vec![
1086 ("h", 0),
1087 ("e", 1),
1088 ("l", 2),
1089 ("o", 3),
1090 ("Ġ", 4),
1091 ("w", 5),
1092 ("r", 6),
1093 ("d", 7),
1094 ("he", 8),
1095 ("Ġw", 9),
1096 ];
1097 let vocab_json: String = vocab
1098 .iter()
1099 .map(|(t, i)| format!("\"{t}\": {i}"))
1100 .collect::<Vec<_>>()
1101 .join(", ");
1102 format!(
1103 r#"{{
1104 "model": {{
1105 "type": "BPE",
1106 "vocab": {{ {vocab_json} }},
1107 "merges": [["h", "e"], ["Ġ", "w"]]
1108 }},
1109 "added_tokens": [
1110 {{"id": 10, "content": "<|eot|>", "special": true}}
1111 ]
1112 }}"#
1113 )
1114 }
1115
1116 #[test]
1119 fn real_tokenizer_parity_when_available() {
1120 let Ok(path) = std::env::var("CMF_TOK_PARITY") else {
1121 return;
1122 };
1123 let t = Tokenizer::from_file(&path).expect("load");
1124 for (text, want) in [
1125 (
1126 "The capital of France is",
1127 vec![671u32, 6102, 294, 8760, 344],
1128 ),
1129 ("2 + 2 =", vec![20, 940, 223, 20, 438]),
1130 ] {
1131 let got = t.encode(text);
1132 assert_eq!(got, want, "«{text}»");
1133 }
1134 }
1135
1136 #[test]
1142 fn every_split_in_a_sequence_is_applied() {
1143 let pt = serde_json::json!({
1144 "type": "Sequence",
1145 "pretokenizers": [
1146 {"type": "Split", "behavior": "Isolated",
1147 "pattern": {"Regex": r"\p{N}{1,3}"}},
1148 {"type": "Split", "behavior": "Isolated",
1149 "pattern": {"Regex": r" ?[\p{L}]+"}},
1150 {"type": "ByteLevel", "add_prefix_space": false, "use_regex": false}
1151 ]
1152 });
1153 let mut pats = Vec::new();
1154 collect_split_patterns(&pt, &mut pats);
1155 assert_eq!(
1156 pats.len(),
1157 2,
1158 "both Split stages must be collected: {pats:?}"
1159 );
1160 assert!(pats[0].contains("p{N}"), "digit rule first");
1161 assert!(pats[1].contains("p{L}"), "word rule second");
1162
1163 let re: Vec<fancy_regex::Regex> = pats
1167 .iter()
1168 .map(|p| fancy_regex::Regex::new(p).unwrap())
1169 .collect();
1170 let norm = "ab cd12";
1171 let mut pieces: Vec<(usize, usize)> = vec![(0, norm.len())];
1172 for r in &re {
1173 let mut next = Vec::new();
1174 for (ps, pe) in pieces {
1175 let seg = &norm[ps..pe];
1176 let mut last = 0;
1177 for m in r.find_iter(seg).flatten() {
1178 if m.start() > last {
1179 next.push((ps + last, ps + m.start()));
1180 }
1181 if m.end() > m.start() {
1182 next.push((ps + m.start(), ps + m.end()));
1183 }
1184 last = m.end();
1185 }
1186 if last < seg.len() {
1187 next.push((ps + last, pe));
1188 }
1189 }
1190 pieces = next;
1191 }
1192 let got: Vec<&str> = pieces.iter().map(|(a, b)| &norm[*a..*b]).collect();
1193 assert_eq!(
1194 got,
1195 vec!["ab", " cd", "12"],
1196 "staged split produced {got:?}"
1197 );
1198 }
1199
1200 #[test]
1201 fn full_pipeline_merges_and_added_tokens() {
1202 let tok = Tokenizer::from_json(&mini_json()).unwrap();
1203 let ids = tok.encode("hello world");
1205 assert_eq!(ids, vec![8, 2, 2, 3, 9, 3, 6, 2, 7]);
1206 assert_eq!(tok.decode(&ids), "hello world");
1207 let ids2 = tok.encode("he<|eot|>he");
1209 assert_eq!(ids2, vec![8, 10, 8]);
1210 assert_eq!(tok.decode(&ids2), "hehe");
1211 }
1212
1213 #[test]
1214 fn non_ascii_is_never_silently_dropped() {
1215 let tok = Tokenizer::from_json(&mini_json()).unwrap();
1216 let ids = tok.encode("hello");
1219 assert!(!ids.is_empty());
1220 }
1221}