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 const TOOL_MARKUP_TOKENS: &[&str] = &[
33 "<tool_call>",
34 "</tool_call>",
35 "<function",
36 "</function>",
37 "<param",
38 "</param>",
39];
40
41pub struct Tokenizer {
43 vocab: HashMap<String, u32>,
45 id_to_token: Vec<String>,
47 ranks: HashMap<(String, String), u32>,
49 added: Vec<(String, u32)>,
51 added_ids: HashSet<u32>,
53 special_ids: HashSet<u32>,
55 split_res: Vec<fancy_regex::Regex>,
59 sp_prepend: bool,
62 sp_prepend_first: bool,
68 metaspace: bool,
71 nfc: bool,
74 byte_to_char: [char; 256],
76 char_to_byte: HashMap<char, u8>,
78 pub bos_token_id: Option<u32>,
80 pub eos_token_id: Option<u32>,
81 pub pad_token_id: Option<u32>,
82 pub im_start_id: Option<u32>,
84 pub im_end_id: Option<u32>,
85 pub chat_template: Option<String>,
88 pub extra_eos: HashSet<u32>,
90 pub add_bos: bool,
92}
93
94impl std::fmt::Debug for Tokenizer {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 f.debug_struct("Tokenizer")
97 .field("vocab", &self.vocab.len())
98 .field("merges", &self.ranks.len())
99 .field("added", &self.added.len())
100 .finish()
101 }
102}
103
104fn bytes_to_unicode() -> ([char; 256], HashMap<char, u8>) {
107 let mut b2c = ['\0'; 256];
108 let mut c2b = HashMap::with_capacity(256);
109 let mut n = 0u32;
110 for b in 0..=255u16 {
111 let printable =
112 (0x21..=0x7E).contains(&b) || (0xA1..=0xAC).contains(&b) || (0xAE..=0xFF).contains(&b);
113 let c = if printable {
114 char::from_u32(b as u32).unwrap()
115 } else {
116 let c = char::from_u32(256 + n).unwrap();
117 n += 1;
118 c
119 };
120 b2c[b as usize] = c;
121 c2b.insert(c, b as u8);
122 }
123 (b2c, c2b)
124}
125
126#[derive(Deserialize)]
128struct HfTokenizerJson {
129 model: HfModel,
130 #[serde(default)]
131 added_tokens: Vec<HfAddedToken>,
132 #[serde(default)]
133 pre_tokenizer: Option<serde_json::Value>,
134 #[serde(default)]
135 normalizer: Option<serde_json::Value>,
136 #[serde(default)]
137 post_processor: Option<serde_json::Value>,
138}
139
140#[derive(Deserialize)]
141struct HfModel {
142 vocab: HashMap<String, u32>,
143 #[serde(default)]
144 merges: Vec<HfMerge>,
145 #[serde(default)]
146 byte_fallback: bool,
147}
148
149#[derive(Deserialize)]
152#[serde(untagged)]
153enum HfMerge {
154 Pair([String; 2]),
155 Text(String),
156}
157
158#[derive(Deserialize)]
159struct HfAddedToken {
160 id: u32,
161 content: String,
162 special: bool,
163}
164
165fn collect_split_patterns(pt: &serde_json::Value, out: &mut Vec<String>) {
174 if pt.get("type").and_then(|t| t.as_str()) == Some("Split") {
175 if let Some(r) = pt
176 .get("pattern")
177 .and_then(|p| p.get("Regex"))
178 .and_then(|r| r.as_str())
179 {
180 out.push(r.to_string());
181 }
182 return;
183 }
184 if let Some(list) = pt.get("pretokenizers").and_then(|l| l.as_array()) {
185 for p in list {
186 collect_split_patterns(p, out);
187 }
188 }
189}
190
191fn find_prepend_scheme(pt: &serde_json::Value) -> Option<String> {
194 if pt.get("type").and_then(|t| t.as_str()) == Some("Metaspace") {
195 return pt
196 .get("prepend_scheme")
197 .and_then(|p| p.as_str())
198 .map(String::from);
199 }
200 if let Some(list) = pt.get("pretokenizers").and_then(|l| l.as_array()) {
201 return list.iter().find_map(find_prepend_scheme);
202 }
203 None
204}
205
206pub(crate) fn strip_generation_tags(tpl: &str) -> std::borrow::Cow<'_, str> {
222 if !tpl.contains("generation") {
223 return std::borrow::Cow::Borrowed(tpl);
224 }
225 let mut out = String::with_capacity(tpl.len());
226 let mut rest = tpl;
227 let mut touched = false;
228 while let Some(open) = rest.find("{%") {
229 let Some(close_rel) = rest[open..].find("%}") else {
230 break;
231 };
232 let close = open + close_rel + 2;
233 let tag = &rest[open..close];
234 let inner = tag[2..tag.len() - 2].trim();
235 let lead = inner.starts_with('-');
236 let trail = inner.ends_with('-');
237 let name = inner.trim_matches('-').trim();
238 out.push_str(&rest[..open]);
239 if name == "generation" || name == "endgeneration" {
240 out.push_str(if lead { "{%-" } else { "{%" });
241 out.push_str(" set _generation_span = true ");
242 out.push_str(if trail { "-%}" } else { "%}" });
243 touched = true;
244 } else {
245 out.push_str(tag);
246 }
247 rest = &rest[close..];
248 }
249 if !touched {
250 return std::borrow::Cow::Borrowed(tpl);
251 }
252 out.push_str(rest);
253 std::borrow::Cow::Owned(out)
254}
255
256impl Tokenizer {
257 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, TokenizerError> {
259 let data = std::fs::read_to_string(path.as_ref())
260 .map_err(|e| TokenizerError::Io(e.to_string()))?;
261 Self::from_json(&data)
262 }
263
264 pub fn from_bytes(bytes: &[u8]) -> Result<Self, TokenizerError> {
266 let s = std::str::from_utf8(bytes)
267 .map_err(|e| TokenizerError::Parse(format!("vocab is not UTF-8: {e}")))?;
268 Self::from_json(s)
269 }
270
271 pub fn from_json(json: &str) -> Result<Self, TokenizerError> {
273 let hf: HfTokenizerJson =
274 serde_json::from_str(json).map_err(|e| TokenizerError::Parse(e.to_string()))?;
275
276 let mut vocab = hf.model.vocab;
277 let mut ranks = HashMap::new();
278 for (rank, m) in hf.model.merges.into_iter().enumerate() {
279 let (a, b) = match m {
280 HfMerge::Pair([a, b]) => (a, b),
281 HfMerge::Text(s) => {
282 let mut it = s.splitn(2, ' ');
283 match (it.next(), it.next()) {
284 (Some(a), Some(b)) => (a.to_string(), b.to_string()),
285 _ => continue,
286 }
287 }
288 };
289 ranks.insert((a, b), rank as u32);
290 }
291
292 let mut saw_gemma_bos = false;
297 let add_bos_detected = hf
298 .post_processor
299 .as_ref()
300 .map(|p| {
301 let pp = p.to_string();
302 pp.contains("\"<s>\"") || pp.contains("\"<bos>\"")
303 })
304 .unwrap_or(false);
305 let nfc = hf
306 .normalizer
307 .as_ref()
308 .map(|n| n.to_string().contains("NFC"))
309 .unwrap_or(false);
310 let metaspace = hf.model.byte_fallback
311 || hf
312 .normalizer
313 .as_ref()
314 .map(|n| n.to_string().contains("\u{2581}") || n.to_string().contains("▁"))
315 .unwrap_or(false);
316 let sp_prepend = hf
317 .normalizer
318 .as_ref()
319 .map(|n| n.to_string().contains("Prepend"))
320 .unwrap_or(false);
321 let (sp_prepend, sp_prepend_first) = if sp_prepend {
325 (true, false)
326 } else {
327 match hf.pre_tokenizer.as_ref().and_then(find_prepend_scheme) {
328 Some(s) if s == "always" => (true, false),
329 Some(s) if s == "first" => (false, true),
330 _ => (false, false),
331 }
332 };
333 let split_res = if metaspace {
334 Vec::new()
335 } else {
336 let mut pats = Vec::new();
337 if let Some(pt) = hf.pre_tokenizer.as_ref() {
338 collect_split_patterns(pt, &mut pats);
339 }
340 if pats.is_empty() {
341 pats.push(DEFAULT_SPLIT.to_string());
342 }
343 pats.iter()
344 .map(|p| {
345 fancy_regex::Regex::new(p)
346 .map_err(|e| TokenizerError::Parse(format!("pre-tokenizer regex: {e}")))
347 })
348 .collect::<Result<Vec<_>, _>>()?
349 };
350
351 let mut bos_token_id = None;
353 let mut eos_token_id = None;
354 let mut pad_token_id = None;
355 let mut im_start_id = None;
356 let mut im_end_id = None;
357 let mut special_ids = HashSet::new();
358 let mut added_ids = HashSet::new();
359 let mut added = Vec::new();
360
361 for at in &hf.added_tokens {
362 vocab.insert(at.content.clone(), at.id);
363 added.push((at.content.clone(), at.id));
364 added_ids.insert(at.id);
365 if at.special && !TOOL_MARKUP_TOKENS.contains(&at.content.as_str()) {
366 special_ids.insert(at.id);
367 }
368 match at.content.as_str() {
369 "<|endoftext|>" | "</s>" | "[EOS]" => eos_token_id = Some(at.id),
370 "<|im_start|>" => im_start_id = Some(at.id),
371 "<|im_end|>" => im_end_id = Some(at.id),
372 "<s>" | "[BOS]" => bos_token_id = Some(at.id),
373 "<bos>" => {
377 bos_token_id = Some(at.id);
378 saw_gemma_bos = true;
379 }
380 "<pad>" => pad_token_id = Some(at.id),
381 _ => {}
382 }
383 }
384
385 const DSV41_SPECIALS: &[&str] = &[
390 "<|begin▁of▁sentence|>",
391 "<|end▁of▁sentence|>",
392 "<|User|>",
393 "<|Assistant|>",
394 "<|System|>",
395 "<|latest_reminder|>",
396 "<|deepseek_image|>",
397 "<|action|>",
398 "<|query|>",
399 "<|authority|>",
400 "<|domain|>",
401 "<|title|>",
402 "<|read_url|>",
403 "<think>",
404 "</think>",
405 "|DSML|",
406 ];
407 for token in DSV41_SPECIALS {
408 if let Some(&id) = vocab.get(*token) {
409 if !added.iter().any(|(content, _)| content.as_str() == *token) {
410 added.push(((*token).to_string(), id));
411 }
412 added_ids.insert(id);
413 match *token {
414 "<|begin▁of▁sentence|>" => bos_token_id = Some(id),
415 "<|end▁of▁sentence|>" => eos_token_id = Some(id),
416 _ => {}
417 }
418 }
419 }
420 added.sort_by_key(|(c, _)| std::cmp::Reverse(c.len()));
421
422 let gemma_family = saw_gemma_bos
429 || vocab.contains_key("<start_of_turn>")
430 || added.iter().any(|(c, _)| c == "<start_of_turn>");
431
432 if let Some(pp) = hf.post_processor.as_ref() {
436 let pp = pp.to_string();
437 for name in ["<bos>", "<s>"] {
438 if pp.contains(&format!("\"{name}\"")) {
439 if let Some(&id) = vocab.get(name) {
440 bos_token_id = Some(id);
441 }
442 break;
443 }
444 }
445 }
446
447 let max_id = vocab.values().copied().max().unwrap_or(0) as usize;
449 let mut id_to_token = vec![String::new(); max_id + 1];
450 for (token, &id) in &vocab {
451 if (id as usize) < id_to_token.len() {
452 id_to_token[id as usize] = token.clone();
453 }
454 }
455
456 let (byte_to_char, char_to_byte) = bytes_to_unicode();
457
458 tracing::info!(
459 "Tokenizer loaded: {} vocab, {} merges, {} added, eos={:?}",
460 vocab.len(),
461 ranks.len(),
462 added.len(),
463 eos_token_id
464 );
465
466 Ok(Self {
467 vocab,
468 id_to_token,
469 ranks,
470 added,
471 added_ids,
472 special_ids,
473 split_res,
474 metaspace,
475 sp_prepend,
476 sp_prepend_first,
477 nfc,
478 byte_to_char,
479 char_to_byte,
480 bos_token_id,
481 eos_token_id,
482 pad_token_id,
483 im_start_id,
484 im_end_id,
485 chat_template: None,
486 extra_eos: HashSet::new(),
487 add_bos: add_bos_detected || gemma_family,
488 })
489 }
490
491 pub fn byte_level() -> Self {
493 let mut vocab = HashMap::new();
494 let mut id_to_token = Vec::with_capacity(256);
495 for i in 0..256u32 {
496 let tok = format!("<0x{:02X}>", i);
497 vocab.insert(tok.clone(), i);
498 id_to_token.push(tok);
499 }
500 let (byte_to_char, char_to_byte) = bytes_to_unicode();
501 Self {
502 vocab,
503 id_to_token,
504 ranks: HashMap::new(),
505 added: Vec::new(),
506 added_ids: HashSet::new(),
507 special_ids: HashSet::new(),
508 split_res: Vec::new(),
509 metaspace: false,
510 sp_prepend: false,
511 sp_prepend_first: false,
512 nfc: false,
513 byte_to_char,
514 char_to_byte,
515 bos_token_id: None,
516 eos_token_id: None,
517 pad_token_id: None,
518 im_start_id: None,
519 im_end_id: None,
520 chat_template: None,
521 extra_eos: HashSet::new(),
522 add_bos: false,
523 }
524 }
525
526 pub fn encode(&self, text: &str) -> Vec<u32> {
528 let mut ids = Vec::new();
529 let mut rest = text;
531 let mut head = true;
536 'outer: while !rest.is_empty() {
537 let mut best: Option<(usize, usize, u32)> = None; for (content, id) in &self.added {
539 if let Some(pos) = rest.find(content.as_str()) {
540 let better = match best {
541 None => true,
542 Some((bp, bl, _)) => pos < bp || (pos == bp && content.len() > bl),
543 };
544 if better {
545 best = Some((pos, content.len(), *id));
546 }
547 if pos == 0 {
548 break; }
550 }
551 }
552 match best {
553 Some((pos, len, id)) => {
554 self.encode_segment_at(&rest[..pos], head, &mut ids);
555 ids.push(id);
556 rest = &rest[pos + len..];
557 head = false;
558 }
559 None => {
560 self.encode_segment_at(rest, head, &mut ids);
561 break 'outer;
562 }
563 }
564 }
565 ids
566 }
567
568 fn encode_segment_at(&self, segment: &str, head: bool, out: &mut Vec<u32>) {
572 if segment.is_empty() {
573 return;
574 }
575 let norm: String = if self.nfc {
576 segment.nfc().collect()
577 } else {
578 segment.to_string()
579 };
580 if self.metaspace {
581 let sp = if self.sp_prepend {
585 format!("\u{2581}{}", norm).replace(' ', "\u{2581}")
588 } else {
589 let replaced = norm.replace(' ', "\u{2581}");
593 if self.sp_prepend_first && head && !replaced.starts_with('\u{2581}') {
594 format!("\u{2581}{replaced}")
595 } else {
596 replaced
597 }
598 };
599 self.bpe_piece_sp(&sp, out);
600 return;
601 }
602 if !self.split_res.is_empty() {
603 let mut pieces: Vec<(usize, usize)> = vec![(0, norm.len())];
606 for re in &self.split_res {
607 let mut next: Vec<(usize, usize)> = Vec::with_capacity(pieces.len() * 2);
608 for (ps, pe) in pieces {
609 let seg = &norm[ps..pe];
610 let mut last = 0usize;
611 for m in re.find_iter(seg) {
612 let m = match m {
613 Ok(m) => m,
614 Err(e) => {
615 tracing::error!("pre-tokenizer regex failed: {e}");
616 break;
617 }
618 };
619 if m.start() > last {
620 next.push((ps + last, ps + m.start()));
621 }
622 if m.end() > m.start() {
623 next.push((ps + m.start(), ps + m.end()));
624 }
625 last = m.end();
626 }
627 if last < seg.len() {
628 next.push((ps + last, pe));
629 }
630 }
631 pieces = next;
632 }
633 for (ps, pe) in pieces {
634 self.bpe_piece(&norm[ps..pe], out);
635 }
636 } else {
637 {
638 for b in norm.bytes() {
640 let tok = format!("<0x{:02X}>", b);
641 if let Some(&id) = self.vocab.get(&tok) {
642 out.push(id);
643 }
644 }
645 }
646 }
647 }
648
649 fn bpe_piece_sp(&self, piece: &str, out: &mut Vec<u32>) {
652 if piece.is_empty() {
653 return;
654 }
655 let mut sym: Vec<String> = piece.chars().map(|c| c.to_string()).collect();
656 loop {
657 let mut best: Option<(u32, usize)> = None;
658 for i in 0..sym.len().saturating_sub(1) {
659 if let Some(&r) = self.ranks.get(&(sym[i].clone(), sym[i + 1].clone())) {
660 if best.map(|(br, _)| r < br).unwrap_or(true) {
661 best = Some((r, i));
662 }
663 }
664 }
665 let Some((_, i)) = best else { break };
666 let merged = format!("{}{}", sym[i], sym[i + 1]);
667 let (left, right) = (sym[i].clone(), sym[i + 1].clone());
668 let mut j = 0;
669 while j + 1 < sym.len() {
670 if sym[j] == left && sym[j + 1] == right {
671 sym[j] = merged.clone();
672 sym.remove(j + 1);
673 }
674 j += 1;
675 }
676 }
677 for t in &sym {
678 if let Some(&id) = self.vocab.get(t) {
679 out.push(id);
680 } else {
681 let mut ok = true;
682 for byte in t.bytes() {
683 let tok = format!("<0x{:02X}>", byte);
684 match self.vocab.get(&tok) {
685 Some(&id) => out.push(id),
686 None => {
687 ok = false;
688 break;
689 }
690 }
691 }
692 if !ok {
693 tracing::error!("tokenizer: no id for SP symbol {t:?} — dropped");
694 }
695 }
696 }
697 }
698
699 fn bpe_piece(&self, piece: &str, out: &mut Vec<u32>) {
701 if piece.is_empty() {
702 return;
703 }
704 let mapped: Vec<String> = piece
705 .bytes()
706 .map(|b| self.byte_to_char[b as usize].to_string())
707 .collect();
708 let mut sym = mapped;
709
710 loop {
712 let mut best: Option<(u32, usize)> = None;
713 for i in 0..sym.len().saturating_sub(1) {
714 if let Some(&r) = self.ranks.get(&(sym[i].clone(), sym[i + 1].clone())) {
715 if best.map(|(br, _)| r < br).unwrap_or(true) {
716 best = Some((r, i));
717 }
718 }
719 }
720 let Some((_, i)) = best else { break };
721 let merged = format!("{}{}", sym[i], sym[i + 1]);
722 let (left, right) = (sym[i].clone(), sym[i + 1].clone());
724 let mut j = 0;
725 while j + 1 < sym.len() {
726 if sym[j] == left && sym[j + 1] == right {
727 sym[j] = merged.clone();
728 sym.remove(j + 1);
729 }
730 j += 1;
731 }
732 }
733
734 for s in &sym {
735 if let Some(&id) = self.vocab.get(s) {
736 out.push(id);
737 } else {
738 let mut ok = true;
740 for ch in s.chars() {
741 let Some(&b) = self.char_to_byte.get(&ch) else {
742 ok = false;
743 break;
744 };
745 let tok = format!("<0x{:02X}>", b);
746 if let Some(&id) = self.vocab.get(&tok) {
747 out.push(id);
748 } else {
749 ok = false;
750 break;
751 }
752 }
753 if !ok {
754 tracing::error!("tokenizer: no id for symbol {s:?} — dropped");
755 }
756 }
757 }
758 }
759
760 pub fn decode(&self, ids: &[u32]) -> String {
763 let mut bytes: Vec<u8> = Vec::new();
764 for &id in ids {
765 if self.special_ids.contains(&id) {
766 continue;
767 }
768 let idx = id as usize;
769 if idx >= self.id_to_token.len() {
770 continue;
771 }
772 let tok = &self.id_to_token[idx];
773 if self.added_ids.contains(&id) {
774 if self.metaspace && tok.contains('\u{2581}') {
777 bytes.extend_from_slice(tok.replace('\u{2581}', " ").as_bytes());
778 } else {
779 bytes.extend_from_slice(tok.as_bytes());
780 }
781 continue;
782 }
783 if tok.starts_with("<0x") && tok.ends_with('>') && tok.len() == 6 {
785 if let Ok(b) = u8::from_str_radix(&tok[3..5], 16) {
786 bytes.push(b);
787 continue;
788 }
789 }
790 if self.metaspace {
791 for ch in tok.chars() {
793 if ch == '\u{2581}' {
794 bytes.push(b' ');
795 } else {
796 let mut buf = [0u8; 4];
797 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
798 }
799 }
800 continue;
801 }
802 for ch in tok.chars() {
803 match self.char_to_byte.get(&ch) {
804 Some(&b) => bytes.push(b),
805 None => {
808 let mut buf = [0u8; 4];
809 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
810 }
811 }
812 }
813 }
814 let text = String::from_utf8_lossy(&bytes).into_owned();
815 if self.metaspace && (self.sp_prepend || self.sp_prepend_first) {
816 if let Some(stripped) = text.strip_prefix(' ') {
818 return stripped.to_string();
819 }
820 }
821 text
822 }
823
824 pub fn decode_token(&self, id: u32) -> String {
827 if self.special_ids.contains(&id) {
828 return String::new();
829 }
830 let idx = id as usize;
831 if idx >= self.id_to_token.len() {
832 return String::new();
833 }
834 let tok = &self.id_to_token[idx];
835 if self.added_ids.contains(&id) {
836 if self.metaspace && tok.contains('\u{2581}') {
837 return tok.replace('\u{2581}', " ");
838 }
839 return tok.clone();
840 }
841 if tok.starts_with("<0x") && tok.ends_with('>') && tok.len() == 6 {
842 if let Ok(b) = u8::from_str_radix(&tok[3..5], 16) {
843 return String::from_utf8_lossy(&[b]).into_owned();
844 }
845 }
846 if self.metaspace {
847 return tok.replace('\u{2581}', " ");
848 }
849 let mut bytes = Vec::new();
850 for ch in tok.chars() {
851 match self.char_to_byte.get(&ch) {
852 Some(&b) => bytes.push(b),
853 None => {
854 let mut buf = [0u8; 4];
855 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
856 }
857 }
858 }
859 String::from_utf8_lossy(&bytes).into_owned()
860 }
861
862 pub fn decode_token_for_hash(&self, id: u32) -> String {
865 let idx = id as usize;
866 if idx >= self.id_to_token.len() {
867 return String::new();
868 }
869 if self.special_ids.contains(&id) {
870 return self.id_to_token[idx].clone();
871 }
872 self.decode_token(id)
873 }
874
875 pub fn decode_for_protocol(&self, ids: &[u32]) -> String {
878 let mut out = String::new();
879 for &id in ids {
880 let idx = id as usize;
881 if self.special_ids.contains(&id) {
882 if let Some(token) = self.id_to_token.get(idx) {
883 out.push_str(token);
884 }
885 } else {
886 out.push_str(&self.decode_token(id));
887 }
888 }
889 out
890 }
891
892 pub fn raw_token_for_hash(&self, id: u32) -> String {
894 self.id_to_token
895 .get(id as usize)
896 .cloned()
897 .unwrap_or_default()
898 }
899
900 pub fn apply_chat_template(&self, messages: &[(String, String)]) -> Vec<u32> {
904 self.apply_chat_template_opts(messages, None)
905 }
906
907 pub fn apply_chat_template_json(
924 &self,
925 messages: &[serde_json::Value],
926 tools: Option<&[serde_json::Value]>,
927 enable_thinking: Option<bool>,
928 ) -> Vec<u32> {
929 match self.try_apply_chat_template_json(messages, tools, enable_thinking) {
930 Ok(ids) => ids,
931 Err(e) => {
932 tracing::error!("chat template render failed ({e}); ChatML fallback");
933 self.chatml_json_fallback(messages, enable_thinking)
934 }
935 }
936 }
937
938 pub fn try_apply_chat_template_json(
949 &self,
950 messages: &[serde_json::Value],
951 tools: Option<&[serde_json::Value]>,
952 enable_thinking: Option<bool>,
953 ) -> Result<Vec<u32>, String> {
954 if let Some(tpl) = &self.chat_template {
955 return self
956 .render_template_json(tpl, messages, tools, enable_thinking)
957 .map(|text| self.with_bos(self.encode(&text)))
958 .map_err(|e| format!("{e:#}"));
959 }
960 Ok(self.chatml_json_fallback(messages, enable_thinking))
961 }
962
963 fn chatml_json_fallback(
964 &self,
965 messages: &[serde_json::Value],
966 enable_thinking: Option<bool>,
967 ) -> Vec<u32> {
968 let pairs: Vec<(String, String)> = messages
969 .iter()
970 .map(|m| {
971 (
972 m.get("role")
973 .and_then(|v| v.as_str())
974 .unwrap_or("user")
975 .to_string(),
976 m.get("content")
977 .and_then(|v| v.as_str())
978 .unwrap_or("")
979 .to_string(),
980 )
981 })
982 .collect();
983 self.with_bos(self.chatml_fallback_opts(&pairs, enable_thinking))
984 }
985
986 pub fn render_chat_json(
988 &self,
989 messages: &[serde_json::Value],
990 tools: Option<&[serde_json::Value]>,
991 enable_thinking: Option<bool>,
992 ) -> Option<String> {
993 let tpl = self.chat_template.as_ref()?;
994 match self.render_template_json(tpl, messages, tools, enable_thinking) {
995 Ok(t) => Some(t),
996 Err(e) => {
997 tracing::error!("chat template render (json): {e:#}");
998 eprintln!("chat template render (json): {e:#}");
999 None
1000 }
1001 }
1002 }
1003
1004 fn render_template_json(
1005 &self,
1006 tpl: &str,
1007 messages: &[serde_json::Value],
1008 tools: Option<&[serde_json::Value]>,
1009 enable_thinking: Option<bool>,
1010 ) -> Result<String, minijinja::Error> {
1011 let mut env = crate::chat_template::environment();
1012 let tpl_src = strip_generation_tags(tpl);
1013 env.add_template("chat", &tpl_src)?;
1014 let msgs: Vec<minijinja::Value> = messages
1015 .iter()
1016 .map(minijinja::Value::from_serialize)
1017 .collect();
1018 let tools_v: Option<Vec<minijinja::Value>> =
1019 tools.map(|ts| ts.iter().map(minijinja::Value::from_serialize).collect());
1020 let tpl = env.get_template("chat")?;
1021 let rendered = match (tools_v, enable_thinking) {
1028 (Some(ts), Some(v)) => tpl.render(minijinja::context! {
1029 messages => msgs, tools => ts, add_generation_prompt => true, enable_thinking => v,
1030 tool_call_format => "json",
1031 })?,
1032 (Some(ts), None) => tpl.render(minijinja::context! {
1033 messages => msgs, tools => ts, add_generation_prompt => true,
1034 tool_call_format => "json",
1035 })?,
1036 (None, Some(v)) => tpl.render(minijinja::context! {
1037 messages => msgs, add_generation_prompt => true, enable_thinking => v,
1038 tool_call_format => "json",
1039 })?,
1040 (None, None) => tpl.render(minijinja::context! {
1041 messages => msgs, add_generation_prompt => true,
1042 tool_call_format => "json",
1043 })?,
1044 };
1045 Ok(rendered)
1046 }
1047
1048 pub fn apply_chat_template_opts(
1049 &self,
1050 messages: &[(String, String)],
1051 enable_thinking: Option<bool>,
1052 ) -> Vec<u32> {
1053 if let Some(tpl) = &self.chat_template {
1054 match self.render_template(tpl, messages, enable_thinking) {
1055 Ok(text) => return self.with_bos(self.encode(&text)),
1056 Err(e) => {
1057 tracing::error!("chat template render failed ({e}); ChatML fallback");
1058 }
1059 }
1060 }
1061 self.with_bos(self.chatml_fallback_opts(messages, enable_thinking))
1062 }
1063
1064 pub fn with_bos(&self, mut ids: Vec<u32>) -> Vec<u32> {
1066 if self.add_bos {
1067 if let Some(b) = self.bos_token_id {
1068 if ids.first() != Some(&b) {
1069 ids.insert(0, b);
1070 }
1071 }
1072 }
1073 ids
1074 }
1075
1076 pub fn render_chat(&self, messages: &[(String, String)]) -> Option<String> {
1078 self.render_chat_opts(messages, None)
1079 }
1080
1081 pub fn render_chat_opts(
1083 &self,
1084 messages: &[(String, String)],
1085 enable_thinking: Option<bool>,
1086 ) -> Option<String> {
1087 let tpl = self.chat_template.as_ref()?;
1088 match self.render_template(tpl, messages, enable_thinking) {
1089 Ok(t) => Some(t),
1090 Err(e) => {
1091 tracing::error!("chat template render: {e:#}");
1092 None
1093 }
1094 }
1095 }
1096
1097 fn render_template(
1098 &self,
1099 tpl: &str,
1100 messages: &[(String, String)],
1101 enable_thinking: Option<bool>,
1102 ) -> Result<String, minijinja::Error> {
1103 let mut env = crate::chat_template::environment();
1104 let tpl_src = strip_generation_tags(tpl);
1105 env.add_template("chat", &tpl_src)?;
1106 let msgs: Vec<minijinja::Value> = messages
1107 .iter()
1108 .map(|(role, content)| {
1109 minijinja::context! { role => role, content => content }
1110 })
1111 .collect();
1112 let rendered = match enable_thinking {
1115 Some(v) => env.get_template("chat")?.render(minijinja::context! {
1116 messages => msgs,
1117 add_generation_prompt => true,
1118 enable_thinking => v,
1119 })?,
1120 None => env.get_template("chat")?.render(minijinja::context! {
1121 messages => msgs,
1122 add_generation_prompt => true,
1123 })?,
1124 };
1125 if enable_thinking == Some(false) && !rendered.contains("</think>") {
1129 if let Some(pos) = rendered.rfind("assistant") {
1130 let mut insert_at = pos + "assistant".len();
1131 if let Some(idx) = rendered[insert_at..].find('\n') {
1132 insert_at += idx + 1;
1133 }
1134 let mut out = String::with_capacity(rendered.len() + 24);
1135 out.push_str(&rendered[..insert_at]);
1136 if !out.ends_with('\n') {
1137 out.push('\n');
1138 }
1139 out.push_str("<think>\n\n</think>\n\n");
1140 out.push_str(&rendered[insert_at..]);
1141 return Ok(out);
1142 }
1143 }
1144 Ok(rendered)
1145 }
1146
1147 fn chatml_fallback(&self, messages: &[(String, String)]) -> Vec<u32> {
1149 self.chatml_fallback_opts(messages, None)
1150 }
1151
1152 fn chatml_fallback_opts(
1154 &self,
1155 messages: &[(String, String)],
1156 enable_thinking: Option<bool>,
1157 ) -> Vec<u32> {
1158 let mut tokens = Vec::new();
1159
1160 for (role, content) in messages {
1161 if let Some(start_id) = self.im_start_id {
1163 tokens.push(start_id);
1164 }
1165 tokens.extend(self.encode(&format!("{}\n{}", role, content)));
1166 if let Some(end_id) = self.im_end_id {
1167 tokens.push(end_id);
1168 }
1169 tokens.extend(self.encode("\n"));
1170 }
1171
1172 if let Some(start_id) = self.im_start_id {
1174 tokens.push(start_id);
1175 }
1176 tokens.extend(self.encode("assistant\n"));
1177 if enable_thinking == Some(false) {
1178 tokens.extend(self.encode("<think>\n\n</think>\n\n"));
1179 }
1180
1181 tokens
1182 }
1183
1184 pub fn vocab_size(&self) -> usize {
1186 self.id_to_token.len()
1187 }
1188
1189 pub fn token_to_id(&self, token: &str) -> Option<u32> {
1193 self.vocab.get(token).copied()
1194 }
1195
1196 pub fn convert_tokens_to_ids(&self, token: &str) -> Option<u32> {
1199 self.token_to_id(token)
1200 }
1201
1202 pub fn is_eos(&self, id: u32) -> bool {
1204 self.eos_token_id == Some(id) || self.im_end_id == Some(id) || self.extra_eos.contains(&id)
1205 }
1206}
1207
1208#[derive(Debug, thiserror::Error)]
1209pub enum TokenizerError {
1210 #[error("IO error: {0}")]
1211 Io(String),
1212 #[error("Parse error: {0}")]
1213 Parse(String),
1214}
1215
1216#[cfg(test)]
1217mod tests {
1218 use super::*;
1219
1220 #[test]
1221 fn byte_unicode_bijection() {
1222 let (b2c, c2b) = bytes_to_unicode();
1223 for b in 0..=255u8 {
1224 assert_eq!(c2b[&b2c[b as usize]], b);
1225 }
1226 assert_eq!(b2c[b' ' as usize], 'Ġ');
1228 assert_eq!(b2c[b'\n' as usize], 'Ċ');
1229 }
1230
1231 #[test]
1232 fn byte_level_roundtrip_utf8() {
1233 let tok = Tokenizer::byte_level();
1234 let text = "hello 🌍 hi\n";
1235 let ids = tok.encode(text);
1236 assert_eq!(ids.len(), text.len()); assert_eq!(tok.decode(&ids), text);
1238 }
1239
1240 fn mini_json() -> String {
1243 let vocab: Vec<(&str, u32)> = vec![
1245 ("h", 0),
1246 ("e", 1),
1247 ("l", 2),
1248 ("o", 3),
1249 ("Ġ", 4),
1250 ("w", 5),
1251 ("r", 6),
1252 ("d", 7),
1253 ("he", 8),
1254 ("Ġw", 9),
1255 ];
1256 let vocab_json: String = vocab
1257 .iter()
1258 .map(|(t, i)| format!("\"{t}\": {i}"))
1259 .collect::<Vec<_>>()
1260 .join(", ");
1261 format!(
1262 r#"{{
1263 "model": {{
1264 "type": "BPE",
1265 "vocab": {{ {vocab_json} }},
1266 "merges": [["h", "e"], ["Ġ", "w"]]
1267 }},
1268 "added_tokens": [
1269 {{"id": 10, "content": "<|eot|>", "special": true}}
1270 ]
1271 }}"#
1272 )
1273 }
1274
1275 #[test]
1279 fn tool_markup_decodes_even_when_special() {
1280 let json = r#"{
1281 "model": {"type": "BPE", "vocab": {"h": 0, "e": 1, "l": 2, "o": 3}, "merges": []},
1282 "added_tokens": [
1283 {"id": 10, "content": "<|im_end|>", "special": true},
1284 {"id": 11, "content": "<function", "special": true},
1285 {"id": 12, "content": "</function>", "special": true},
1286 {"id": 13, "content": "<param", "special": true},
1287 {"id": 14, "content": "</param>", "special": true},
1288 {"id": 15, "content": "<tool_call>", "special": true}
1289 ]
1290 }"#;
1291 let t = Tokenizer::from_json(json).unwrap();
1292 let ids = [11, 0, 1, 13, 2, 14, 12, 15, 10];
1293 assert_eq!(
1294 t.decode(&ids),
1295 "<functionhe<paraml</param></function><tool_call>"
1296 );
1297 let streamed: String = ids.iter().map(|&i| t.decode_token(i)).collect();
1298 assert_eq!(streamed, t.decode(&ids), "streaming must agree with decode");
1299 assert!(
1300 !t.decode(&[10]).contains("im_end"),
1301 "control tokens stay hidden"
1302 );
1303 }
1304
1305 #[test]
1308 fn real_tokenizer_parity_when_available() {
1309 let Ok(path) = std::env::var("CMF_TOK_PARITY") else {
1310 return;
1311 };
1312 let t = Tokenizer::from_file(&path).expect("load");
1313 for (text, want) in [
1314 (
1315 "The capital of France is",
1316 vec![671u32, 6102, 294, 8760, 344],
1317 ),
1318 ("2 + 2 =", vec![20, 940, 223, 20, 438]),
1319 ] {
1320 let got = t.encode(text);
1321 assert_eq!(got, want, "«{text}»");
1322 }
1323 }
1324
1325 #[test]
1330 fn granite_42_chat_template_when_available() {
1331 let Ok(path) = std::env::var("CMF_GRANITE_CHAT_TEMPLATE") else {
1332 return;
1333 };
1334 let mut tok = Tokenizer::byte_level();
1335 tok.chat_template = Some(std::fs::read_to_string(path).expect("read Granite template"));
1336 let messages = vec![("user".to_string(), "Hello".to_string())];
1337
1338 let thinking = tok
1339 .render_chat_opts(&messages, Some(true))
1340 .expect("render Granite thinking prompt");
1341 assert_eq!(
1342 thinking,
1343 "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\n<think>\n"
1344 );
1345
1346 let direct = tok
1347 .render_chat_opts(&messages, Some(false))
1348 .expect("render Granite direct prompt");
1349 assert_eq!(
1350 direct,
1351 "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\n<think></think>"
1352 );
1353 }
1354
1355 #[test]
1361 fn every_split_in_a_sequence_is_applied() {
1362 let pt = serde_json::json!({
1363 "type": "Sequence",
1364 "pretokenizers": [
1365 {"type": "Split", "behavior": "Isolated",
1366 "pattern": {"Regex": r"\p{N}{1,3}"}},
1367 {"type": "Split", "behavior": "Isolated",
1368 "pattern": {"Regex": r" ?[\p{L}]+"}},
1369 {"type": "ByteLevel", "add_prefix_space": false, "use_regex": false}
1370 ]
1371 });
1372 let mut pats = Vec::new();
1373 collect_split_patterns(&pt, &mut pats);
1374 assert_eq!(
1375 pats.len(),
1376 2,
1377 "both Split stages must be collected: {pats:?}"
1378 );
1379 assert!(pats[0].contains("p{N}"), "digit rule first");
1380 assert!(pats[1].contains("p{L}"), "word rule second");
1381
1382 let re: Vec<fancy_regex::Regex> = pats
1386 .iter()
1387 .map(|p| fancy_regex::Regex::new(p).unwrap())
1388 .collect();
1389 let norm = "ab cd12";
1390 let mut pieces: Vec<(usize, usize)> = vec![(0, norm.len())];
1391 for r in &re {
1392 let mut next = Vec::new();
1393 for (ps, pe) in pieces {
1394 let seg = &norm[ps..pe];
1395 let mut last = 0;
1396 for m in r.find_iter(seg).flatten() {
1397 if m.start() > last {
1398 next.push((ps + last, ps + m.start()));
1399 }
1400 if m.end() > m.start() {
1401 next.push((ps + m.start(), ps + m.end()));
1402 }
1403 last = m.end();
1404 }
1405 if last < seg.len() {
1406 next.push((ps + last, pe));
1407 }
1408 }
1409 pieces = next;
1410 }
1411 let got: Vec<&str> = pieces.iter().map(|(a, b)| &norm[*a..*b]).collect();
1412 assert_eq!(
1413 got,
1414 vec!["ab", " cd", "12"],
1415 "staged split produced {got:?}"
1416 );
1417 }
1418
1419 #[test]
1420 fn full_pipeline_merges_and_added_tokens() {
1421 let tok = Tokenizer::from_json(&mini_json()).unwrap();
1422 let ids = tok.encode("hello world");
1424 assert_eq!(ids, vec![8, 2, 2, 3, 9, 3, 6, 2, 7]);
1425 assert_eq!(tok.decode(&ids), "hello world");
1426 let ids2 = tok.encode("he<|eot|>he");
1428 assert_eq!(ids2, vec![8, 10, 8]);
1429 assert_eq!(tok.decode(&ids2), "hehe");
1430 }
1431
1432 #[test]
1433 fn non_ascii_is_never_silently_dropped() {
1434 let tok = Tokenizer::from_json(&mini_json()).unwrap();
1435 let ids = tok.encode("hello");
1438 assert!(!ids.is_empty());
1439 }
1440}
1441
1442#[cfg(test)]
1443mod generation_tag_tests {
1444 use super::strip_generation_tags;
1445
1446 #[test]
1451 fn a_generation_block_becomes_a_no_op_keeping_its_whitespace_control() {
1452 let tpl = "a{%- generation -%}b{%- endgeneration -%}c";
1453 let out = strip_generation_tags(tpl);
1454 assert!(!out.contains("{%- generation"));
1455 assert!(!out.contains("endgeneration"));
1456 assert_eq!(out.matches("{%-").count(), 2);
1458 assert_eq!(out.matches("-%}").count(), 2);
1459 assert!(out.starts_with('a') && out.ends_with('c'));
1460 }
1461
1462 #[test]
1465 fn each_side_keeps_its_own_dash() {
1466 let out = strip_generation_tags("{% generation %}x{%- endgeneration %}");
1467 assert!(out.starts_with("{% set"), "no dash added on the left");
1468 assert!(out.contains("{%- set"), "the right tag keeps its dash");
1469 assert!(!out.contains("-%}"), "no trailing dash invented");
1470 }
1471
1472 #[test]
1475 fn everything_else_is_left_alone() {
1476 let plain = "{%- if x -%}{{ y }}{%- endif -%}";
1477 assert_eq!(strip_generation_tags(plain), plain);
1478 let prose = "{{ 'the generation of tokens' }}";
1480 assert_eq!(strip_generation_tags(prose), prose);
1481 }
1482}