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 sp_prepend_first: bool,
47 metaspace: bool,
50 nfc: bool,
53 byte_to_char: [char; 256],
55 char_to_byte: HashMap<char, u8>,
57 pub bos_token_id: Option<u32>,
59 pub eos_token_id: Option<u32>,
60 pub pad_token_id: Option<u32>,
61 pub im_start_id: Option<u32>,
63 pub im_end_id: Option<u32>,
64 pub chat_template: Option<String>,
67 pub extra_eos: HashSet<u32>,
69 pub add_bos: bool,
71}
72
73impl std::fmt::Debug for Tokenizer {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.debug_struct("Tokenizer")
76 .field("vocab", &self.vocab.len())
77 .field("merges", &self.ranks.len())
78 .field("added", &self.added.len())
79 .finish()
80 }
81}
82
83fn bytes_to_unicode() -> ([char; 256], HashMap<char, u8>) {
86 let mut b2c = ['\0'; 256];
87 let mut c2b = HashMap::with_capacity(256);
88 let mut n = 0u32;
89 for b in 0..=255u16 {
90 let printable =
91 (0x21..=0x7E).contains(&b) || (0xA1..=0xAC).contains(&b) || (0xAE..=0xFF).contains(&b);
92 let c = if printable {
93 char::from_u32(b as u32).unwrap()
94 } else {
95 let c = char::from_u32(256 + n).unwrap();
96 n += 1;
97 c
98 };
99 b2c[b as usize] = c;
100 c2b.insert(c, b as u8);
101 }
102 (b2c, c2b)
103}
104
105#[derive(Deserialize)]
107struct HfTokenizerJson {
108 model: HfModel,
109 #[serde(default)]
110 added_tokens: Vec<HfAddedToken>,
111 #[serde(default)]
112 pre_tokenizer: Option<serde_json::Value>,
113 #[serde(default)]
114 normalizer: Option<serde_json::Value>,
115 #[serde(default)]
116 post_processor: Option<serde_json::Value>,
117}
118
119#[derive(Deserialize)]
120struct HfModel {
121 vocab: HashMap<String, u32>,
122 #[serde(default)]
123 merges: Vec<HfMerge>,
124 #[serde(default)]
125 byte_fallback: bool,
126}
127
128#[derive(Deserialize)]
131#[serde(untagged)]
132enum HfMerge {
133 Pair([String; 2]),
134 Text(String),
135}
136
137#[derive(Deserialize)]
138struct HfAddedToken {
139 id: u32,
140 content: String,
141 special: bool,
142}
143
144fn find_split_pattern(pt: &serde_json::Value) -> Option<String> {
147 if pt.get("type").and_then(|t| t.as_str()) == Some("Split") {
148 return pt
149 .get("pattern")
150 .and_then(|p| p.get("Regex"))
151 .and_then(|r| r.as_str())
152 .map(String::from);
153 }
154 if let Some(list) = pt.get("pretokenizers").and_then(|l| l.as_array()) {
155 return list.iter().find_map(find_split_pattern);
156 }
157 None
158}
159
160fn find_prepend_scheme(pt: &serde_json::Value) -> Option<String> {
163 if pt.get("type").and_then(|t| t.as_str()) == Some("Metaspace") {
164 return pt
165 .get("prepend_scheme")
166 .and_then(|p| p.as_str())
167 .map(String::from);
168 }
169 if let Some(list) = pt.get("pretokenizers").and_then(|l| l.as_array()) {
170 return list.iter().find_map(find_prepend_scheme);
171 }
172 None
173}
174
175impl Tokenizer {
176 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, TokenizerError> {
178 let data = std::fs::read_to_string(path.as_ref())
179 .map_err(|e| TokenizerError::Io(e.to_string()))?;
180 Self::from_json(&data)
181 }
182
183 pub fn from_bytes(bytes: &[u8]) -> Result<Self, TokenizerError> {
185 let s = std::str::from_utf8(bytes)
186 .map_err(|e| TokenizerError::Parse(format!("vocab is not UTF-8: {e}")))?;
187 Self::from_json(s)
188 }
189
190 pub fn from_json(json: &str) -> Result<Self, TokenizerError> {
192 let hf: HfTokenizerJson =
193 serde_json::from_str(json).map_err(|e| TokenizerError::Parse(e.to_string()))?;
194
195 let mut vocab = hf.model.vocab;
196 let mut ranks = HashMap::new();
197 for (rank, m) in hf.model.merges.into_iter().enumerate() {
198 let (a, b) = match m {
199 HfMerge::Pair([a, b]) => (a, b),
200 HfMerge::Text(s) => {
201 let mut it = s.splitn(2, ' ');
202 match (it.next(), it.next()) {
203 (Some(a), Some(b)) => (a.to_string(), b.to_string()),
204 _ => continue,
205 }
206 }
207 };
208 ranks.insert((a, b), rank as u32);
209 }
210
211 let mut saw_gemma_bos = false;
216 let add_bos_detected = hf
217 .post_processor
218 .as_ref()
219 .map(|p| {
220 let pp = p.to_string();
221 pp.contains("\"<s>\"") || pp.contains("\"<bos>\"")
222 })
223 .unwrap_or(false);
224 let nfc = hf
225 .normalizer
226 .as_ref()
227 .map(|n| n.to_string().contains("NFC"))
228 .unwrap_or(false);
229 let metaspace = hf.model.byte_fallback
230 || hf
231 .normalizer
232 .as_ref()
233 .map(|n| n.to_string().contains("\u{2581}") || n.to_string().contains("▁"))
234 .unwrap_or(false);
235 let sp_prepend = hf
236 .normalizer
237 .as_ref()
238 .map(|n| n.to_string().contains("Prepend"))
239 .unwrap_or(false);
240 let (sp_prepend, sp_prepend_first) = if sp_prepend {
244 (true, false)
245 } else {
246 match hf.pre_tokenizer.as_ref().and_then(find_prepend_scheme) {
247 Some(s) if s == "always" => (true, false),
248 Some(s) if s == "first" => (false, true),
249 _ => (false, false),
250 }
251 };
252 let split_re = if metaspace {
253 None
254 } else {
255 let pattern = hf
256 .pre_tokenizer
257 .as_ref()
258 .and_then(find_split_pattern)
259 .unwrap_or_else(|| DEFAULT_SPLIT.to_string());
260 Some(
261 fancy_regex::Regex::new(&pattern)
262 .map_err(|e| TokenizerError::Parse(format!("pre-tokenizer regex: {e}")))?,
263 )
264 };
265
266 let mut bos_token_id = None;
268 let mut eos_token_id = None;
269 let mut pad_token_id = None;
270 let mut im_start_id = None;
271 let mut im_end_id = None;
272 let mut special_ids = HashSet::new();
273 let mut added_ids = HashSet::new();
274 let mut added = Vec::new();
275
276 for at in &hf.added_tokens {
277 vocab.insert(at.content.clone(), at.id);
278 added.push((at.content.clone(), at.id));
279 added_ids.insert(at.id);
280 if at.special {
281 special_ids.insert(at.id);
282 }
283 match at.content.as_str() {
284 "<|endoftext|>" | "</s>" | "[EOS]" => eos_token_id = Some(at.id),
285 "<|im_start|>" => im_start_id = Some(at.id),
286 "<|im_end|>" => im_end_id = Some(at.id),
287 "<s>" | "[BOS]" => bos_token_id = Some(at.id),
288 "<bos>" => {
292 bos_token_id = Some(at.id);
293 saw_gemma_bos = true;
294 }
295 "<pad>" => pad_token_id = Some(at.id),
296 _ => {}
297 }
298 }
299 added.sort_by_key(|(c, _)| std::cmp::Reverse(c.len()));
300
301 let gemma_family = saw_gemma_bos
308 || vocab.contains_key("<start_of_turn>")
309 || added.iter().any(|(c, _)| c == "<start_of_turn>");
310
311 if let Some(pp) = hf.post_processor.as_ref() {
315 let pp = pp.to_string();
316 for name in ["<bos>", "<s>"] {
317 if pp.contains(&format!("\"{name}\"")) {
318 if let Some(&id) = vocab.get(name) {
319 bos_token_id = Some(id);
320 }
321 break;
322 }
323 }
324 }
325
326 let max_id = vocab.values().copied().max().unwrap_or(0) as usize;
328 let mut id_to_token = vec![String::new(); max_id + 1];
329 for (token, &id) in &vocab {
330 if (id as usize) < id_to_token.len() {
331 id_to_token[id as usize] = token.clone();
332 }
333 }
334
335 let (byte_to_char, char_to_byte) = bytes_to_unicode();
336
337 tracing::info!(
338 "Tokenizer loaded: {} vocab, {} merges, {} added, eos={:?}",
339 vocab.len(),
340 ranks.len(),
341 added.len(),
342 eos_token_id
343 );
344
345 Ok(Self {
346 vocab,
347 id_to_token,
348 ranks,
349 added,
350 added_ids,
351 special_ids,
352 split_re,
353 metaspace,
354 sp_prepend,
355 sp_prepend_first,
356 nfc,
357 byte_to_char,
358 char_to_byte,
359 bos_token_id,
360 eos_token_id,
361 pad_token_id,
362 im_start_id,
363 im_end_id,
364 chat_template: None,
365 extra_eos: HashSet::new(),
366 add_bos: add_bos_detected || gemma_family,
367 })
368 }
369
370 pub fn byte_level() -> Self {
372 let mut vocab = HashMap::new();
373 let mut id_to_token = Vec::with_capacity(256);
374 for i in 0..256u32 {
375 let tok = format!("<0x{:02X}>", i);
376 vocab.insert(tok.clone(), i);
377 id_to_token.push(tok);
378 }
379 let (byte_to_char, char_to_byte) = bytes_to_unicode();
380 Self {
381 vocab,
382 id_to_token,
383 ranks: HashMap::new(),
384 added: Vec::new(),
385 added_ids: HashSet::new(),
386 special_ids: HashSet::new(),
387 split_re: None,
388 metaspace: false,
389 sp_prepend: false,
390 sp_prepend_first: false,
391 nfc: false,
392 byte_to_char,
393 char_to_byte,
394 bos_token_id: None,
395 eos_token_id: None,
396 pad_token_id: None,
397 im_start_id: None,
398 im_end_id: None,
399 chat_template: None,
400 extra_eos: HashSet::new(),
401 add_bos: false,
402 }
403 }
404
405 pub fn encode(&self, text: &str) -> Vec<u32> {
407 let mut ids = Vec::new();
408 let mut rest = text;
410 let mut head = true;
415 'outer: while !rest.is_empty() {
416 let mut best: Option<(usize, usize, u32)> = None; for (content, id) in &self.added {
418 if let Some(pos) = rest.find(content.as_str()) {
419 let better = match best {
420 None => true,
421 Some((bp, bl, _)) => pos < bp || (pos == bp && content.len() > bl),
422 };
423 if better {
424 best = Some((pos, content.len(), *id));
425 }
426 if pos == 0 {
427 break; }
429 }
430 }
431 match best {
432 Some((pos, len, id)) => {
433 self.encode_segment_at(&rest[..pos], head, &mut ids);
434 ids.push(id);
435 rest = &rest[pos + len..];
436 head = false;
437 }
438 None => {
439 self.encode_segment_at(rest, head, &mut ids);
440 break 'outer;
441 }
442 }
443 }
444 ids
445 }
446
447 fn encode_segment_at(&self, segment: &str, head: bool, out: &mut Vec<u32>) {
451 if segment.is_empty() {
452 return;
453 }
454 let norm: String = if self.nfc {
455 segment.nfc().collect()
456 } else {
457 segment.to_string()
458 };
459 if self.metaspace {
460 let sp = if self.sp_prepend {
464 format!("\u{2581}{}", norm).replace(' ', "\u{2581}")
467 } else {
468 let replaced = norm.replace(' ', "\u{2581}");
472 if self.sp_prepend_first && head && !replaced.starts_with('\u{2581}') {
473 format!("\u{2581}{replaced}")
474 } else {
475 replaced
476 }
477 };
478 self.bpe_piece_sp(&sp, out);
479 return;
480 }
481 match &self.split_re {
482 Some(re) => {
483 let mut last = 0;
484 for m in re.find_iter(&norm) {
485 let m = match m {
486 Ok(m) => m,
487 Err(e) => {
488 tracing::error!("pre-tokenizer regex failed: {e}");
489 break;
490 }
491 };
492 if m.start() > last {
493 self.bpe_piece(&norm[last..m.start()], out);
495 }
496 self.bpe_piece(m.as_str(), out);
497 last = m.end();
498 }
499 if last < norm.len() {
500 self.bpe_piece(&norm[last..], out);
501 }
502 }
503 None => {
504 for b in norm.bytes() {
506 let tok = format!("<0x{:02X}>", b);
507 if let Some(&id) = self.vocab.get(&tok) {
508 out.push(id);
509 }
510 }
511 }
512 }
513 }
514
515 fn bpe_piece_sp(&self, piece: &str, out: &mut Vec<u32>) {
518 if piece.is_empty() {
519 return;
520 }
521 let mut sym: Vec<String> = piece.chars().map(|c| c.to_string()).collect();
522 loop {
523 let mut best: Option<(u32, usize)> = None;
524 for i in 0..sym.len().saturating_sub(1) {
525 if let Some(&r) = self.ranks.get(&(sym[i].clone(), sym[i + 1].clone())) {
526 if best.map(|(br, _)| r < br).unwrap_or(true) {
527 best = Some((r, i));
528 }
529 }
530 }
531 let Some((_, i)) = best else { break };
532 let merged = format!("{}{}", sym[i], sym[i + 1]);
533 let (left, right) = (sym[i].clone(), sym[i + 1].clone());
534 let mut j = 0;
535 while j + 1 < sym.len() {
536 if sym[j] == left && sym[j + 1] == right {
537 sym[j] = merged.clone();
538 sym.remove(j + 1);
539 }
540 j += 1;
541 }
542 }
543 for t in &sym {
544 if let Some(&id) = self.vocab.get(t) {
545 out.push(id);
546 } else {
547 let mut ok = true;
548 for byte in t.bytes() {
549 let tok = format!("<0x{:02X}>", byte);
550 match self.vocab.get(&tok) {
551 Some(&id) => out.push(id),
552 None => {
553 ok = false;
554 break;
555 }
556 }
557 }
558 if !ok {
559 tracing::error!("tokenizer: no id for SP symbol {t:?} — dropped");
560 }
561 }
562 }
563 }
564
565 fn bpe_piece(&self, piece: &str, out: &mut Vec<u32>) {
567 if piece.is_empty() {
568 return;
569 }
570 let mapped: Vec<String> = piece
571 .bytes()
572 .map(|b| self.byte_to_char[b as usize].to_string())
573 .collect();
574 let mut sym = mapped;
575
576 loop {
578 let mut best: Option<(u32, usize)> = None;
579 for i in 0..sym.len().saturating_sub(1) {
580 if let Some(&r) = self.ranks.get(&(sym[i].clone(), sym[i + 1].clone())) {
581 if best.map(|(br, _)| r < br).unwrap_or(true) {
582 best = Some((r, i));
583 }
584 }
585 }
586 let Some((_, i)) = best else { break };
587 let merged = format!("{}{}", sym[i], sym[i + 1]);
588 let (left, right) = (sym[i].clone(), sym[i + 1].clone());
590 let mut j = 0;
591 while j + 1 < sym.len() {
592 if sym[j] == left && sym[j + 1] == right {
593 sym[j] = merged.clone();
594 sym.remove(j + 1);
595 }
596 j += 1;
597 }
598 }
599
600 for s in &sym {
601 if let Some(&id) = self.vocab.get(s) {
602 out.push(id);
603 } else {
604 let mut ok = true;
606 for ch in s.chars() {
607 let Some(&b) = self.char_to_byte.get(&ch) else {
608 ok = false;
609 break;
610 };
611 let tok = format!("<0x{:02X}>", b);
612 if let Some(&id) = self.vocab.get(&tok) {
613 out.push(id);
614 } else {
615 ok = false;
616 break;
617 }
618 }
619 if !ok {
620 tracing::error!("tokenizer: no id for symbol {s:?} — dropped");
621 }
622 }
623 }
624 }
625
626 pub fn decode(&self, ids: &[u32]) -> String {
629 let mut bytes: Vec<u8> = Vec::new();
630 for &id in ids {
631 if self.special_ids.contains(&id) {
632 continue;
633 }
634 let idx = id as usize;
635 if idx >= self.id_to_token.len() {
636 continue;
637 }
638 let tok = &self.id_to_token[idx];
639 if self.added_ids.contains(&id) {
640 if self.metaspace && tok.contains('\u{2581}') {
643 bytes.extend_from_slice(tok.replace('\u{2581}', " ").as_bytes());
644 } else {
645 bytes.extend_from_slice(tok.as_bytes());
646 }
647 continue;
648 }
649 if tok.starts_with("<0x") && tok.ends_with('>') && tok.len() == 6 {
651 if let Ok(b) = u8::from_str_radix(&tok[3..5], 16) {
652 bytes.push(b);
653 continue;
654 }
655 }
656 if self.metaspace {
657 for ch in tok.chars() {
659 if ch == '\u{2581}' {
660 bytes.push(b' ');
661 } else {
662 let mut buf = [0u8; 4];
663 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
664 }
665 }
666 continue;
667 }
668 for ch in tok.chars() {
669 match self.char_to_byte.get(&ch) {
670 Some(&b) => bytes.push(b),
671 None => {
674 let mut buf = [0u8; 4];
675 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
676 }
677 }
678 }
679 }
680 let text = String::from_utf8_lossy(&bytes).into_owned();
681 if self.metaspace && (self.sp_prepend || self.sp_prepend_first) {
682 if let Some(stripped) = text.strip_prefix(' ') {
684 return stripped.to_string();
685 }
686 }
687 text
688 }
689
690 pub fn decode_token(&self, id: u32) -> String {
693 if self.special_ids.contains(&id) {
694 return String::new();
695 }
696 let idx = id as usize;
697 if idx >= self.id_to_token.len() {
698 return String::new();
699 }
700 let tok = &self.id_to_token[idx];
701 if self.added_ids.contains(&id) {
702 if self.metaspace && tok.contains('\u{2581}') {
703 return tok.replace('\u{2581}', " ");
704 }
705 return tok.clone();
706 }
707 if tok.starts_with("<0x") && tok.ends_with('>') && tok.len() == 6 {
708 if let Ok(b) = u8::from_str_radix(&tok[3..5], 16) {
709 return String::from_utf8_lossy(&[b]).into_owned();
710 }
711 }
712 if self.metaspace {
713 return tok.replace('\u{2581}', " ");
714 }
715 let mut bytes = Vec::new();
716 for ch in tok.chars() {
717 match self.char_to_byte.get(&ch) {
718 Some(&b) => bytes.push(b),
719 None => {
720 let mut buf = [0u8; 4];
721 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
722 }
723 }
724 }
725 String::from_utf8_lossy(&bytes).into_owned()
726 }
727
728 pub fn apply_chat_template(&self, messages: &[(String, String)]) -> Vec<u32> {
732 self.apply_chat_template_opts(messages, None)
733 }
734
735 pub fn apply_chat_template_opts(
740 &self,
741 messages: &[(String, String)],
742 enable_thinking: Option<bool>,
743 ) -> Vec<u32> {
744 if let Some(tpl) = &self.chat_template {
745 match self.render_template(tpl, messages, enable_thinking) {
746 Ok(text) => return self.with_bos(self.encode(&text)),
747 Err(e) => {
748 tracing::error!("chat template render failed ({e}); ChatML fallback");
749 }
750 }
751 }
752 self.with_bos(self.chatml_fallback_opts(messages, enable_thinking))
753 }
754
755 pub fn with_bos(&self, mut ids: Vec<u32>) -> Vec<u32> {
757 if self.add_bos {
758 if let Some(b) = self.bos_token_id {
759 if ids.first() != Some(&b) {
760 ids.insert(0, b);
761 }
762 }
763 }
764 ids
765 }
766
767 pub fn render_chat(&self, messages: &[(String, String)]) -> Option<String> {
769 self.render_chat_opts(messages, None)
770 }
771
772 pub fn render_chat_opts(
774 &self,
775 messages: &[(String, String)],
776 enable_thinking: Option<bool>,
777 ) -> Option<String> {
778 let tpl = self.chat_template.as_ref()?;
779 match self.render_template(tpl, messages, enable_thinking) {
780 Ok(t) => Some(t),
781 Err(e) => {
782 tracing::error!("chat template render: {e:#}");
783 None
784 }
785 }
786 }
787
788 fn render_template(
789 &self,
790 tpl: &str,
791 messages: &[(String, String)],
792 enable_thinking: Option<bool>,
793 ) -> Result<String, minijinja::Error> {
794 let mut env = minijinja::Environment::new();
795 env.set_trim_blocks(true);
796 env.set_lstrip_blocks(true);
797 env.set_unknown_method_callback(minijinja_contrib::pycompat::unknown_method_callback);
799 env.add_template("chat", tpl)?;
800 let msgs: Vec<minijinja::Value> = messages
801 .iter()
802 .map(|(role, content)| {
803 minijinja::context! { role => role, content => content }
804 })
805 .collect();
806 let rendered = match enable_thinking {
809 Some(v) => env.get_template("chat")?.render(minijinja::context! {
810 messages => msgs,
811 add_generation_prompt => true,
812 enable_thinking => v,
813 })?,
814 None => env.get_template("chat")?.render(minijinja::context! {
815 messages => msgs,
816 add_generation_prompt => true,
817 })?,
818 };
819 if enable_thinking == Some(false) && !rendered.contains("</think>") {
823 if let Some(pos) = rendered.rfind("assistant") {
824 let mut insert_at = pos + "assistant".len();
825 if let Some(idx) = rendered[insert_at..].find('\n') {
826 insert_at += idx + 1;
827 }
828 let mut out = String::with_capacity(rendered.len() + 24);
829 out.push_str(&rendered[..insert_at]);
830 if !out.ends_with('\n') {
831 out.push('\n');
832 }
833 out.push_str("<think>\n\n</think>\n\n");
834 out.push_str(&rendered[insert_at..]);
835 return Ok(out);
836 }
837 }
838 Ok(rendered)
839 }
840
841 fn chatml_fallback(&self, messages: &[(String, String)]) -> Vec<u32> {
843 self.chatml_fallback_opts(messages, None)
844 }
845
846 fn chatml_fallback_opts(
848 &self,
849 messages: &[(String, String)],
850 enable_thinking: Option<bool>,
851 ) -> Vec<u32> {
852 let mut tokens = Vec::new();
853
854 for (role, content) in messages {
855 if let Some(start_id) = self.im_start_id {
857 tokens.push(start_id);
858 }
859 tokens.extend(self.encode(&format!("{}\n{}", role, content)));
860 if let Some(end_id) = self.im_end_id {
861 tokens.push(end_id);
862 }
863 tokens.extend(self.encode("\n"));
864 }
865
866 if let Some(start_id) = self.im_start_id {
868 tokens.push(start_id);
869 }
870 tokens.extend(self.encode("assistant\n"));
871 if enable_thinking == Some(false) {
872 tokens.extend(self.encode("<think>\n\n</think>\n\n"));
873 }
874
875 tokens
876 }
877
878 pub fn vocab_size(&self) -> usize {
880 self.id_to_token.len()
881 }
882
883 pub fn is_eos(&self, id: u32) -> bool {
885 self.eos_token_id == Some(id) || self.im_end_id == Some(id) || self.extra_eos.contains(&id)
886 }
887}
888
889#[derive(Debug, thiserror::Error)]
890pub enum TokenizerError {
891 #[error("IO error: {0}")]
892 Io(String),
893 #[error("Parse error: {0}")]
894 Parse(String),
895}
896
897#[cfg(test)]
898mod tests {
899 use super::*;
900
901 #[test]
902 fn byte_unicode_bijection() {
903 let (b2c, c2b) = bytes_to_unicode();
904 for b in 0..=255u8 {
905 assert_eq!(c2b[&b2c[b as usize]], b);
906 }
907 assert_eq!(b2c[b' ' as usize], 'Ġ');
909 assert_eq!(b2c[b'\n' as usize], 'Ċ');
910 }
911
912 #[test]
913 fn byte_level_roundtrip_utf8() {
914 let tok = Tokenizer::byte_level();
915 let text = "hello 🌍 hi\n";
916 let ids = tok.encode(text);
917 assert_eq!(ids.len(), text.len()); assert_eq!(tok.decode(&ids), text);
919 }
920
921 fn mini_json() -> String {
924 let vocab: Vec<(&str, u32)> = vec![
926 ("h", 0),
927 ("e", 1),
928 ("l", 2),
929 ("o", 3),
930 ("Ġ", 4),
931 ("w", 5),
932 ("r", 6),
933 ("d", 7),
934 ("he", 8),
935 ("Ġw", 9),
936 ];
937 let vocab_json: String = vocab
938 .iter()
939 .map(|(t, i)| format!("\"{t}\": {i}"))
940 .collect::<Vec<_>>()
941 .join(", ");
942 format!(
943 r#"{{
944 "model": {{
945 "type": "BPE",
946 "vocab": {{ {vocab_json} }},
947 "merges": [["h", "e"], ["Ġ", "w"]]
948 }},
949 "added_tokens": [
950 {{"id": 10, "content": "<|eot|>", "special": true}}
951 ]
952 }}"#
953 )
954 }
955
956 #[test]
957 fn full_pipeline_merges_and_added_tokens() {
958 let tok = Tokenizer::from_json(&mini_json()).unwrap();
959 let ids = tok.encode("hello world");
961 assert_eq!(ids, vec![8, 2, 2, 3, 9, 3, 6, 2, 7]);
962 assert_eq!(tok.decode(&ids), "hello world");
963 let ids2 = tok.encode("he<|eot|>he");
965 assert_eq!(ids2, vec![8, 10, 8]);
966 assert_eq!(tok.decode(&ids2), "hehe");
967 }
968
969 #[test]
970 fn non_ascii_is_never_silently_dropped() {
971 let tok = Tokenizer::from_json(&mini_json()).unwrap();
972 let ids = tok.encode("hello");
975 assert!(!ids.is_empty());
976 }
977}