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_opts(
768 &self,
769 messages: &[(String, String)],
770 enable_thinking: Option<bool>,
771 ) -> Vec<u32> {
772 if let Some(tpl) = &self.chat_template {
773 match self.render_template(tpl, messages, enable_thinking) {
774 Ok(text) => return self.with_bos(self.encode(&text)),
775 Err(e) => {
776 tracing::error!("chat template render failed ({e}); ChatML fallback");
777 }
778 }
779 }
780 self.with_bos(self.chatml_fallback_opts(messages, enable_thinking))
781 }
782
783 pub fn with_bos(&self, mut ids: Vec<u32>) -> Vec<u32> {
785 if self.add_bos {
786 if let Some(b) = self.bos_token_id {
787 if ids.first() != Some(&b) {
788 ids.insert(0, b);
789 }
790 }
791 }
792 ids
793 }
794
795 pub fn render_chat(&self, messages: &[(String, String)]) -> Option<String> {
797 self.render_chat_opts(messages, None)
798 }
799
800 pub fn render_chat_opts(
802 &self,
803 messages: &[(String, String)],
804 enable_thinking: Option<bool>,
805 ) -> Option<String> {
806 let tpl = self.chat_template.as_ref()?;
807 match self.render_template(tpl, messages, enable_thinking) {
808 Ok(t) => Some(t),
809 Err(e) => {
810 tracing::error!("chat template render: {e:#}");
811 None
812 }
813 }
814 }
815
816 fn render_template(
817 &self,
818 tpl: &str,
819 messages: &[(String, String)],
820 enable_thinking: Option<bool>,
821 ) -> Result<String, minijinja::Error> {
822 let mut env = minijinja::Environment::new();
823 env.set_trim_blocks(true);
824 env.set_lstrip_blocks(true);
825 env.set_unknown_method_callback(minijinja_contrib::pycompat::unknown_method_callback);
827 env.add_template("chat", tpl)?;
828 let msgs: Vec<minijinja::Value> = messages
829 .iter()
830 .map(|(role, content)| {
831 minijinja::context! { role => role, content => content }
832 })
833 .collect();
834 let rendered = match enable_thinking {
837 Some(v) => env.get_template("chat")?.render(minijinja::context! {
838 messages => msgs,
839 add_generation_prompt => true,
840 enable_thinking => v,
841 })?,
842 None => env.get_template("chat")?.render(minijinja::context! {
843 messages => msgs,
844 add_generation_prompt => true,
845 })?,
846 };
847 if enable_thinking == Some(false) && !rendered.contains("</think>") {
851 if let Some(pos) = rendered.rfind("assistant") {
852 let mut insert_at = pos + "assistant".len();
853 if let Some(idx) = rendered[insert_at..].find('\n') {
854 insert_at += idx + 1;
855 }
856 let mut out = String::with_capacity(rendered.len() + 24);
857 out.push_str(&rendered[..insert_at]);
858 if !out.ends_with('\n') {
859 out.push('\n');
860 }
861 out.push_str("<think>\n\n</think>\n\n");
862 out.push_str(&rendered[insert_at..]);
863 return Ok(out);
864 }
865 }
866 Ok(rendered)
867 }
868
869 fn chatml_fallback(&self, messages: &[(String, String)]) -> Vec<u32> {
871 self.chatml_fallback_opts(messages, None)
872 }
873
874 fn chatml_fallback_opts(
876 &self,
877 messages: &[(String, String)],
878 enable_thinking: Option<bool>,
879 ) -> Vec<u32> {
880 let mut tokens = Vec::new();
881
882 for (role, content) in messages {
883 if let Some(start_id) = self.im_start_id {
885 tokens.push(start_id);
886 }
887 tokens.extend(self.encode(&format!("{}\n{}", role, content)));
888 if let Some(end_id) = self.im_end_id {
889 tokens.push(end_id);
890 }
891 tokens.extend(self.encode("\n"));
892 }
893
894 if let Some(start_id) = self.im_start_id {
896 tokens.push(start_id);
897 }
898 tokens.extend(self.encode("assistant\n"));
899 if enable_thinking == Some(false) {
900 tokens.extend(self.encode("<think>\n\n</think>\n\n"));
901 }
902
903 tokens
904 }
905
906 pub fn vocab_size(&self) -> usize {
908 self.id_to_token.len()
909 }
910
911 pub fn is_eos(&self, id: u32) -> bool {
913 self.eos_token_id == Some(id) || self.im_end_id == Some(id) || self.extra_eos.contains(&id)
914 }
915}
916
917#[derive(Debug, thiserror::Error)]
918pub enum TokenizerError {
919 #[error("IO error: {0}")]
920 Io(String),
921 #[error("Parse error: {0}")]
922 Parse(String),
923}
924
925#[cfg(test)]
926mod tests {
927 use super::*;
928
929 #[test]
930 fn byte_unicode_bijection() {
931 let (b2c, c2b) = bytes_to_unicode();
932 for b in 0..=255u8 {
933 assert_eq!(c2b[&b2c[b as usize]], b);
934 }
935 assert_eq!(b2c[b' ' as usize], 'Ġ');
937 assert_eq!(b2c[b'\n' as usize], 'Ċ');
938 }
939
940 #[test]
941 fn byte_level_roundtrip_utf8() {
942 let tok = Tokenizer::byte_level();
943 let text = "hello 🌍 hi\n";
944 let ids = tok.encode(text);
945 assert_eq!(ids.len(), text.len()); assert_eq!(tok.decode(&ids), text);
947 }
948
949 fn mini_json() -> String {
952 let vocab: Vec<(&str, u32)> = vec![
954 ("h", 0),
955 ("e", 1),
956 ("l", 2),
957 ("o", 3),
958 ("Ġ", 4),
959 ("w", 5),
960 ("r", 6),
961 ("d", 7),
962 ("he", 8),
963 ("Ġw", 9),
964 ];
965 let vocab_json: String = vocab
966 .iter()
967 .map(|(t, i)| format!("\"{t}\": {i}"))
968 .collect::<Vec<_>>()
969 .join(", ");
970 format!(
971 r#"{{
972 "model": {{
973 "type": "BPE",
974 "vocab": {{ {vocab_json} }},
975 "merges": [["h", "e"], ["Ġ", "w"]]
976 }},
977 "added_tokens": [
978 {{"id": 10, "content": "<|eot|>", "special": true}}
979 ]
980 }}"#
981 )
982 }
983
984 #[test]
987 fn real_tokenizer_parity_when_available() {
988 let Ok(path) = std::env::var("CMF_TOK_PARITY") else {
989 return;
990 };
991 let t = Tokenizer::from_file(&path).expect("load");
992 for (text, want) in [
993 (
994 "The capital of France is",
995 vec![671u32, 6102, 294, 8760, 344],
996 ),
997 ("2 + 2 =", vec![20, 940, 223, 20, 438]),
998 ] {
999 let got = t.encode(text);
1000 assert_eq!(got, want, "«{text}»");
1001 }
1002 }
1003
1004 #[test]
1010 fn every_split_in_a_sequence_is_applied() {
1011 let pt = serde_json::json!({
1012 "type": "Sequence",
1013 "pretokenizers": [
1014 {"type": "Split", "behavior": "Isolated",
1015 "pattern": {"Regex": r"\p{N}{1,3}"}},
1016 {"type": "Split", "behavior": "Isolated",
1017 "pattern": {"Regex": r" ?[\p{L}]+"}},
1018 {"type": "ByteLevel", "add_prefix_space": false, "use_regex": false}
1019 ]
1020 });
1021 let mut pats = Vec::new();
1022 collect_split_patterns(&pt, &mut pats);
1023 assert_eq!(
1024 pats.len(),
1025 2,
1026 "both Split stages must be collected: {pats:?}"
1027 );
1028 assert!(pats[0].contains("p{N}"), "digit rule first");
1029 assert!(pats[1].contains("p{L}"), "word rule second");
1030
1031 let re: Vec<fancy_regex::Regex> = pats
1035 .iter()
1036 .map(|p| fancy_regex::Regex::new(p).unwrap())
1037 .collect();
1038 let norm = "ab cd12";
1039 let mut pieces: Vec<(usize, usize)> = vec![(0, norm.len())];
1040 for r in &re {
1041 let mut next = Vec::new();
1042 for (ps, pe) in pieces {
1043 let seg = &norm[ps..pe];
1044 let mut last = 0;
1045 for m in r.find_iter(seg).flatten() {
1046 if m.start() > last {
1047 next.push((ps + last, ps + m.start()));
1048 }
1049 if m.end() > m.start() {
1050 next.push((ps + m.start(), ps + m.end()));
1051 }
1052 last = m.end();
1053 }
1054 if last < seg.len() {
1055 next.push((ps + last, pe));
1056 }
1057 }
1058 pieces = next;
1059 }
1060 let got: Vec<&str> = pieces.iter().map(|(a, b)| &norm[*a..*b]).collect();
1061 assert_eq!(
1062 got,
1063 vec!["ab", " cd", "12"],
1064 "staged split produced {got:?}"
1065 );
1066 }
1067
1068 #[test]
1069 fn full_pipeline_merges_and_added_tokens() {
1070 let tok = Tokenizer::from_json(&mini_json()).unwrap();
1071 let ids = tok.encode("hello world");
1073 assert_eq!(ids, vec![8, 2, 2, 3, 9, 3, 6, 2, 7]);
1074 assert_eq!(tok.decode(&ids), "hello world");
1075 let ids2 = tok.encode("he<|eot|>he");
1077 assert_eq!(ids2, vec![8, 10, 8]);
1078 assert_eq!(tok.decode(&ids2), "hehe");
1079 }
1080
1081 #[test]
1082 fn non_ascii_is_never_silently_dropped() {
1083 let tok = Tokenizer::from_json(&mini_json()).unwrap();
1084 let ids = tok.encode("hello");
1087 assert!(!ids.is_empty());
1088 }
1089}