1use std::path::Path;
14
15use anyhow::{bail, Result};
16use tokenizers::Tokenizer;
17
18pub struct LocalTokenizer {
20 tokenizer: Tokenizer,
21}
22
23impl LocalTokenizer {
24 pub fn from_file(path: &Path) -> Result<Self> {
26 let tokenizer = Tokenizer::from_file(path).map_err(|error| {
27 anyhow::anyhow!("loading {} failed: {error}", path.display())
28 })?;
29 Ok(Self { tokenizer })
30 }
31
32 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
34 let tokenizer = Tokenizer::from_bytes(bytes)
35 .map_err(|error| anyhow::anyhow!("parsing the tokenizer failed: {error}"))?;
36 Ok(Self { tokenizer })
37 }
38
39 pub fn tokenize(&self, content: &str) -> Result<Vec<u32>> {
41 let encoding = self
42 .tokenizer
43 .encode(content, false)
44 .map_err(|error| anyhow::anyhow!("tokenizing failed: {error}"))?;
45 Ok(encoding.get_ids().to_vec())
46 }
47
48 pub fn detokenize(&self, tokens: &[u32]) -> Result<String> {
50 self.tokenizer
51 .decode(tokens, true)
52 .map_err(|error| anyhow::anyhow!("detokenizing failed: {error}"))
53 }
54
55 pub fn vocab_size(&self) -> usize {
57 self.tokenizer.get_vocab_size(true)
58 }
59
60 pub fn verify_special_tokens(&self, expected: &serde_json::Map<String, serde_json::Value>) -> Result<()> {
66 for (name, value) in expected {
67 let Some(text) = value.as_str() else {
68 continue;
69 };
70 if !name.ends_with("_token") {
71 continue;
72 }
73 let known = self.tokenizer.token_to_id(text).is_some();
74 if !known {
75 bail!(
76 "the chat template context sets {name} = {text:?}, but this tokenizer has no \
77 such token; the rendered prompt would not match the model"
78 );
79 }
80 }
81 Ok(())
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn a_missing_file_fails_with_a_clear_message() {
91 let error = LocalTokenizer::from_file(Path::new("/nonexistent/tokenizer.json"))
92 .err()
93 .expect("a missing file must not load");
94 assert!(
95 format!("{error:#}").contains("/nonexistent/tokenizer.json"),
96 "{error:#}"
97 );
98 }
99
100 #[test]
101 fn garbage_bytes_are_rejected() {
102 let error = LocalTokenizer::from_bytes(b"not a tokenizer")
103 .err()
104 .expect("garbage must not parse");
105 assert!(
106 format!("{error:#}").contains("parsing the tokenizer failed"),
107 "{error:#}"
108 );
109 }
110}