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