1use base64::Engine;
34use fancy_regex::Regex;
35use std::collections::HashMap;
36use thiserror::Error;
37
38const PAT_STR: &str = concat!(
43 r"[\p{Han}]+",
44 "|",
45 r"[^\r\n\p{L}\p{N}]?(?:(?!\p{Han})[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}])*(?:(?!\p{Han})[\p{Ll}\p{Lm}\p{Lo}\p{M}])+(?i:'s|'t|'re|'ve|'m|'ll|'d)?",
46 "|",
47 r"[^\r\n\p{L}\p{N}]?(?:(?!\p{Han})[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}])+(?:(?!\p{Han})[\p{Ll}\p{Lm}\p{Lo}\p{M}])*(?i:'s|'t|'re|'ve|'m|'ll|'d)?",
48 "|",
49 r"\p{N}{1,3}",
50 "|",
51 r" ?[^\s\p{L}\p{N}]+[\r\n]*",
52 "|",
53 r"\s*[\r\n]+",
54 "|",
55 r"\s+(?!\S)",
56 "|",
57 r"\s+",
58);
59
60#[derive(Debug, Error)]
61pub enum KimiTokenizerError {
62 #[error("invalid tiktoken vocab line {0}: {1:?}")]
63 InvalidVocabLine(usize, String),
64 #[error("split regex failed to compile: {0}")]
65 Regex(#[from] fancy_regex::Error),
66 #[error("failed to parse tokenizer_config.json: {0}")]
67 TokenizerConfigJson(#[from] serde_json::Error),
68}
69
70pub fn parse_special_tokens(json_text: &str) -> Result<HashMap<String, u32>, KimiTokenizerError> {
76 #[derive(serde::Deserialize)]
77 struct AddedToken {
78 content: String,
79 }
80 #[derive(serde::Deserialize)]
81 struct TokenizerConfig {
82 #[serde(default)]
83 added_tokens_decoder: HashMap<String, AddedToken>,
84 }
85 let cfg: TokenizerConfig = serde_json::from_str(json_text)?;
86 Ok(cfg
87 .added_tokens_decoder
88 .into_iter()
89 .filter_map(|(id_str, tok)| id_str.parse::<u32>().ok().map(|id| (tok.content, id)))
90 .collect())
91}
92
93pub fn parse_tiktoken_vocab(text: &str) -> Result<HashMap<Vec<u8>, u32>, KimiTokenizerError> {
98 let mut ranks = HashMap::new();
99 for (i, line) in text.lines().enumerate() {
100 let line = line.trim();
101 if line.is_empty() {
102 continue;
103 }
104 let mut parts = line.splitn(2, ' ');
105 let b64 = parts
106 .next()
107 .ok_or_else(|| KimiTokenizerError::InvalidVocabLine(i, line.to_string()))?;
108 let rank_str = parts
109 .next()
110 .ok_or_else(|| KimiTokenizerError::InvalidVocabLine(i, line.to_string()))?;
111 let bytes = base64::engine::general_purpose::STANDARD
112 .decode(b64)
113 .map_err(|_| KimiTokenizerError::InvalidVocabLine(i, line.to_string()))?;
114 let rank: u32 = rank_str
115 .parse()
116 .map_err(|_| KimiTokenizerError::InvalidVocabLine(i, line.to_string()))?;
117 ranks.insert(bytes, rank);
118 }
119 Ok(ranks)
120}
121
122fn byte_pair_merge(piece: &[u8], ranks: &HashMap<Vec<u8>, u32>) -> Vec<u32> {
132 if piece.is_empty() {
133 return Vec::new();
134 }
135 if piece.len() == 1 {
136 return vec![*ranks.get(piece).unwrap_or(&0)];
137 }
138
139 let mut boundaries: Vec<usize> = (0..=piece.len()).collect();
142
143 let rank_of = |boundaries: &[usize], i: usize| -> Option<u32> {
144 if i + 2 >= boundaries.len() {
145 return None;
146 }
147 ranks.get(&piece[boundaries[i]..boundaries[i + 2]]).copied()
148 };
149
150 loop {
151 if boundaries.len() <= 2 {
152 break;
153 }
154 let mut best: Option<(u32, usize)> = None;
155 for i in 0..boundaries.len() - 2 {
156 if let Some(r) = rank_of(&boundaries, i) {
157 if best.is_none_or(|(br, _)| r < br) {
158 best = Some((r, i));
159 }
160 }
161 }
162 match best {
163 Some((_, i)) => {
164 boundaries.remove(i + 1);
165 }
166 None => break,
167 }
168 }
169
170 boundaries
171 .windows(2)
172 .map(|w| {
173 *ranks
174 .get(&piece[w[0]..w[1]])
175 .expect("every final span must be a real vocab entry")
176 })
177 .collect()
178}
179
180pub struct KimiTokenizer {
181 encoder: HashMap<Vec<u8>, u32>,
182 decoder: HashMap<u32, Vec<u8>>,
183 special_tokens: HashMap<String, u32>,
184 split_re: Regex,
185}
186
187impl KimiTokenizer {
188 pub fn new(
189 encoder: HashMap<Vec<u8>, u32>,
190 special_tokens: HashMap<String, u32>,
191 ) -> Result<Self, KimiTokenizerError> {
192 let decoder = encoder.iter().map(|(k, v)| (*v, k.clone())).collect();
193 let split_re = Regex::new(PAT_STR)?;
194 Ok(KimiTokenizer {
195 encoder,
196 decoder,
197 special_tokens,
198 split_re,
199 })
200 }
201
202 pub fn vocab_size(&self) -> usize {
203 self.encoder.len()
204 }
205
206 pub fn special_token_id(&self, name: &str) -> Option<u32> {
207 self.special_tokens.get(name).copied()
208 }
209
210 pub fn encode(&self, text: &str) -> Vec<u32> {
216 let mut out = Vec::new();
217 for piece in self.split_re.find_iter(text) {
218 let piece = piece.expect("split regex match should not error mid-scan");
219 let bytes = piece.as_str().as_bytes();
220 if let Some(&id) = self.encoder.get(bytes) {
221 out.push(id);
222 continue;
223 }
224 out.extend(byte_pair_merge(bytes, &self.encoder));
225 }
226 out
227 }
228
229 pub fn decode(&self, ids: &[u32]) -> String {
230 let mut bytes = Vec::new();
231 for &id in ids {
232 if let Some(b) = self.decoder.get(&id) {
233 bytes.extend_from_slice(b);
234 }
235 }
236 String::from_utf8_lossy(&bytes).into_owned()
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 fn tiny_ranks() -> HashMap<Vec<u8>, u32> {
245 let mut ranks: HashMap<Vec<u8>, u32> = (0u32..256).map(|b| (vec![b as u8], b)).collect();
251 let mut next = 256u32;
252 let add = |bytes: &[u8], ranks: &mut HashMap<Vec<u8>, u32>, next: &mut u32| {
253 ranks.insert(bytes.to_vec(), *next);
254 *next += 1;
255 };
256 add(b"he", &mut ranks, &mut next); add(b"ll", &mut ranks, &mut next); add(b"hel", &mut ranks, &mut next); add(b"hell", &mut ranks, &mut next); add(b"hello", &mut ranks, &mut next); ranks
262 }
263
264 #[test]
265 fn byte_pair_merge_prefers_the_lowest_rank_pair_first() {
266 let ranks = tiny_ranks();
267 let ids = byte_pair_merge(b"hello", &ranks);
268 let hello_id = *ranks.get(b"hello".as_slice()).unwrap();
272 assert_eq!(ids, vec![hello_id]);
273 }
274
275 #[test]
276 fn byte_pair_merge_falls_back_to_single_bytes_with_no_mergeable_pairs() {
277 let ranks = tiny_ranks();
278 let ids = byte_pair_merge(b"xyz", &ranks);
279 assert_eq!(ids, vec![b'x' as u32, b'y' as u32, b'z' as u32]);
280 }
281
282 #[test]
283 fn byte_pair_merge_merges_a_known_pair_but_not_unknown_neighbors() {
284 let ranks = tiny_ranks();
285 let ids = byte_pair_merge(b"hez", &ranks);
287 let he_id = *ranks.get(b"he".as_slice()).unwrap();
288 assert_eq!(ids, vec![he_id, b'z' as u32]);
289 }
290
291 #[test]
292 fn encode_decode_roundtrips_on_a_tiny_synthetic_vocab() {
293 let ranks = tiny_ranks();
294 let tok = KimiTokenizer::new(ranks, HashMap::new()).expect("regex must compile");
295 let ids = tok.encode("hello");
296 let back = tok.decode(&ids);
297 assert_eq!(back, "hello");
298 }
299
300 #[test]
301 fn split_regex_separates_words_punctuation_and_whitespace() {
302 let ranks = tiny_ranks();
303 let tok = KimiTokenizer::new(ranks, HashMap::new()).expect("regex must compile");
304 let pieces: Vec<&str> = tok
305 .split_re
306 .find_iter("hello, world!")
307 .map(|m| m.unwrap().as_str())
308 .collect();
309 assert_eq!(pieces, vec!["hello", ",", " world", "!"]);
310 }
311
312 #[test]
313 fn split_regex_keeps_han_script_separate_from_latin_words() {
314 let ranks = tiny_ranks();
315 let tok = KimiTokenizer::new(ranks, HashMap::new()).expect("regex must compile");
316 let pieces: Vec<&str> = tok
317 .split_re
318 .find_iter("Hi你好")
319 .map(|m| m.unwrap().as_str())
320 .collect();
321 assert_eq!(pieces, vec!["Hi", "你好"]);
325 }
326
327 #[test]
328 fn parses_a_real_tiktoken_format_vocab_file() {
329 let text = "IQ== 0\nIg== 1\n";
332 let ranks = parse_tiktoken_vocab(text).expect("must parse");
333 assert_eq!(ranks.get(&vec![0x21]), Some(&0));
334 assert_eq!(ranks.get(&vec![0x22]), Some(&1));
335 }
336
337 #[test]
338 fn ignores_blank_lines_in_a_tiktoken_vocab_file() {
339 let text = "IQ== 0\n\nIg== 1\n\n";
340 let ranks = parse_tiktoken_vocab(text).expect("must parse");
341 assert_eq!(ranks.len(), 2);
342 }
343
344 #[test]
345 fn parses_real_shaped_special_tokens_from_tokenizer_config_json() {
346 let json = r#"{
350 "added_tokens_decoder": {
351 "163584": {"content": "[BOS]", "special": true},
352 "163585": {"content": "[EOS]", "special": true},
353 "163586": {"content": "<|end_of_msg|>", "special": true}
354 }
355 }"#;
356 let tokens = parse_special_tokens(json).expect("must parse");
357 assert_eq!(tokens.get("[BOS]"), Some(&163584));
358 assert_eq!(tokens.get("[EOS]"), Some(&163585));
359 assert_eq!(tokens.get("<|end_of_msg|>"), Some(&163586));
360 assert_eq!(tokens.len(), 3);
361 }
362}