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+",
);
#[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 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 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]);
}
}