use std::collections::{HashMap, HashSet};
use crate::error::{FocrError, FocrResult};
use super::pretok::in_ranges;
use super::unicode_tables as ucd;
use super::utf8_char_len;
pub const ENDOFTEXT: u32 = 151_643;
pub const IM_START: u32 = 151_644;
pub const IM_END: u32 = 151_645;
pub const EXTRA_BASE: u32 = 151_646;
pub const NUM_EXTRAS: u32 = 205;
pub const REF: u32 = 151_851;
pub const REF_END: u32 = 151_852;
pub const BOX: u32 = 151_853;
pub const BOX_END: u32 = 151_854;
pub const QUAD: u32 = 151_855;
pub const QUAD_END: u32 = 151_856;
pub const IMG_START: u32 = 151_857;
pub const IMG_END: u32 = 151_858;
pub const IMG_PAD: u32 = 151_859;
pub const N_VOCAB: usize = 151_860;
const N_BASE: usize = 151_643;
const N_SPECIAL: usize = 217;
#[derive(Debug)]
pub struct Tiktoken {
ranks: HashMap<Vec<u8>, u32>,
rev: Vec<Vec<u8>>,
special_by_content: HashMap<String, u32>,
special_ids: HashSet<u32>,
specials_sorted: Vec<(String, u32)>,
bos: u32,
eos: u32,
pad: u32,
}
impl Tiktoken {
pub fn from_qwen_tiktoken(file: &[u8]) -> FocrResult<Self> {
let mut ranks: HashMap<Vec<u8>, u32> = HashMap::with_capacity(N_BASE);
let mut max_rank: i64 = -1;
for (lineno, line) in file.split(|&b| b == b'\n').enumerate() {
if line.is_empty() {
continue;
}
let sp = line.iter().position(|&b| b == b' ').ok_or_else(|| {
FocrError::FormatMismatch(format!(
"qwen.tiktoken line {lineno}: no space separator"
))
})?;
let tok = b64_decode(&line[..sp]).ok_or_else(|| {
FocrError::FormatMismatch(format!(
"qwen.tiktoken line {lineno}: invalid base64 token"
))
})?;
let rank_str = std::str::from_utf8(&line[sp + 1..]).map_err(|_| {
FocrError::FormatMismatch(format!("qwen.tiktoken line {lineno}: rank not UTF-8"))
})?;
let rank: u32 = rank_str.trim().parse().map_err(|_| {
FocrError::FormatMismatch(format!("qwen.tiktoken line {lineno}: rank not a u32"))
})?;
max_rank = max_rank.max(i64::from(rank));
ranks.insert(tok, rank);
}
if ranks.len() != N_BASE || max_rank != (N_BASE as i64 - 1) {
return Err(FocrError::FormatMismatch(format!(
"qwen.tiktoken: expected {N_BASE} dense ranks 0..={}, got {} entries (max rank {max_rank})",
N_BASE - 1,
ranks.len()
)));
}
for b in 0u8..=255 {
if !ranks.contains_key(std::slice::from_ref(&b)) {
return Err(FocrError::FormatMismatch(format!(
"qwen.tiktoken: single byte 0x{b:02x} missing (no byte fallback possible)"
)));
}
}
let mut specials: Vec<(String, u32)> = Vec::with_capacity(N_SPECIAL);
specials.push(("<|endoftext|>".to_string(), ENDOFTEXT));
specials.push(("<|im_start|>".to_string(), IM_START));
specials.push(("<|im_end|>".to_string(), IM_END));
for i in 0..NUM_EXTRAS {
specials.push((format!("<|extra_{i}|>"), EXTRA_BASE + i)); }
for (s, id) in [
("<ref>", REF),
("</ref>", REF_END),
("<box>", BOX),
("</box>", BOX_END),
("<quad>", QUAD),
("</quad>", QUAD_END),
("<img>", IMG_START),
("</img>", IMG_END),
("<imgpad>", IMG_PAD),
] {
specials.push((s.to_string(), id));
}
debug_assert_eq!(specials.len(), N_SPECIAL);
let mut rev: Vec<Vec<u8>> = vec![Vec::new(); N_VOCAB];
for (tok, &rank) in &ranks {
rev[rank as usize] = tok.clone();
}
let mut special_by_content = HashMap::with_capacity(N_SPECIAL);
let mut special_ids = HashSet::with_capacity(N_SPECIAL);
for (content, id) in &specials {
rev[*id as usize] = content.as_bytes().to_vec();
special_by_content.insert(content.clone(), *id);
special_ids.insert(*id);
}
let mut specials_sorted = specials;
specials_sorted.sort_by(|a, b| b.0.len().cmp(&a.0.len()).then(a.1.cmp(&b.1)));
Ok(Self {
ranks,
rev,
special_by_content,
special_ids,
specials_sorted,
bos: ENDOFTEXT,
eos: ENDOFTEXT,
pad: ENDOFTEXT,
})
}
pub fn encode(&self, text: &str) -> FocrResult<Vec<u32>> {
let mut out = Vec::new();
let bytes = text.as_bytes();
let mut i = 0usize;
let mut run_start = 0usize;
while i < bytes.len() {
let mut matched = None;
for (content, id) in &self.specials_sorted {
let c = content.as_bytes();
if bytes[i..].starts_with(c) {
matched = Some((c.len(), *id));
break; }
}
if let Some((len, id)) = matched {
if run_start < i {
self.encode_ordinary_str(&text[run_start..i], &mut out)?;
}
out.push(id);
i += len;
run_start = i;
} else {
i += utf8_char_len(bytes[i]);
}
}
if run_start < text.len() {
self.encode_ordinary_str(&text[run_start..], &mut out)?;
}
Ok(out)
}
pub fn encode_ordinary(&self, text: &str) -> FocrResult<Vec<u32>> {
let mut out = Vec::new();
self.encode_ordinary_str(text, &mut out)?;
Ok(out)
}
fn encode_ordinary_str(&self, text: &str, out: &mut Vec<u32>) -> FocrResult<()> {
let chars: Vec<char> = text.chars().collect();
let mut i = 0usize;
while i < chars.len() {
let len = match_piece(&chars, i).unwrap_or(1);
let piece: String = chars[i..i + len].iter().collect();
self.bpe_bytes(piece.as_bytes(), out)?;
i += len;
}
Ok(())
}
fn bpe_bytes(&self, piece: &[u8], out: &mut Vec<u32>) -> FocrResult<()> {
if let Some(&r) = self.ranks.get(piece) {
out.push(r);
return Ok(());
}
let mut parts: Vec<(usize, usize)> = (0..piece.len()).map(|k| (k, k + 1)).collect();
loop {
let mut best: Option<(usize, u32)> = None;
for i in 0..parts.len().saturating_sub(1) {
let a = parts[i].0;
let c = parts[i + 1].1;
if let Some(&r) = self.ranks.get(&piece[a..c]) {
match best {
Some((_, br)) if r >= br => {}
_ => best = Some((i, r)),
}
}
}
let Some((i, _)) = best else { break };
let a = parts[i].0;
let c = parts[i + 1].1;
parts[i] = (a, c);
parts.remove(i + 1);
}
for (a, c) in parts {
let r = self.ranks.get(&piece[a..c]).copied().ok_or_else(|| {
FocrError::FormatMismatch("tiktoken: a final BPE segment was not in ranks".into())
})?; out.push(r);
}
Ok(())
}
pub fn decode(&self, ids: &[u32]) -> FocrResult<String> {
self.decode_inner(ids, false)
}
pub fn decode_skip_special(&self, ids: &[u32]) -> FocrResult<String> {
self.decode_inner(ids, true)
}
fn decode_inner(&self, ids: &[u32], skip_special: bool) -> FocrResult<String> {
let mut buf: Vec<u8> = Vec::new();
for &id in ids {
if skip_special && self.special_ids.contains(&id) {
continue;
}
let bytes = self.rev.get(id as usize).ok_or_else(|| {
FocrError::FormatMismatch(format!("decode: token id {id} out of range"))
})?;
buf.extend_from_slice(bytes);
}
Ok(String::from_utf8_lossy(&buf).into_owned())
}
#[must_use]
pub fn token_to_id(&self, content: &str) -> Option<u32> {
self.special_by_content
.get(content)
.copied()
.or_else(|| self.ranks.get(content.as_bytes()).copied())
}
#[must_use]
pub fn id_to_token(&self, id: u32) -> Option<String> {
let b = self.rev.get(id as usize)?;
if b.is_empty() {
return None;
}
Some(String::from_utf8_lossy(b).into_owned())
}
#[must_use]
pub fn vocab_size(&self) -> usize {
N_VOCAB
}
#[must_use]
pub fn bos_id(&self) -> u32 {
self.bos
}
#[must_use]
pub fn eos_id(&self) -> u32 {
self.eos
}
#[must_use]
pub fn pad_id(&self) -> u32 {
self.pad
}
#[must_use]
pub fn image_pad_id(&self) -> u32 {
IMG_PAD
}
}
#[inline]
fn is_l(c: char) -> bool {
in_ranges(c as u32, ucd::LETTER)
}
#[inline]
fn is_n(c: char) -> bool {
in_ranges(c as u32, ucd::NUMBER)
}
#[inline]
fn is_ws(c: char) -> bool {
c.is_whitespace() }
fn match_piece(chars: &[char], i: usize) -> Option<usize> {
let n = chars.len();
if chars[i] == '\'' {
const SUFFIXES: [&[u8]; 7] = [b"s", b"t", b"re", b"ve", b"m", b"ll", b"d"];
for suf in SUFFIXES {
if i + 1 + suf.len() <= n
&& suf.iter().enumerate().all(|(k, &b)| {
chars[i + 1 + k].is_ascii() && (chars[i + 1 + k] as u8).eq_ignore_ascii_case(&b)
})
{
return Some(1 + suf.len());
}
}
}
{
let c0 = chars[i];
let mut j = i;
if c0 != '\r'
&& c0 != '\n'
&& !is_l(c0)
&& !is_n(c0)
&& chars.get(i + 1).is_some_and(|&c| is_l(c))
{
j = i + 1; }
let run_start = j;
while j < n && is_l(chars[j]) {
j += 1;
}
if j > run_start {
return Some(j - i);
}
}
if is_n(chars[i]) {
return Some(1);
}
{
let mut j = i;
if chars[i] == ' ' {
j += 1;
}
let run_start = j;
while j < n {
let c = chars[j];
if !is_ws(c) && !is_l(c) && !is_n(c) {
j += 1;
} else {
break;
}
}
if j > run_start {
while j < n && (chars[j] == '\r' || chars[j] == '\n') {
j += 1;
}
return Some(j - i);
}
}
{
let mut last_crlf_end = None;
let mut k = i;
while k < n && is_ws(chars[k]) {
if chars[k] == '\r' || chars[k] == '\n' {
last_crlf_end = Some(k + 1);
}
k += 1;
}
if let Some(end) = last_crlf_end {
return Some(end - i);
}
}
{
let mut j = i;
while j < n && is_ws(chars[j]) {
j += 1;
}
let w = j - i;
if w >= 1 {
if j == n {
return Some(w);
} else if w >= 2 {
return Some(w - 1);
}
}
}
{
let mut j = i;
while j < n && is_ws(chars[j]) {
j += 1;
}
if j > i {
return Some(j - i);
}
}
None }
fn b64_decode(input: &[u8]) -> Option<Vec<u8>> {
#[inline]
fn val(b: u8) -> Option<u8> {
match b {
b'A'..=b'Z' => Some(b - b'A'),
b'a'..=b'z' => Some(b - b'a' + 26),
b'0'..=b'9' => Some(b - b'0' + 52),
b'+' => Some(62),
b'/' => Some(63),
_ => None,
}
}
let mut s = input;
while s.last() == Some(&b'=') {
s = &s[..s.len() - 1];
}
let mut out = Vec::with_capacity(s.len() * 3 / 4);
let mut acc: u32 = 0;
let mut nbits: u32 = 0;
for &b in s {
let v = u32::from(val(b)?);
acc = (acc << 6) | v;
nbits += 6;
if nbits >= 8 {
nbits -= 8;
out.push((acc >> nbits) as u8);
}
}
if nbits > 0 && (acc & ((1 << nbits) - 1)) != 0 {
return None;
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn b64_decodes_known_vectors() {
assert_eq!(b64_decode(b"SGVsbG8=").unwrap(), b"Hello".to_vec());
assert_eq!(b64_decode(b"IQ==").unwrap(), vec![0x21]); assert_eq!(b64_decode(b"Ig==").unwrap(), vec![0x22]); assert_eq!(b64_decode(b"").unwrap(), Vec::<u8>::new());
assert_eq!(b64_decode(b"++//").unwrap(), vec![0xfb, 0xef, 0xff]);
assert!(b64_decode(b"****").is_none());
}
fn pieces(text: &str) -> Vec<String> {
let chars: Vec<char> = text.chars().collect();
let mut out = Vec::new();
let mut i = 0;
while i < chars.len() {
let len = match_piece(&chars, i).unwrap();
out.push(chars[i..i + len].iter().collect());
i += len;
}
out
}
#[test]
fn pretokenizer_partitions_match_qwen_pattern() {
for s in [
"Hello, world!",
" leading and multiple spaces",
"Line1\nLine2\n",
"café 1234 ²x",
"fn main() {}",
] {
assert_eq!(
pieces(s).concat(),
s,
"partition must be lossless for {s:?}"
);
}
assert_eq!(pieces("1234"), vec!["1", "2", "3", "4"]);
assert_eq!(pieces(" world"), vec![" world"]);
assert_eq!(pieces("It's"), vec!["It", "'s"]);
assert_eq!(pieces("IT'S"), vec!["IT", "'S"]);
}
fn load_real() -> Option<Tiktoken> {
let p = std::env::var("FOCR_GOT_TIKTOKEN").ok()?;
let bytes = std::fs::read(p).ok()?;
Some(Tiktoken::from_qwen_tiktoken(&bytes).expect("real qwen.tiktoken must parse"))
}
#[test]
fn loads_and_validates_real_vocab() {
let Some(tk) = load_real() else {
return;
};
assert_eq!(tk.vocab_size(), N_VOCAB);
assert_eq!(tk.eos_id(), ENDOFTEXT);
assert_eq!(tk.image_pad_id(), IMG_PAD);
}
#[test]
fn digit_split_canary() {
let Some(tk) = load_real() else {
return;
};
assert_eq!(
tk.encode_ordinary("1234567890").unwrap(),
vec![16, 17, 18, 19, 20, 21, 22, 23, 24, 15]
);
}
#[test]
fn special_vs_ordinary_split() {
let Some(tk) = load_real() else {
return;
};
assert_eq!(
tk.encode("say <|endoftext|> now").unwrap(),
vec![36790, 220, 151643, 1431]
);
assert_eq!(
tk.encode_ordinary("say <|endoftext|> now").unwrap(),
vec![36790, 82639, 8691, 723, 427, 91, 29, 1431]
);
}
#[test]
fn decode_specials_and_grounding_ids() {
let Some(tk) = load_real() else {
return;
};
assert_eq!(
tk.decode(&[151643, 151857, 151859, 151858]).unwrap(),
"<|endoftext|><img><imgpad></img>"
);
assert_eq!(tk.token_to_id("<ref>"), Some(REF));
assert_eq!(tk.token_to_id("<quad>"), Some(QUAD));
assert_eq!(tk.token_to_id("<imgpad>"), Some(IMG_PAD));
}
#[test]
fn id_bytes_spotcheck_and_lossy_decode() {
let Some(tk) = load_real() else {
return;
};
assert_eq!(tk.decode(&[9707]).unwrap(), "Hello");
assert_eq!(tk.decode(&[1879]).unwrap(), " world");
assert_eq!(tk.decode(&[15]).unwrap(), "0");
assert_eq!(tk.decode(&[108386]).unwrap(), "你好");
assert_eq!(tk.decode(&[11162]).unwrap(), " \u{fffd}");
}
#[test]
fn token_id_conformance_gate() {
let Some(tk) = load_real() else {
return;
};
const EXPECTED: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/tokenizer_got/expected.json"
));
let v: serde_json::Value = serde_json::from_str(EXPECTED).unwrap();
let cases = v["fixtures"].as_object().expect("fixtures object");
let mut mismatches = 0usize;
for (text, ids) in cases {
let want: Vec<u32> = ids
.as_array()
.unwrap()
.iter()
.map(|x| x.as_u64().unwrap() as u32)
.collect();
let got = tk.encode(text).unwrap();
if got != want {
eprintln!("MISMATCH {text:?}\n got {got:?}\n want {want:?}");
mismatches += 1;
}
}
assert_eq!(
mismatches, 0,
"tok_id_mismatch_count must be 0 (got {mismatches})"
);
assert_eq!(cases.len(), 24, "expected the 24 golden cases");
}
#[test]
fn prompt_id_oracle_cross_check() {
let Some(tk) = load_real() else {
return;
};
const L0C: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/got/l0c_prompt.json"
));
let v: serde_json::Value = serde_json::from_str(L0C).unwrap();
let prompt = v["prompt"].as_str().unwrap();
let want: Vec<u32> = v["ids"]
.as_array()
.unwrap()
.iter()
.map(|x| x.as_u64().unwrap() as u32)
.collect();
let got = tk.encode(prompt).unwrap();
assert_eq!(got.len(), 287, "GOT plain-OCR prompt is 287 ids");
assert_eq!(
got, want,
"Rust tiktoken must match the torch-oracle GOT prompt ids exactly"
);
assert_eq!(
got.iter().filter(|&&id| id == IMG_PAD).count(),
256,
"256 <imgpad> image slots"
);
}
#[test]
fn round_trip_byte_reconstructable_subset() {
let Some(tk) = load_real() else {
return;
};
for s in [
"Hello, world!",
"The quick brown fox jumps over the lazy dog.",
"snake_case camelCase PascalCase kebab-case",
"你好,世界!这是一个测试。",
"https://example.com/path?q=1&r=2#frag-2",
] {
let ids = tk.encode(s).unwrap();
assert_eq!(tk.decode(&ids).unwrap(), s, "round-trip for {s:?}");
}
}
}