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_re: Option<fancy_regex::Regex>,
38 sp_prepend: bool,
41 metaspace: bool,
44 nfc: bool,
47 byte_to_char: [char; 256],
49 char_to_byte: HashMap<char, u8>,
51 pub bos_token_id: Option<u32>,
53 pub eos_token_id: Option<u32>,
54 pub pad_token_id: Option<u32>,
55 pub im_start_id: Option<u32>,
57 pub im_end_id: Option<u32>,
58 pub chat_template: Option<String>,
61 pub extra_eos: HashSet<u32>,
63 pub add_bos: bool,
65}
66
67impl std::fmt::Debug for Tokenizer {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 f.debug_struct("Tokenizer")
70 .field("vocab", &self.vocab.len())
71 .field("merges", &self.ranks.len())
72 .field("added", &self.added.len())
73 .finish()
74 }
75}
76
77fn bytes_to_unicode() -> ([char; 256], HashMap<char, u8>) {
80 let mut b2c = ['\0'; 256];
81 let mut c2b = HashMap::with_capacity(256);
82 let mut n = 0u32;
83 for b in 0..=255u16 {
84 let printable = (0x21..=0x7E).contains(&b)
85 || (0xA1..=0xAC).contains(&b)
86 || (0xAE..=0xFF).contains(&b);
87 let c = if printable {
88 char::from_u32(b as u32).unwrap()
89 } else {
90 let c = char::from_u32(256 + n).unwrap();
91 n += 1;
92 c
93 };
94 b2c[b as usize] = c;
95 c2b.insert(c, b as u8);
96 }
97 (b2c, c2b)
98}
99
100#[derive(Deserialize)]
102struct HfTokenizerJson {
103 model: HfModel,
104 #[serde(default)]
105 added_tokens: Vec<HfAddedToken>,
106 #[serde(default)]
107 pre_tokenizer: Option<serde_json::Value>,
108 #[serde(default)]
109 normalizer: Option<serde_json::Value>,
110 #[serde(default)]
111 post_processor: Option<serde_json::Value>,
112}
113
114#[derive(Deserialize)]
115struct HfModel {
116 vocab: HashMap<String, u32>,
117 #[serde(default)]
118 merges: Vec<HfMerge>,
119 #[serde(default)]
120 byte_fallback: bool,
121}
122
123#[derive(Deserialize)]
126#[serde(untagged)]
127enum HfMerge {
128 Pair([String; 2]),
129 Text(String),
130}
131
132#[derive(Deserialize)]
133struct HfAddedToken {
134 id: u32,
135 content: String,
136 special: bool,
137}
138
139fn find_split_pattern(pt: &serde_json::Value) -> Option<String> {
142 if pt.get("type").and_then(|t| t.as_str()) == Some("Split") {
143 return pt
144 .get("pattern")
145 .and_then(|p| p.get("Regex"))
146 .and_then(|r| r.as_str())
147 .map(String::from);
148 }
149 if let Some(list) = pt.get("pretokenizers").and_then(|l| l.as_array()) {
150 return list.iter().find_map(find_split_pattern);
151 }
152 None
153}
154
155impl Tokenizer {
156 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, TokenizerError> {
158 let data = std::fs::read_to_string(path.as_ref())
159 .map_err(|e| TokenizerError::Io(e.to_string()))?;
160 Self::from_json(&data)
161 }
162
163 pub fn from_bytes(bytes: &[u8]) -> Result<Self, TokenizerError> {
165 let s = std::str::from_utf8(bytes)
166 .map_err(|e| TokenizerError::Parse(format!("vocab is not UTF-8: {e}")))?;
167 Self::from_json(s)
168 }
169
170 pub fn from_json(json: &str) -> Result<Self, TokenizerError> {
172 let hf: HfTokenizerJson =
173 serde_json::from_str(json).map_err(|e| TokenizerError::Parse(e.to_string()))?;
174
175 let mut vocab = hf.model.vocab;
176 let mut ranks = HashMap::new();
177 for (rank, m) in hf.model.merges.into_iter().enumerate() {
178 let (a, b) = match m {
179 HfMerge::Pair([a, b]) => (a, b),
180 HfMerge::Text(s) => {
181 let mut it = s.splitn(2, ' ');
182 match (it.next(), it.next()) {
183 (Some(a), Some(b)) => (a.to_string(), b.to_string()),
184 _ => continue,
185 }
186 }
187 };
188 ranks.insert((a, b), rank as u32);
189 }
190
191 let add_bos = hf
196 .post_processor
197 .as_ref()
198 .map(|p| {
199 let pp = p.to_string();
200 pp.contains("\"<s>\"") || pp.contains("\"<bos>\"")
201 })
202 .unwrap_or(false);
203 let nfc = hf
204 .normalizer
205 .as_ref()
206 .map(|n| n.to_string().contains("NFC"))
207 .unwrap_or(false);
208 let metaspace = hf.model.byte_fallback
209 || hf
210 .normalizer
211 .as_ref()
212 .map(|n| n.to_string().contains("\u{2581}") || n.to_string().contains("▁"))
213 .unwrap_or(false);
214 let sp_prepend = hf
215 .normalizer
216 .as_ref()
217 .map(|n| n.to_string().contains("Prepend"))
218 .unwrap_or(false);
219 let split_re = if metaspace {
220 None
221 } else {
222 let pattern = hf
223 .pre_tokenizer
224 .as_ref()
225 .and_then(find_split_pattern)
226 .unwrap_or_else(|| DEFAULT_SPLIT.to_string());
227 Some(fancy_regex::Regex::new(&pattern).map_err(|e| {
228 TokenizerError::Parse(format!("pre-tokenizer regex: {e}"))
229 })?)
230 };
231
232 let mut bos_token_id = None;
234 let mut eos_token_id = None;
235 let mut pad_token_id = None;
236 let mut im_start_id = None;
237 let mut im_end_id = None;
238 let mut special_ids = HashSet::new();
239 let mut added_ids = HashSet::new();
240 let mut added = Vec::new();
241
242 for at in &hf.added_tokens {
243 vocab.insert(at.content.clone(), at.id);
244 added.push((at.content.clone(), at.id));
245 added_ids.insert(at.id);
246 if at.special {
247 special_ids.insert(at.id);
248 }
249 match at.content.as_str() {
250 "<|endoftext|>" | "</s>" => eos_token_id = Some(at.id),
251 "<|im_start|>" => im_start_id = Some(at.id),
252 "<|im_end|>" => im_end_id = Some(at.id),
253 "<s>" | "<bos>" => bos_token_id = Some(at.id),
254 "<pad>" => pad_token_id = Some(at.id),
255 _ => {}
256 }
257 }
258 added.sort_by_key(|(c, _)| std::cmp::Reverse(c.len()));
259
260 let max_id = vocab.values().copied().max().unwrap_or(0) as usize;
262 let mut id_to_token = vec![String::new(); max_id + 1];
263 for (token, &id) in &vocab {
264 if (id as usize) < id_to_token.len() {
265 id_to_token[id as usize] = token.clone();
266 }
267 }
268
269 let (byte_to_char, char_to_byte) = bytes_to_unicode();
270
271 tracing::info!(
272 "Tokenizer loaded: {} vocab, {} merges, {} added, eos={:?}",
273 vocab.len(),
274 ranks.len(),
275 added.len(),
276 eos_token_id
277 );
278
279 Ok(Self {
280 vocab,
281 id_to_token,
282 ranks,
283 added,
284 added_ids,
285 special_ids,
286 split_re,
287 metaspace,
288 sp_prepend,
289 nfc,
290 byte_to_char,
291 char_to_byte,
292 bos_token_id,
293 eos_token_id,
294 pad_token_id,
295 im_start_id,
296 im_end_id,
297 chat_template: None,
298 extra_eos: HashSet::new(),
299 add_bos,
300 })
301 }
302
303 pub fn byte_level() -> Self {
305 let mut vocab = HashMap::new();
306 let mut id_to_token = Vec::with_capacity(256);
307 for i in 0..256u32 {
308 let tok = format!("<0x{:02X}>", i);
309 vocab.insert(tok.clone(), i);
310 id_to_token.push(tok);
311 }
312 let (byte_to_char, char_to_byte) = bytes_to_unicode();
313 Self {
314 vocab,
315 id_to_token,
316 ranks: HashMap::new(),
317 added: Vec::new(),
318 added_ids: HashSet::new(),
319 special_ids: HashSet::new(),
320 split_re: None,
321 metaspace: false,
322 sp_prepend: false,
323 nfc: false,
324 byte_to_char,
325 char_to_byte,
326 bos_token_id: None,
327 eos_token_id: None,
328 pad_token_id: None,
329 im_start_id: None,
330 im_end_id: None,
331 chat_template: None,
332 extra_eos: HashSet::new(),
333 add_bos: false,
334 }
335 }
336
337 pub fn encode(&self, text: &str) -> Vec<u32> {
339 let mut ids = Vec::new();
340 let mut rest = text;
342 'outer: while !rest.is_empty() {
343 let mut best: Option<(usize, usize, u32)> = None; for (content, id) in &self.added {
345 if let Some(pos) = rest.find(content.as_str()) {
346 let better = match best {
347 None => true,
348 Some((bp, bl, _)) => pos < bp || (pos == bp && content.len() > bl),
349 };
350 if better {
351 best = Some((pos, content.len(), *id));
352 }
353 if pos == 0 {
354 break; }
356 }
357 }
358 match best {
359 Some((pos, len, id)) => {
360 self.encode_segment(&rest[..pos], &mut ids);
361 ids.push(id);
362 rest = &rest[pos + len..];
363 }
364 None => {
365 self.encode_segment(rest, &mut ids);
366 break 'outer;
367 }
368 }
369 }
370 ids
371 }
372
373 fn encode_segment(&self, segment: &str, out: &mut Vec<u32>) {
375 if segment.is_empty() {
376 return;
377 }
378 let norm: String = if self.nfc {
379 segment.nfc().collect()
380 } else {
381 segment.to_string()
382 };
383 if self.metaspace {
384 let sp = if self.sp_prepend {
388 format!("\u{2581}{}", norm).replace(' ', "\u{2581}")
389 } else {
390 norm.replace(' ', "\u{2581}")
391 };
392 self.bpe_piece_sp(&sp, out);
393 return;
394 }
395 match &self.split_re {
396 Some(re) => {
397 let mut last = 0;
398 for m in re.find_iter(&norm) {
399 let m = match m {
400 Ok(m) => m,
401 Err(e) => {
402 tracing::error!("pre-tokenizer regex failed: {e}");
403 break;
404 }
405 };
406 if m.start() > last {
407 self.bpe_piece(&norm[last..m.start()], out);
409 }
410 self.bpe_piece(m.as_str(), out);
411 last = m.end();
412 }
413 if last < norm.len() {
414 self.bpe_piece(&norm[last..], out);
415 }
416 }
417 None => {
418 for b in norm.bytes() {
420 let tok = format!("<0x{:02X}>", b);
421 if let Some(&id) = self.vocab.get(&tok) {
422 out.push(id);
423 }
424 }
425 }
426 }
427 }
428
429 fn bpe_piece_sp(&self, piece: &str, out: &mut Vec<u32>) {
432 if piece.is_empty() {
433 return;
434 }
435 let mut sym: Vec<String> = piece.chars().map(|c| c.to_string()).collect();
436 loop {
437 let mut best: Option<(u32, usize)> = None;
438 for i in 0..sym.len().saturating_sub(1) {
439 if let Some(&r) = self.ranks.get(&(sym[i].clone(), sym[i + 1].clone())) {
440 if best.map(|(br, _)| r < br).unwrap_or(true) {
441 best = Some((r, i));
442 }
443 }
444 }
445 let Some((_, i)) = best else { break };
446 let merged = format!("{}{}", sym[i], sym[i + 1]);
447 let (left, right) = (sym[i].clone(), sym[i + 1].clone());
448 let mut j = 0;
449 while j + 1 < sym.len() {
450 if sym[j] == left && sym[j + 1] == right {
451 sym[j] = merged.clone();
452 sym.remove(j + 1);
453 }
454 j += 1;
455 }
456 }
457 for t in &sym {
458 if let Some(&id) = self.vocab.get(t) {
459 out.push(id);
460 } else {
461 let mut ok = true;
462 for byte in t.bytes() {
463 let tok = format!("<0x{:02X}>", byte);
464 match self.vocab.get(&tok) {
465 Some(&id) => out.push(id),
466 None => {
467 ok = false;
468 break;
469 }
470 }
471 }
472 if !ok {
473 tracing::error!("tokenizer: no id for SP symbol {t:?} — dropped");
474 }
475 }
476 }
477 }
478
479 fn bpe_piece(&self, piece: &str, out: &mut Vec<u32>) {
481 if piece.is_empty() {
482 return;
483 }
484 let mapped: Vec<String> = piece
485 .bytes()
486 .map(|b| self.byte_to_char[b as usize].to_string())
487 .collect();
488 let mut sym = mapped;
489
490 loop {
492 let mut best: Option<(u32, usize)> = None;
493 for i in 0..sym.len().saturating_sub(1) {
494 if let Some(&r) = self
495 .ranks
496 .get(&(sym[i].clone(), sym[i + 1].clone()))
497 {
498 if best.map(|(br, _)| r < br).unwrap_or(true) {
499 best = Some((r, i));
500 }
501 }
502 }
503 let Some((_, i)) = best else { break };
504 let merged = format!("{}{}", sym[i], sym[i + 1]);
505 let (left, right) = (sym[i].clone(), sym[i + 1].clone());
507 let mut j = 0;
508 while j + 1 < sym.len() {
509 if sym[j] == left && sym[j + 1] == right {
510 sym[j] = merged.clone();
511 sym.remove(j + 1);
512 }
513 j += 1;
514 }
515 }
516
517 for s in &sym {
518 if let Some(&id) = self.vocab.get(s) {
519 out.push(id);
520 } else {
521 let mut ok = true;
523 for ch in s.chars() {
524 let Some(&b) = self.char_to_byte.get(&ch) else {
525 ok = false;
526 break;
527 };
528 let tok = format!("<0x{:02X}>", b);
529 if let Some(&id) = self.vocab.get(&tok) {
530 out.push(id);
531 } else {
532 ok = false;
533 break;
534 }
535 }
536 if !ok {
537 tracing::error!("tokenizer: no id for symbol {s:?} — dropped");
538 }
539 }
540 }
541 }
542
543 pub fn decode(&self, ids: &[u32]) -> String {
546 let mut bytes: Vec<u8> = Vec::new();
547 for &id in ids {
548 if self.special_ids.contains(&id) {
549 continue;
550 }
551 let idx = id as usize;
552 if idx >= self.id_to_token.len() {
553 continue;
554 }
555 let tok = &self.id_to_token[idx];
556 if self.added_ids.contains(&id) {
557 bytes.extend_from_slice(tok.as_bytes());
558 continue;
559 }
560 if tok.starts_with("<0x") && tok.ends_with('>') && tok.len() == 6 {
562 if let Ok(b) = u8::from_str_radix(&tok[3..5], 16) {
563 bytes.push(b);
564 continue;
565 }
566 }
567 if self.metaspace {
568 for ch in tok.chars() {
570 if ch == '\u{2581}' {
571 bytes.push(b' ');
572 } else {
573 let mut buf = [0u8; 4];
574 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
575 }
576 }
577 continue;
578 }
579 for ch in tok.chars() {
580 match self.char_to_byte.get(&ch) {
581 Some(&b) => bytes.push(b),
582 None => {
585 let mut buf = [0u8; 4];
586 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
587 }
588 }
589 }
590 }
591 let text = String::from_utf8_lossy(&bytes).into_owned();
592 if self.metaspace && self.sp_prepend {
593 if let Some(stripped) = text.strip_prefix(' ') {
595 return stripped.to_string();
596 }
597 }
598 text
599 }
600
601 pub fn decode_token(&self, id: u32) -> String {
604 if self.special_ids.contains(&id) {
605 return String::new();
606 }
607 let idx = id as usize;
608 if idx >= self.id_to_token.len() {
609 return String::new();
610 }
611 let tok = &self.id_to_token[idx];
612 if self.added_ids.contains(&id) {
613 return tok.clone();
614 }
615 if tok.starts_with("<0x") && tok.ends_with('>') && tok.len() == 6 {
616 if let Ok(b) = u8::from_str_radix(&tok[3..5], 16) {
617 return String::from_utf8_lossy(&[b]).into_owned();
618 }
619 }
620 if self.metaspace {
621 return tok.replace('\u{2581}', " ");
622 }
623 let mut bytes = Vec::new();
624 for ch in tok.chars() {
625 match self.char_to_byte.get(&ch) {
626 Some(&b) => bytes.push(b),
627 None => {
628 let mut buf = [0u8; 4];
629 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
630 }
631 }
632 }
633 String::from_utf8_lossy(&bytes).into_owned()
634 }
635
636 pub fn apply_chat_template(&self, messages: &[(String, String)]) -> Vec<u32> {
640 self.apply_chat_template_opts(messages, None)
641 }
642
643 pub fn apply_chat_template_opts(
648 &self,
649 messages: &[(String, String)],
650 enable_thinking: Option<bool>,
651 ) -> Vec<u32> {
652 if let Some(tpl) = &self.chat_template {
653 match self.render_template(tpl, messages, enable_thinking) {
654 Ok(text) => return self.with_bos(self.encode(&text)),
655 Err(e) => {
656 tracing::error!("chat template render failed ({e}); ChatML fallback");
657 }
658 }
659 }
660 self.with_bos(self.chatml_fallback(messages))
661 }
662
663 pub fn with_bos(&self, mut ids: Vec<u32>) -> Vec<u32> {
665 if self.add_bos {
666 if let Some(b) = self.bos_token_id {
667 if ids.first() != Some(&b) {
668 ids.insert(0, b);
669 }
670 }
671 }
672 ids
673 }
674
675 pub fn render_chat(&self, messages: &[(String, String)]) -> Option<String> {
677 let tpl = self.chat_template.as_ref()?;
678 match self.render_template(tpl, messages, None) {
679 Ok(t) => Some(t),
680 Err(e) => {
681 tracing::error!("chat template render: {e:#}");
682 None
683 }
684 }
685 }
686
687 fn render_template(
688 &self,
689 tpl: &str,
690 messages: &[(String, String)],
691 enable_thinking: Option<bool>,
692 ) -> Result<String, minijinja::Error> {
693 let mut env = minijinja::Environment::new();
694 env.set_trim_blocks(true);
695 env.set_lstrip_blocks(true);
696 env.set_unknown_method_callback(minijinja_contrib::pycompat::unknown_method_callback);
698 env.add_template("chat", tpl)?;
699 let msgs: Vec<minijinja::Value> = messages
700 .iter()
701 .map(|(role, content)| {
702 minijinja::context! { role => role, content => content }
703 })
704 .collect();
705 let rendered = match enable_thinking {
708 Some(v) => env.get_template("chat")?.render(minijinja::context! {
709 messages => msgs,
710 add_generation_prompt => true,
711 enable_thinking => v,
712 })?,
713 None => env.get_template("chat")?.render(minijinja::context! {
714 messages => msgs,
715 add_generation_prompt => true,
716 })?,
717 };
718 if enable_thinking == Some(false) && !rendered.contains("</think>") {
722 if let Some(pos) = rendered.rfind("assistant\n") {
723 let insert_at = pos + "assistant\n".len();
724 let mut out = String::with_capacity(rendered.len() + 24);
725 out.push_str(&rendered[..insert_at]);
726 out.push_str("<think>\n\n</think>\n\n");
727 out.push_str(&rendered[insert_at..]);
728 return Ok(out);
729 }
730 }
731 Ok(rendered)
732 }
733
734 fn chatml_fallback(&self, messages: &[(String, String)]) -> Vec<u32> {
736 let mut tokens = Vec::new();
737
738 for (role, content) in messages {
739 if let Some(start_id) = self.im_start_id {
741 tokens.push(start_id);
742 }
743 tokens.extend(self.encode(&format!("{}\n{}", role, content)));
744 if let Some(end_id) = self.im_end_id {
745 tokens.push(end_id);
746 }
747 tokens.extend(self.encode("\n"));
748 }
749
750 if let Some(start_id) = self.im_start_id {
752 tokens.push(start_id);
753 }
754 tokens.extend(self.encode("assistant\n"));
755
756 tokens
757 }
758
759 pub fn vocab_size(&self) -> usize {
761 self.id_to_token.len()
762 }
763
764 pub fn is_eos(&self, id: u32) -> bool {
766 self.eos_token_id == Some(id)
767 || self.im_end_id == Some(id)
768 || self.extra_eos.contains(&id)
769 }
770}
771
772#[derive(Debug, thiserror::Error)]
773pub enum TokenizerError {
774 #[error("IO error: {0}")]
775 Io(String),
776 #[error("Parse error: {0}")]
777 Parse(String),
778}
779
780#[cfg(test)]
781mod tests {
782 use super::*;
783
784 #[test]
785 fn byte_unicode_bijection() {
786 let (b2c, c2b) = bytes_to_unicode();
787 for b in 0..=255u8 {
788 assert_eq!(c2b[&b2c[b as usize]], b);
789 }
790 assert_eq!(b2c[b' ' as usize], 'Ġ');
792 assert_eq!(b2c[b'\n' as usize], 'Ċ');
793 }
794
795 #[test]
796 fn byte_level_roundtrip_utf8() {
797 let tok = Tokenizer::byte_level();
798 let text = "hello 🌍 hi\n";
799 let ids = tok.encode(text);
800 assert_eq!(ids.len(), text.len()); assert_eq!(tok.decode(&ids), text);
802 }
803
804 fn mini_json() -> String {
807 let vocab: Vec<(&str, u32)> = vec![
809 ("h", 0),
810 ("e", 1),
811 ("l", 2),
812 ("o", 3),
813 ("Ġ", 4),
814 ("w", 5),
815 ("r", 6),
816 ("d", 7),
817 ("he", 8),
818 ("Ġw", 9),
819 ];
820 let vocab_json: String = vocab
821 .iter()
822 .map(|(t, i)| format!("\"{t}\": {i}"))
823 .collect::<Vec<_>>()
824 .join(", ");
825 format!(
826 r#"{{
827 "model": {{
828 "type": "BPE",
829 "vocab": {{ {vocab_json} }},
830 "merges": [["h", "e"], ["Ġ", "w"]]
831 }},
832 "added_tokens": [
833 {{"id": 10, "content": "<|eot|>", "special": true}}
834 ]
835 }}"#
836 )
837 }
838
839 #[test]
840 fn full_pipeline_merges_and_added_tokens() {
841 let tok = Tokenizer::from_json(&mini_json()).unwrap();
842 let ids = tok.encode("hello world");
844 assert_eq!(ids, vec![8, 2, 2, 3, 9, 3, 6, 2, 7]);
845 assert_eq!(tok.decode(&ids), "hello world");
846 let ids2 = tok.encode("he<|eot|>he");
848 assert_eq!(ids2, vec![8, 10, 8]);
849 assert_eq!(tok.decode(&ids2), "hehe");
850 }
851
852 #[test]
853 fn non_ascii_is_never_silently_dropped() {
854 let tok = Tokenizer::from_json(&mini_json()).unwrap();
855 let ids = tok.encode("hello");
858 assert!(!ids.is_empty());
859 }
860}