use std::collections::HashMap;
use crate::Error;
pub const CL100K_BASE_PATTERN: &str = concat!(
r"(?i:'s|'t|'re|'ve|'m|'ll|'d)",
r"|[^\r\n\p{L}\p{N}]?\p{L}+",
r"|\p{N}{1,3}",
r"| ?[^\s\p{L}\p{N}]+[\r\n]*",
r"|\s*[\r\n]+",
r"|\s+(?!\S)",
r"|\s+",
);
pub const O200K_BASE_PATTERN: &str = concat!(
r"[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]*[\p{Ll}\p{Lm}\p{Lo}\p{M}]+(?i:'s|'t|'re|'ve|'m|'ll|'d)?",
r"|[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]+[\p{Ll}\p{Lm}\p{Lo}\p{M}]*(?i:'s|'t|'re|'ve|'m|'ll|'d)?",
r"|\p{N}{1,3}",
r"| ?[^\s\p{L}\p{N}]+[\r\n/]*",
r"|\s*[\r\n]+",
r"|\s+(?!\S)",
r"|\s+",
);
pub const KIMI_PATTERN: &str = concat!(
r"[\p{Han}]+",
r"|[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]*[\p{Ll}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]+(?i:'s|'t|'re|'ve|'m|'ll|'d)?",
r"|[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]+[\p{Ll}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]*(?i:'s|'t|'re|'ve|'m|'ll|'d)?",
r"|\p{N}{1,3}",
r"| ?[^\s\p{L}\p{N}]+[\r\n]*",
r"|\s*[\r\n]+",
r"|\s+(?!\S)",
r"|\s+",
);
pub const KIMI_RESERVED_SPECIAL_TOKENS: u32 = 256;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TiktokenFamily {
Kimi,
}
impl TiktokenFamily {
#[must_use]
pub fn detect(tokenizer_class: Option<&str>, auto_map_tokenizer: Option<&str>) -> Option<Self> {
let module = auto_map_tokenizer?.split('.').next()?;
match (module, tokenizer_class) {
("tokenization_kimi", Some("TikTokenTokenizer")) => Some(Self::Kimi),
_ => None,
}
}
}
#[derive(Clone, Debug)]
pub struct TiktokenConfig {
pub pattern: String,
pub special_tokens: Vec<(String, u32)>,
}
impl TiktokenConfig {
pub fn new(pattern: impl Into<String>, special_tokens: Vec<(String, u32)>) -> Self {
Self {
pattern: pattern.into(),
special_tokens,
}
}
pub fn cl100k_base() -> Self {
Self::new(
CL100K_BASE_PATTERN,
vec![
("<|endoftext|>".into(), 100257),
("<|fim_prefix|>".into(), 100258),
("<|fim_middle|>".into(), 100259),
("<|fim_suffix|>".into(), 100260),
("<|endofprompt|>".into(), 100276),
],
)
}
pub fn o200k_base() -> Self {
Self::new(
O200K_BASE_PATTERN,
vec![
("<|endoftext|>".into(), 199999),
("<|endofprompt|>".into(), 200018),
],
)
}
pub fn kimi(num_ranks: u32, named: impl IntoIterator<Item = (u32, String)>) -> Self {
let end = num_ranks.saturating_add(KIMI_RESERVED_SPECIAL_TOKENS);
let mut names: HashMap<u32, String> = named
.into_iter()
.filter(|(id, _)| (num_ranks..end).contains(id))
.collect();
let mut special_tokens = Vec::with_capacity(KIMI_RESERVED_SPECIAL_TOKENS as usize);
for id in num_ranks..end {
let content = names
.remove(&id)
.unwrap_or_else(|| format!("<|reserved_token_{id}|>"));
special_tokens.push((content, id));
}
Self::new(KIMI_PATTERN, special_tokens)
}
pub fn from_preset(name: &str) -> Option<Self> {
match name {
"cl100k_base" | "cl100k" => Some(Self::cl100k_base()),
"o200k_base" | "o200k" => Some(Self::o200k_base()),
_ => None,
}
}
}
pub fn parse_tiktoken_model(contents: &str) -> Result<Vec<(Vec<u8>, u32)>, Error> {
let mut out = Vec::new();
for (i, line) in contents.lines().enumerate() {
let line = line.trim();
if line.is_empty() {
continue;
}
let mut fields = line.split_whitespace();
let token_b64 = fields
.next()
.ok_or_else(|| Error::Tiktoken(format!("line {}: empty entry", i + 1)))?;
let rank_str = fields
.next()
.ok_or_else(|| Error::Tiktoken(format!("line {}: missing rank", i + 1)))?;
let bytes = base64_decode(token_b64)
.map_err(|e| Error::Tiktoken(format!("line {}: {e}", i + 1)))?;
let rank: u32 = rank_str
.parse()
.map_err(|_| Error::Tiktoken(format!("line {}: invalid rank {rank_str:?}", i + 1)))?;
out.push((bytes, rank));
}
if out.is_empty() {
return Err(Error::Tiktoken("no token entries found".into()));
}
Ok(out)
}
fn base64_decode(s: &str) -> Result<Vec<u8>, String> {
#[inline]
fn sextet(c: u8) -> Option<u8> {
match c {
b'A'..=b'Z' => Some(c - b'A'),
b'a'..=b'z' => Some(c - b'a' + 26),
b'0'..=b'9' => Some(c - b'0' + 52),
b'+' => Some(62),
b'/' => Some(63),
_ => None,
}
}
let bytes = s.as_bytes();
let mut end = bytes.len();
while end > 0 && bytes[end - 1] == b'=' {
end -= 1;
}
let mut out = Vec::with_capacity(end * 3 / 4 + 1);
let mut acc: u32 = 0;
let mut bits: u32 = 0;
for &c in &bytes[..end] {
let v = sextet(c).ok_or_else(|| format!("invalid base64 character {:?}", c as char))?;
acc = (acc << 6) | v as u32;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((acc >> bits) as u8);
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kimi_pattern_matches_the_model_pat_str() {
const KIMI_PAT_STR_FROM_MODEL: &str = r#"[\p{Han}]+|[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]*[\p{Ll}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]+(?i:'s|'t|'re|'ve|'m|'ll|'d)?|[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]+[\p{Ll}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]*(?i:'s|'t|'re|'ve|'m|'ll|'d)?|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+"#;
assert_eq!(KIMI_PATTERN, KIMI_PAT_STR_FROM_MODEL);
}
#[test]
fn kimi_config_fills_the_reserved_window() {
let config = TiktokenConfig::kimi(163_584, []);
assert_eq!(config.pattern, KIMI_PATTERN);
assert_eq!(config.special_tokens.len(), 256);
assert_eq!(
config.special_tokens.first().unwrap(),
&("<|reserved_token_163584|>".to_string(), 163_584)
);
assert_eq!(
config.special_tokens.last().unwrap(),
&("<|reserved_token_163839|>".to_string(), 163_839)
);
let ids: Vec<u32> = config.special_tokens.iter().map(|&(_, id)| id).collect();
assert_eq!(ids, (163_584..163_840).collect::<Vec<u32>>());
}
#[test]
fn kimi_config_applies_declared_names() {
let config = TiktokenConfig::kimi(
163_584,
[
(163_584, "[BOS]".to_string()),
(163_586, "<|im_end|>".to_string()),
(163_839, "[PAD]".to_string()),
],
);
assert_eq!(config.special_tokens.len(), 256);
let by_id: HashMap<u32, &str> = config
.special_tokens
.iter()
.map(|(content, id)| (*id, content.as_str()))
.collect();
assert_eq!(by_id[&163_584], "[BOS]");
assert_eq!(by_id[&163_586], "<|im_end|>");
assert_eq!(by_id[&163_839], "[PAD]");
assert_eq!(by_id[&163_585], "<|reserved_token_163585|>");
assert_eq!(by_id[&163_700], "<|reserved_token_163700|>");
}
#[test]
fn kimi_config_ignores_ids_outside_the_reserved_window() {
let config = TiktokenConfig::kimi(
163_584,
[
(5, "regular-bpe-token".to_string()),
(163_583, "last-rank".to_string()),
(163_840, "past-the-window".to_string()),
(999_999, "way-out".to_string()),
],
);
assert_eq!(config.special_tokens.len(), 256);
assert!(
config
.special_tokens
.iter()
.all(|(content, _)| content.starts_with("<|reserved_token_")),
"out-of-window names must not leak into the table"
);
}
#[test]
fn kimi_is_absent_from_from_preset() {
assert!(TiktokenConfig::from_preset("kimi").is_none());
}
#[test]
fn base64_roundtrip() {
assert_eq!(base64_decode("IQ==").unwrap(), b"!");
assert_eq!(base64_decode("Ig==").unwrap(), b"\"");
assert_eq!(base64_decode("Iw==").unwrap(), b"#");
assert_eq!(base64_decode("aGVsbG8").unwrap(), b"hello");
assert_eq!(base64_decode("aGVsbG8=").unwrap(), b"hello");
}
#[test]
fn base64_rejects_garbage() {
assert!(base64_decode("not base64!!").is_err());
}
#[test]
fn parse_simple_model() {
let contents = "IQ== 0\nIg== 1\n\nIw== 2\n";
let ranks = parse_tiktoken_model(contents).unwrap();
assert_eq!(
ranks,
vec![(b"!".to_vec(), 0), (b"\"".to_vec(), 1), (b"#".to_vec(), 2)]
);
}
#[test]
fn parse_rejects_empty() {
assert!(parse_tiktoken_model("\n\n").is_err());
}
#[test]
fn parse_rejects_missing_rank() {
assert!(parse_tiktoken_model("IQ==\n").is_err());
}
#[test]
fn presets_resolve() {
assert!(TiktokenConfig::from_preset("cl100k_base").is_some());
assert!(TiktokenConfig::from_preset("o200k_base").is_some());
assert!(TiktokenConfig::from_preset("nope").is_none());
}
#[test]
fn end_to_end_tiny_bpe() {
use crate::Tokenizer;
let ranks: Vec<(Vec<u8>, u32)> = vec![
(b"a".to_vec(), 0),
(b"b".to_vec(), 1),
(b"c".to_vec(), 2),
(b" ".to_vec(), 3),
(b"ab".to_vec(), 4),
(b"abc".to_vec(), 5),
];
let config = TiktokenConfig::new(r"\S+| +", vec![("<|end|>".to_string(), 6)]);
let tok = Tokenizer::from_tiktoken_ranks(&ranks, config).unwrap();
assert_eq!(tok.encode("abc").unwrap(), vec![5]);
assert_eq!(tok.encode("ab").unwrap(), vec![4]);
assert_eq!(tok.encode("abab").unwrap(), vec![4, 4]);
assert_eq!(tok.encode("ab c").unwrap(), vec![4, 3, 2]);
let ids = tok.encode("ab<|end|>c").unwrap();
assert_eq!(ids, vec![4, 6, 2]);
assert_eq!(tok.token_to_id("<|end|>"), Some(6));
assert_eq!(tok.id_to_token(6), Some("<|end|>"));
assert!(tok.is_special_token(6));
assert_eq!(tok.decode(&ids, false).unwrap(), "ab<|end|>c");
assert_eq!(tok.decode(&ids, true).unwrap(), "abc");
}
#[test]
fn prefix_cache_matches_uncached() {
use crate::Tokenizer;
let ranks: Vec<(Vec<u8>, u32)> = (0..256u32).map(|i| (vec![i as u8], i)).collect();
let cfg = TiktokenConfig::new(O200K_BASE_PATTERN, vec![]);
let pa = "the quick brown fox jumps over the lazy dog 12 times\n".repeat(250);
let pb = "SECTION header: values (a, b, c) and 1, 2, 3 -- see notes.\n".repeat(250);
let tails = [
"",
"x",
" trailing",
"\n\n \nmore text here\n",
"!!!weird***",
"\t\tindented line\n",
"'contraction test's fine",
];
let tok = Tokenizer::from_tiktoken_ranks(&ranks, cfg.clone()).unwrap();
let mut inputs = Vec::new();
for p in [&pa, &pb] {
for t in tails {
inputs.push(format!("{p}{t}"));
}
}
let expected: Vec<Vec<u32>> = inputs.iter().map(|s| tok.encode(s).unwrap()).collect();
let mut cached = Tokenizer::from_tiktoken_ranks(&ranks, cfg).unwrap();
cached.enable_input_cache(4);
for _ in 0..2 {
for (s, exp) in inputs.iter().zip(&expected) {
assert_eq!(&cached.encode(s).unwrap(), exp);
}
}
}
#[test]
fn from_str_roundtrip_with_space_byte() {
use crate::Tokenizer;
let contents = "YQ== 0\nIA== 1\nYSA= 2\n";
let config = TiktokenConfig::new(r"\S+| +|\S", vec![]);
let tok = Tokenizer::from_tiktoken_str(contents, config).unwrap();
assert_eq!(tok.decode(&[1], false).unwrap(), " ");
assert_eq!(tok.encode("a").unwrap(), vec![0]);
}
}