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 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.ranks.get(&(sym[i].clone(), sym[i + 1].clone())) {
495 if best.map(|(br, _)| r < br).unwrap_or(true) {
496 best = Some((r, i));
497 }
498 }
499 }
500 let Some((_, i)) = best else { break };
501 let merged = format!("{}{}", sym[i], sym[i + 1]);
502 let (left, right) = (sym[i].clone(), sym[i + 1].clone());
504 let mut j = 0;
505 while j + 1 < sym.len() {
506 if sym[j] == left && sym[j + 1] == right {
507 sym[j] = merged.clone();
508 sym.remove(j + 1);
509 }
510 j += 1;
511 }
512 }
513
514 for s in &sym {
515 if let Some(&id) = self.vocab.get(s) {
516 out.push(id);
517 } else {
518 let mut ok = true;
520 for ch in s.chars() {
521 let Some(&b) = self.char_to_byte.get(&ch) else {
522 ok = false;
523 break;
524 };
525 let tok = format!("<0x{:02X}>", b);
526 if let Some(&id) = self.vocab.get(&tok) {
527 out.push(id);
528 } else {
529 ok = false;
530 break;
531 }
532 }
533 if !ok {
534 tracing::error!("tokenizer: no id for symbol {s:?} — dropped");
535 }
536 }
537 }
538 }
539
540 pub fn decode(&self, ids: &[u32]) -> String {
543 let mut bytes: Vec<u8> = Vec::new();
544 for &id in ids {
545 if self.special_ids.contains(&id) {
546 continue;
547 }
548 let idx = id as usize;
549 if idx >= self.id_to_token.len() {
550 continue;
551 }
552 let tok = &self.id_to_token[idx];
553 if self.added_ids.contains(&id) {
554 bytes.extend_from_slice(tok.as_bytes());
555 continue;
556 }
557 if tok.starts_with("<0x") && tok.ends_with('>') && tok.len() == 6 {
559 if let Ok(b) = u8::from_str_radix(&tok[3..5], 16) {
560 bytes.push(b);
561 continue;
562 }
563 }
564 if self.metaspace {
565 for ch in tok.chars() {
567 if ch == '\u{2581}' {
568 bytes.push(b' ');
569 } else {
570 let mut buf = [0u8; 4];
571 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
572 }
573 }
574 continue;
575 }
576 for ch in tok.chars() {
577 match self.char_to_byte.get(&ch) {
578 Some(&b) => bytes.push(b),
579 None => {
582 let mut buf = [0u8; 4];
583 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
584 }
585 }
586 }
587 }
588 let text = String::from_utf8_lossy(&bytes).into_owned();
589 if self.metaspace && self.sp_prepend {
590 if let Some(stripped) = text.strip_prefix(' ') {
592 return stripped.to_string();
593 }
594 }
595 text
596 }
597
598 pub fn decode_token(&self, id: u32) -> String {
601 if self.special_ids.contains(&id) {
602 return String::new();
603 }
604 let idx = id as usize;
605 if idx >= self.id_to_token.len() {
606 return String::new();
607 }
608 let tok = &self.id_to_token[idx];
609 if self.added_ids.contains(&id) {
610 return tok.clone();
611 }
612 if tok.starts_with("<0x") && tok.ends_with('>') && tok.len() == 6 {
613 if let Ok(b) = u8::from_str_radix(&tok[3..5], 16) {
614 return String::from_utf8_lossy(&[b]).into_owned();
615 }
616 }
617 if self.metaspace {
618 return tok.replace('\u{2581}', " ");
619 }
620 let mut bytes = Vec::new();
621 for ch in tok.chars() {
622 match self.char_to_byte.get(&ch) {
623 Some(&b) => bytes.push(b),
624 None => {
625 let mut buf = [0u8; 4];
626 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
627 }
628 }
629 }
630 String::from_utf8_lossy(&bytes).into_owned()
631 }
632
633 pub fn apply_chat_template(&self, messages: &[(String, String)]) -> Vec<u32> {
637 self.apply_chat_template_opts(messages, None)
638 }
639
640 pub fn apply_chat_template_opts(
645 &self,
646 messages: &[(String, String)],
647 enable_thinking: Option<bool>,
648 ) -> Vec<u32> {
649 if let Some(tpl) = &self.chat_template {
650 match self.render_template(tpl, messages, enable_thinking) {
651 Ok(text) => return self.with_bos(self.encode(&text)),
652 Err(e) => {
653 tracing::error!("chat template render failed ({e}); ChatML fallback");
654 }
655 }
656 }
657 self.with_bos(self.chatml_fallback_opts(messages, enable_thinking))
658 }
659
660 pub fn with_bos(&self, mut ids: Vec<u32>) -> Vec<u32> {
662 if self.add_bos {
663 if let Some(b) = self.bos_token_id {
664 if ids.first() != Some(&b) {
665 ids.insert(0, b);
666 }
667 }
668 }
669 ids
670 }
671
672 pub fn render_chat(&self, messages: &[(String, String)]) -> Option<String> {
674 self.render_chat_opts(messages, None)
675 }
676
677 pub fn render_chat_opts(
679 &self,
680 messages: &[(String, String)],
681 enable_thinking: Option<bool>,
682 ) -> Option<String> {
683 let tpl = self.chat_template.as_ref()?;
684 match self.render_template(tpl, messages, enable_thinking) {
685 Ok(t) => Some(t),
686 Err(e) => {
687 tracing::error!("chat template render: {e:#}");
688 None
689 }
690 }
691 }
692
693 fn render_template(
694 &self,
695 tpl: &str,
696 messages: &[(String, String)],
697 enable_thinking: Option<bool>,
698 ) -> Result<String, minijinja::Error> {
699 let mut env = minijinja::Environment::new();
700 env.set_trim_blocks(true);
701 env.set_lstrip_blocks(true);
702 env.set_unknown_method_callback(minijinja_contrib::pycompat::unknown_method_callback);
704 env.add_template("chat", tpl)?;
705 let msgs: Vec<minijinja::Value> = messages
706 .iter()
707 .map(|(role, content)| {
708 minijinja::context! { role => role, content => content }
709 })
710 .collect();
711 let rendered = match enable_thinking {
714 Some(v) => env.get_template("chat")?.render(minijinja::context! {
715 messages => msgs,
716 add_generation_prompt => true,
717 enable_thinking => v,
718 })?,
719 None => env.get_template("chat")?.render(minijinja::context! {
720 messages => msgs,
721 add_generation_prompt => true,
722 })?,
723 };
724 if enable_thinking == Some(false) && !rendered.contains("</think>") {
728 if let Some(pos) = rendered.rfind("assistant") {
729 let mut insert_at = pos + "assistant".len();
730 if let Some(idx) = rendered[insert_at..].find('\n') {
731 insert_at += idx + 1;
732 }
733 let mut out = String::with_capacity(rendered.len() + 24);
734 out.push_str(&rendered[..insert_at]);
735 if !out.ends_with('\n') {
736 out.push('\n');
737 }
738 out.push_str("<think>\n\n</think>\n\n");
739 out.push_str(&rendered[insert_at..]);
740 return Ok(out);
741 }
742 }
743 Ok(rendered)
744 }
745
746 fn chatml_fallback(&self, messages: &[(String, String)]) -> Vec<u32> {
748 self.chatml_fallback_opts(messages, None)
749 }
750
751 fn chatml_fallback_opts(
753 &self,
754 messages: &[(String, String)],
755 enable_thinking: Option<bool>,
756 ) -> Vec<u32> {
757 let mut tokens = Vec::new();
758
759 for (role, content) in messages {
760 if let Some(start_id) = self.im_start_id {
762 tokens.push(start_id);
763 }
764 tokens.extend(self.encode(&format!("{}\n{}", role, content)));
765 if let Some(end_id) = self.im_end_id {
766 tokens.push(end_id);
767 }
768 tokens.extend(self.encode("\n"));
769 }
770
771 if let Some(start_id) = self.im_start_id {
773 tokens.push(start_id);
774 }
775 tokens.extend(self.encode("assistant\n"));
776 if enable_thinking == Some(false) {
777 tokens.extend(self.encode("<think>\n\n</think>\n\n"));
778 }
779
780 tokens
781 }
782
783 pub fn vocab_size(&self) -> usize {
785 self.id_to_token.len()
786 }
787
788 pub fn is_eos(&self, id: u32) -> bool {
790 self.eos_token_id == Some(id) || self.im_end_id == Some(id) || self.extra_eos.contains(&id)
791 }
792}
793
794#[derive(Debug, thiserror::Error)]
795pub enum TokenizerError {
796 #[error("IO error: {0}")]
797 Io(String),
798 #[error("Parse error: {0}")]
799 Parse(String),
800}
801
802#[cfg(test)]
803mod tests {
804 use super::*;
805
806 #[test]
807 fn byte_unicode_bijection() {
808 let (b2c, c2b) = bytes_to_unicode();
809 for b in 0..=255u8 {
810 assert_eq!(c2b[&b2c[b as usize]], b);
811 }
812 assert_eq!(b2c[b' ' as usize], 'Ġ');
814 assert_eq!(b2c[b'\n' as usize], 'Ċ');
815 }
816
817 #[test]
818 fn byte_level_roundtrip_utf8() {
819 let tok = Tokenizer::byte_level();
820 let text = "hello 🌍 hi\n";
821 let ids = tok.encode(text);
822 assert_eq!(ids.len(), text.len()); assert_eq!(tok.decode(&ids), text);
824 }
825
826 fn mini_json() -> String {
829 let vocab: Vec<(&str, u32)> = vec![
831 ("h", 0),
832 ("e", 1),
833 ("l", 2),
834 ("o", 3),
835 ("Ġ", 4),
836 ("w", 5),
837 ("r", 6),
838 ("d", 7),
839 ("he", 8),
840 ("Ġw", 9),
841 ];
842 let vocab_json: String = vocab
843 .iter()
844 .map(|(t, i)| format!("\"{t}\": {i}"))
845 .collect::<Vec<_>>()
846 .join(", ");
847 format!(
848 r#"{{
849 "model": {{
850 "type": "BPE",
851 "vocab": {{ {vocab_json} }},
852 "merges": [["h", "e"], ["Ġ", "w"]]
853 }},
854 "added_tokens": [
855 {{"id": 10, "content": "<|eot|>", "special": true}}
856 ]
857 }}"#
858 )
859 }
860
861 #[test]
862 fn full_pipeline_merges_and_added_tokens() {
863 let tok = Tokenizer::from_json(&mini_json()).unwrap();
864 let ids = tok.encode("hello world");
866 assert_eq!(ids, vec![8, 2, 2, 3, 9, 3, 6, 2, 7]);
867 assert_eq!(tok.decode(&ids), "hello world");
868 let ids2 = tok.encode("he<|eot|>he");
870 assert_eq!(ids2, vec![8, 10, 8]);
871 assert_eq!(tok.decode(&ids2), "hehe");
872 }
873
874 #[test]
875 fn non_ascii_is_never_silently_dropped() {
876 let tok = Tokenizer::from_json(&mini_json()).unwrap();
877 let ids = tok.encode("hello");
880 assert!(!ids.is_empty());
881 }
882}