1use crate::tokenizer::{SpecialKind, SpecialTokenTable, SpecialTokens, TextOrSpecial};
34use base64::Engine;
35use fancy_regex::Regex;
36use std::collections::HashMap;
37use thiserror::Error;
38
39const PAT_STR: &str = concat!(
44 r"[\p{Han}]+",
45 "|",
46 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)?",
47 "|",
48 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)?",
49 "|",
50 r"\p{N}{1,3}",
51 "|",
52 r" ?[^\s\p{L}\p{N}]+[\r\n]*",
53 "|",
54 r"\s*[\r\n]+",
55 "|",
56 r"\s+(?!\S)",
57 "|",
58 r"\s+",
59);
60
61#[derive(Debug, Error)]
62pub enum KimiTokenizerError {
63 #[error("invalid tiktoken vocab line {0}: {1:?}")]
64 InvalidVocabLine(usize, String),
65 #[error("split regex failed to compile: {0}")]
66 Regex(#[from] fancy_regex::Error),
67 #[error("failed to parse tokenizer_config.json: {0}")]
68 TokenizerConfigJson(#[from] serde_json::Error),
69}
70
71pub fn parse_special_tokens(json_text: &str) -> Result<HashMap<String, u32>, KimiTokenizerError> {
77 #[derive(serde::Deserialize)]
78 struct AddedToken {
79 content: String,
80 }
81 #[derive(serde::Deserialize)]
82 struct TokenizerConfig {
83 #[serde(default)]
84 added_tokens_decoder: HashMap<String, AddedToken>,
85 }
86 let cfg: TokenizerConfig = serde_json::from_str(json_text)?;
87 Ok(cfg
88 .added_tokens_decoder
89 .into_iter()
90 .filter_map(|(id_str, tok)| id_str.parse::<u32>().ok().map(|id| (tok.content, id)))
91 .collect())
92}
93
94pub fn parse_tiktoken_vocab(text: &str) -> Result<HashMap<Vec<u8>, u32>, KimiTokenizerError> {
99 let mut ranks = HashMap::new();
100 for (i, line) in text.lines().enumerate() {
101 let line = line.trim();
102 if line.is_empty() {
103 continue;
104 }
105 let mut parts = line.splitn(2, ' ');
106 let b64 = parts
107 .next()
108 .ok_or_else(|| KimiTokenizerError::InvalidVocabLine(i, line.to_string()))?;
109 let rank_str = parts
110 .next()
111 .ok_or_else(|| KimiTokenizerError::InvalidVocabLine(i, line.to_string()))?;
112 let bytes = base64::engine::general_purpose::STANDARD
113 .decode(b64)
114 .map_err(|_| KimiTokenizerError::InvalidVocabLine(i, line.to_string()))?;
115 let rank: u32 = rank_str
116 .parse()
117 .map_err(|_| KimiTokenizerError::InvalidVocabLine(i, line.to_string()))?;
118 ranks.insert(bytes, rank);
119 }
120 Ok(ranks)
121}
122
123fn byte_pair_merge(piece: &[u8], ranks: &HashMap<Vec<u8>, u32>) -> Vec<u32> {
133 if piece.is_empty() {
134 return Vec::new();
135 }
136 if piece.len() == 1 {
137 return vec![*ranks.get(piece).unwrap_or(&0)];
138 }
139
140 let mut boundaries: Vec<usize> = (0..=piece.len()).collect();
143
144 let rank_of = |boundaries: &[usize], i: usize| -> Option<u32> {
145 if i + 2 >= boundaries.len() {
146 return None;
147 }
148 ranks.get(&piece[boundaries[i]..boundaries[i + 2]]).copied()
149 };
150
151 loop {
152 if boundaries.len() <= 2 {
153 break;
154 }
155 let mut best: Option<(u32, usize)> = None;
156 for i in 0..boundaries.len() - 2 {
157 if let Some(r) = rank_of(&boundaries, i) {
158 if best.is_none_or(|(br, _)| r < br) {
159 best = Some((r, i));
160 }
161 }
162 }
163 match best {
164 Some((_, i)) => {
165 boundaries.remove(i + 1);
166 }
167 None => break,
168 }
169 }
170
171 boundaries
172 .windows(2)
173 .map(|w| {
174 *ranks
175 .get(&piece[w[0]..w[1]])
176 .expect("every final span must be a real vocab entry")
177 })
178 .collect()
179}
180
181pub struct KimiTokenizer {
182 encoder: HashMap<Vec<u8>, u32>,
183 decoder: HashMap<u32, Vec<u8>>,
184 special_tokens: HashMap<String, u32>,
185 special_table: SpecialTokenTable,
188 split_re: Regex,
189}
190
191impl KimiTokenizer {
192 pub fn new(
193 encoder: HashMap<Vec<u8>, u32>,
194 special_tokens: HashMap<String, u32>,
195 ) -> Result<Self, KimiTokenizerError> {
196 let decoder = encoder.iter().map(|(k, v)| (*v, k.clone())).collect();
197 let split_re = Regex::new(PAT_STR)?;
198 let special_table = SpecialTokenTable::from_entries(
199 special_tokens
200 .iter()
201 .map(|(name, &id)| (name.as_str(), id, SpecialKind::Control)),
202 );
203 Ok(KimiTokenizer {
204 encoder,
205 decoder,
206 special_tokens,
207 special_table,
208 split_re,
209 })
210 }
211
212 pub fn vocab_size(&self) -> usize {
213 self.encoder.len()
214 }
215
216 pub fn special_token_id(&self, name: &str) -> Option<u32> {
217 self.special_tokens.get(name).copied()
218 }
219
220 pub fn encode(&self, text: &str, specials: SpecialTokens) -> Vec<u32> {
227 let mut out = Vec::new();
228 for seg in self.special_table.split(text, specials) {
229 match seg {
230 TextOrSpecial::Special(id) => out.push(id),
231 TextOrSpecial::Text(run) => self.encode_text_run(run, &mut out),
232 }
233 }
234 out
235 }
236
237 fn encode_text_run(&self, text: &str, out: &mut Vec<u32>) {
238 for piece in self.split_re.find_iter(text) {
239 let piece = piece.expect("split regex match should not error mid-scan");
240 let bytes = piece.as_str().as_bytes();
241 if let Some(&id) = self.encoder.get(bytes) {
242 out.push(id);
243 continue;
244 }
245 out.extend(byte_pair_merge(bytes, &self.encoder));
246 }
247 }
248
249 pub fn decode(&self, ids: &[u32]) -> String {
250 let mut bytes = Vec::new();
251 for &id in ids {
252 if let Some(b) = self.decoder.get(&id) {
253 bytes.extend_from_slice(b);
254 }
255 }
256 String::from_utf8_lossy(&bytes).into_owned()
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 fn tiny_ranks() -> HashMap<Vec<u8>, u32> {
265 let mut ranks: HashMap<Vec<u8>, u32> = (0u32..256).map(|b| (vec![b as u8], b)).collect();
271 let mut next = 256u32;
272 let add = |bytes: &[u8], ranks: &mut HashMap<Vec<u8>, u32>, next: &mut u32| {
273 ranks.insert(bytes.to_vec(), *next);
274 *next += 1;
275 };
276 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
282 }
283
284 #[test]
285 fn byte_pair_merge_prefers_the_lowest_rank_pair_first() {
286 let ranks = tiny_ranks();
287 let ids = byte_pair_merge(b"hello", &ranks);
288 let hello_id = *ranks.get(b"hello".as_slice()).unwrap();
292 assert_eq!(ids, vec![hello_id]);
293 }
294
295 #[test]
296 fn byte_pair_merge_falls_back_to_single_bytes_with_no_mergeable_pairs() {
297 let ranks = tiny_ranks();
298 let ids = byte_pair_merge(b"xyz", &ranks);
299 assert_eq!(ids, vec![b'x' as u32, b'y' as u32, b'z' as u32]);
300 }
301
302 #[test]
303 fn byte_pair_merge_merges_a_known_pair_but_not_unknown_neighbors() {
304 let ranks = tiny_ranks();
305 let ids = byte_pair_merge(b"hez", &ranks);
307 let he_id = *ranks.get(b"he".as_slice()).unwrap();
308 assert_eq!(ids, vec![he_id, b'z' as u32]);
309 }
310
311 #[test]
312 fn encode_decode_roundtrips_on_a_tiny_synthetic_vocab() {
313 let ranks = tiny_ranks();
314 let tok = KimiTokenizer::new(ranks, HashMap::new()).expect("regex must compile");
315 let ids = tok.encode("hello", SpecialTokens::AsText);
316 let back = tok.decode(&ids);
317 assert_eq!(back, "hello");
318 }
319
320 #[test]
323 fn a_written_marker_is_bytes_as_text_and_one_id_when_parsed() {
324 let ranks = tiny_ranks();
325 let specials = HashMap::from([("[EOS]".to_string(), 9_000u32)]);
326 let tok = KimiTokenizer::new(ranks, specials).expect("regex must compile");
327 let as_text = tok.encode("hello[EOS]", SpecialTokens::AsText);
328 assert!(!as_text.contains(&9_000), "got {as_text:?}");
329 let parsed = tok.encode("hello[EOS]", SpecialTokens::Parse);
330 assert_eq!(parsed.last(), Some(&9_000), "got {parsed:?}");
331 assert_eq!(
332 &parsed[..parsed.len() - 1],
333 &tok.encode("hello", SpecialTokens::AsText)[..]
334 );
335 }
336
337 #[test]
338 fn split_regex_separates_words_punctuation_and_whitespace() {
339 let ranks = tiny_ranks();
340 let tok = KimiTokenizer::new(ranks, HashMap::new()).expect("regex must compile");
341 let pieces: Vec<&str> = tok
342 .split_re
343 .find_iter("hello, world!")
344 .map(|m| m.unwrap().as_str())
345 .collect();
346 assert_eq!(pieces, vec!["hello", ",", " world", "!"]);
347 }
348
349 #[test]
350 fn split_regex_keeps_han_script_separate_from_latin_words() {
351 let ranks = tiny_ranks();
352 let tok = KimiTokenizer::new(ranks, HashMap::new()).expect("regex must compile");
353 let pieces: Vec<&str> = tok
354 .split_re
355 .find_iter("Hi你好")
356 .map(|m| m.unwrap().as_str())
357 .collect();
358 assert_eq!(pieces, vec!["Hi", "你好"]);
362 }
363
364 #[test]
365 fn parses_a_real_tiktoken_format_vocab_file() {
366 let text = "IQ== 0\nIg== 1\n";
369 let ranks = parse_tiktoken_vocab(text).expect("must parse");
370 assert_eq!(ranks.get(&vec![0x21]), Some(&0));
371 assert_eq!(ranks.get(&vec![0x22]), Some(&1));
372 }
373
374 #[test]
375 fn ignores_blank_lines_in_a_tiktoken_vocab_file() {
376 let text = "IQ== 0\n\nIg== 1\n\n";
377 let ranks = parse_tiktoken_vocab(text).expect("must parse");
378 assert_eq!(ranks.len(), 2);
379 }
380
381 #[test]
382 fn parses_real_shaped_special_tokens_from_tokenizer_config_json() {
383 let json = r#"{
387 "added_tokens_decoder": {
388 "163584": {"content": "[BOS]", "special": true},
389 "163585": {"content": "[EOS]", "special": true},
390 "163586": {"content": "<|end_of_msg|>", "special": true}
391 }
392 }"#;
393 let tokens = parse_special_tokens(json).expect("must parse");
394 assert_eq!(tokens.get("[BOS]"), Some(&163584));
395 assert_eq!(tokens.get("[EOS]"), Some(&163585));
396 assert_eq!(tokens.get("<|end_of_msg|>"), Some(&163586));
397 assert_eq!(tokens.len(), 3);
398 }
399}