use std::collections::HashMap;
use std::path::Path;
use serde::Deserialize;
use crate::error::{FocrError, FocrResult};
pub mod music;
mod ops;
pub mod pretok;
pub mod tiktoken;
mod unicode_tables;
pub use ops::TokenizerOps;
pub mod special_smollm2 {
pub const UNK: u32 = 0;
pub const BOS: u32 = 1;
pub const PAD: u32 = 2;
pub const GLOBAL_IMG: u32 = 49152;
pub const FAKE_AROUND_IMAGE: u32 = 49189;
pub const IMAGE: u32 = 49190;
pub const END_OF_UTTERANCE: u32 = 49279;
}
pub mod special_opt {
pub const PAD: u32 = 1;
pub const BOS_EOS: u32 = 2;
pub const IMG_PAD: u32 = 50_265;
pub const IMG_START: u32 = 50_266;
pub const IMG_END: u32 = 50_267;
pub const NUMBER: u32 = 50_268;
}
pub mod special {
pub const BOS: u32 = 0;
pub const EOS: u32 = 1;
pub const PAD: u32 = 2;
pub const IMAGE: u32 = 128815;
pub const REF: u32 = 128816;
pub const REF_END: u32 = 128817;
pub const DET: u32 = 128818;
pub const DET_END: u32 = 128819;
pub const GROUNDING: u32 = 128820;
pub const TD: u32 = 128821;
pub const TD_END: u32 = 128822;
pub const TR: u32 = 128823;
pub const TR_END: u32 = 128824;
pub const USER: u32 = 128825;
pub const ASSISTANT: u32 = 128826;
pub const PAGE: u32 = 128827;
pub const BLOCK: u32 = 128828;
pub const FORK: u32 = 128829;
pub const CHILD: u32 = 128830;
}
#[derive(Debug, Deserialize)]
struct RawTokenizer {
#[serde(default)]
added_tokens: Vec<RawAddedToken>,
model: RawModel,
#[serde(default)]
pre_tokenizer: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
struct RawAddedToken {
id: u32,
content: String,
#[serde(default)]
special: bool,
}
#[derive(Debug, Deserialize)]
struct RawModel {
vocab: HashMap<String, u32>,
#[serde(default)]
merges: Vec<RawMerge>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum RawMerge {
Pair([String; 2]),
Joined(String),
}
impl RawMerge {
fn into_pair(self) -> Option<(String, String)> {
match self {
RawMerge::Pair([l, r]) => Some((l, r)),
RawMerge::Joined(s) => {
let mut it = s.splitn(2, ' ');
let l = it.next()?.to_string();
let r = it.next()?.to_string();
Some((l, r))
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PretokScheme {
DeepSeekV2,
SmolLm2,
Gpt2,
}
fn classify_pretok(v: Option<&serde_json::Value>) -> FocrResult<PretokScheme> {
let Some(v) = v else {
return Ok(PretokScheme::DeepSeekV2);
};
let stages = v
.get("pretokenizers")
.and_then(|p| p.as_array())
.ok_or_else(|| {
FocrError::FormatMismatch(format!(
"unsupported tokenizer.json pre_tokenizer (expected a Sequence): {v}"
))
})?;
fn ty(s: &serde_json::Value) -> &str {
s.get("type").and_then(|t| t.as_str()).unwrap_or("")
}
if stages.len() == 2
&& ty(&stages[0]) == "Digits"
&& stages[0].get("individual_digits").and_then(|b| b.as_bool()) == Some(true)
&& ty(&stages[1]) == "ByteLevel"
&& stages[1].get("use_regex").and_then(|b| b.as_bool()) == Some(true)
{
return Ok(PretokScheme::SmolLm2);
}
if stages.len() == 4
&& ty(&stages[0]) == "Split"
&& stages[0].pointer("/pattern/Regex").and_then(|r| r.as_str()) == Some("\\p{N}{1,3}")
&& ty(&stages[3]) == "ByteLevel"
&& stages[3].get("use_regex").and_then(|b| b.as_bool()) == Some(false)
{
return Ok(PretokScheme::DeepSeekV2);
}
Err(FocrError::FormatMismatch(format!(
"unsupported tokenizer.json pre_tokenizer sequence (neither the \
DeepSeek-V2 4-stage Split nor the SmolLM2 Digits+ByteLevel shape): {v}"
)))
}
#[derive(Debug)]
pub struct Tokenizer {
vocab: HashMap<String, u32>,
id_to_token: HashMap<u32, String>,
merge_ranks: HashMap<(String, String), u32>,
added: Vec<AddedToken>,
added_by_content: HashMap<String, u32>,
special_ids: std::collections::HashSet<u32>,
scheme: PretokScheme,
}
#[derive(Debug, Clone)]
struct AddedToken {
content: String,
id: u32,
#[allow(dead_code)]
special: bool,
}
impl Tokenizer {
pub fn from_file(path: &Path) -> FocrResult<Self> {
let bytes = std::fs::read(path).map_err(|e| {
FocrError::ModelNotFound(format!("tokenizer.json at {}: {e}", path.display()))
})?;
Self::from_json_bytes(&bytes)
}
pub fn load(path: &Path) -> FocrResult<Self> {
Self::from_file(path)
}
pub fn from_json_bytes(bytes: &[u8]) -> FocrResult<Self> {
let raw: RawTokenizer = serde_json::from_slice(bytes)
.map_err(|e| FocrError::FormatMismatch(format!("tokenizer.json parse: {e}")))?;
let scheme = classify_pretok(raw.pre_tokenizer.as_ref())?;
let vocab = raw.model.vocab;
let mut merge_ranks = HashMap::with_capacity(raw.model.merges.len());
for (rank, m) in raw.model.merges.into_iter().enumerate() {
if let Some(pair) = m.into_pair() {
merge_ranks.entry(pair).or_insert(rank as u32);
}
}
let mut id_to_token: HashMap<u32, String> = HashMap::with_capacity(vocab.len());
for (tok, &id) in &vocab {
id_to_token.insert(id, tok.clone());
}
let mut added = Vec::with_capacity(raw.added_tokens.len());
let mut added_by_content = HashMap::with_capacity(raw.added_tokens.len());
let mut special_ids = std::collections::HashSet::new();
for at in raw.added_tokens {
id_to_token.insert(at.id, at.content.clone());
added_by_content.insert(at.content.clone(), at.id);
if at.special {
special_ids.insert(at.id);
}
added.push(AddedToken {
content: at.content,
id: at.id,
special: at.special,
});
}
added.sort_by(|a, b| b.content.len().cmp(&a.content.len()).then(a.id.cmp(&b.id)));
let tk = Tokenizer {
vocab,
id_to_token,
merge_ranks,
added,
added_by_content,
special_ids,
scheme,
};
tk.validate_pinned_ids()?;
Ok(tk)
}
pub fn from_opt_dir(dir: &Path) -> FocrResult<Self> {
let read = |name: &str| -> FocrResult<Vec<u8>> {
std::fs::read(dir.join(name)).map_err(|e| {
FocrError::ModelNotFound(format!("{} at {}: {e}", name, dir.display()))
})
};
Self::from_opt_files(
&read("vocab.json")?,
&read("merges.txt")?,
&read("added_tokens.json")?,
)
}
pub fn from_opt_files(
vocab_json: &[u8],
merges_txt: &[u8],
added_tokens_json: &[u8],
) -> FocrResult<Self> {
let vocab: HashMap<String, u32> = serde_json::from_slice(vocab_json)
.map_err(|e| FocrError::FormatMismatch(format!("vocab.json parse: {e}")))?;
let merges_text = std::str::from_utf8(merges_txt)
.map_err(|e| FocrError::FormatMismatch(format!("merges.txt not UTF-8: {e}")))?;
let mut merge_ranks = HashMap::new();
let mut rank = 0u32;
for line in merges_text.lines() {
let line = line.trim_end();
if line.is_empty() || line.starts_with("#version") {
continue;
}
let Some((l, r)) = line.split_once(' ') else {
return Err(FocrError::FormatMismatch(format!(
"merges.txt line without a space: {line:?}"
)));
};
merge_ranks
.entry((l.to_string(), r.to_string()))
.or_insert(rank);
rank += 1;
}
let added_map: HashMap<String, u32> = serde_json::from_slice(added_tokens_json)
.map_err(|e| FocrError::FormatMismatch(format!("added_tokens.json parse: {e}")))?;
let mut id_to_token: HashMap<u32, String> = HashMap::with_capacity(vocab.len());
for (tok, &id) in &vocab {
id_to_token.insert(id, tok.clone());
}
let mut added = Vec::with_capacity(added_map.len());
let mut added_by_content = HashMap::with_capacity(added_map.len());
let mut special_ids = std::collections::HashSet::new();
for (content, id) in added_map {
id_to_token.insert(id, content.clone());
added_by_content.insert(content.clone(), id);
special_ids.insert(id);
added.push(AddedToken {
content,
id,
special: true,
});
}
for surface in ["<s>", "<pad>", "</s>", "<unk>"] {
if let Some(&id) = vocab.get(surface)
&& !added_by_content.contains_key(surface)
{
added_by_content.insert(surface.to_string(), id);
special_ids.insert(id);
added.push(AddedToken {
content: surface.to_string(),
id,
special: true,
});
}
}
added.sort_by(|a, b| b.content.len().cmp(&a.content.len()).then(a.id.cmp(&b.id)));
let tk = Tokenizer {
vocab,
id_to_token,
merge_ranks,
added,
added_by_content,
special_ids,
scheme: PretokScheme::Gpt2,
};
tk.validate_pinned_ids()?;
Ok(tk)
}
fn validate_pinned_ids(&self) -> FocrResult<()> {
let checks: &[(&str, u32)] = match self.scheme {
PretokScheme::DeepSeekV2 => &[
("<image>", special::IMAGE),
("<|ref|>", special::REF),
("<|/ref|>", special::REF_END),
("<|det|>", special::DET),
("<|/det|>", special::DET_END),
("<|grounding|>", special::GROUNDING),
("<|User|>", special::USER),
("<|Assistant|>", special::ASSISTANT),
],
PretokScheme::SmolLm2 => &[
("<|endoftext|>", special_smollm2::UNK),
("<|im_start|>", special_smollm2::BOS),
("<|im_end|>", special_smollm2::PAD),
("<global-img>", special_smollm2::GLOBAL_IMG),
(
"<fake_token_around_image>",
special_smollm2::FAKE_AROUND_IMAGE,
),
("<image>", special_smollm2::IMAGE),
("<end_of_utterance>", special_smollm2::END_OF_UTTERANCE),
],
PretokScheme::Gpt2 => &[
("<imgpad>", special_opt::IMG_PAD),
("<img>", special_opt::IMG_START),
("</img>", special_opt::IMG_END),
("<Number>", special_opt::NUMBER),
],
};
for &(content, want) in checks {
if let Some(&got) = self.added_by_content.get(content)
&& got != want
{
return Err(FocrError::FormatMismatch(format!(
"tokenizer.json id mismatch for {content}: file says {got}, expected {want}"
)));
}
}
Ok(())
}
pub fn encode(&self, text: &str) -> FocrResult<Vec<u32>> {
let mut ids = Vec::new();
for segment in self.split_on_added(text) {
match segment {
Segment::Added(id) => ids.push(id),
Segment::Text(s) => self.encode_text_segment(s, &mut ids)?,
}
}
Ok(ids)
}
fn encode_text_segment(&self, text: &str, out: &mut Vec<u32>) -> FocrResult<()> {
let pieces = match self.scheme {
PretokScheme::DeepSeekV2 => pretok::pretokenize(text),
PretokScheme::SmolLm2 => pretok::pretokenize_smollm2(text),
PretokScheme::Gpt2 => pretok::pretokenize_gpt2(text),
};
for piece in pieces {
let symbols = self.bpe(&piece);
for sym in symbols {
let id = self.vocab.get(&sym).copied().ok_or_else(|| {
FocrError::FormatMismatch(format!(
"byte-level symbol {sym:?} missing from vocab (corrupt tokenizer.json)"
))
})?;
out.push(id);
}
}
Ok(())
}
fn bpe(&self, piece: &str) -> Vec<String> {
let mut symbols: Vec<String> = piece.chars().map(|c| c.to_string()).collect();
if symbols.len() < 2 {
return symbols;
}
loop {
let mut best: Option<(usize, u32)> = None;
for i in 0..symbols.len() - 1 {
let pair = (symbols[i].clone(), symbols[i + 1].clone());
if let Some(&rank) = self.merge_ranks.get(&pair) {
match best {
Some((_, br)) if rank >= br => {}
_ => best = Some((i, rank)),
}
}
}
let Some((i, _)) = best else { break };
let merged = format!("{}{}", symbols[i], symbols[i + 1]);
symbols.splice(i..=i + 1, std::iter::once(merged));
if symbols.len() < 2 {
break;
}
}
symbols
}
fn split_on_added<'a>(&self, text: &'a str) -> Vec<Segment<'a>> {
if self.added.is_empty() {
return vec![Segment::Text(text)];
}
let bytes = text.as_bytes();
let mut out = Vec::new();
let mut run_start = 0usize; let mut i = 0usize;
while i < bytes.len() {
let mut matched = None;
for at in &self.added {
let c = at.content.as_bytes();
if !c.is_empty() && bytes[i..].starts_with(c) {
matched = Some((c.len(), at.id));
break; }
}
if let Some((len, id)) = matched {
if run_start < i {
out.push(Segment::Text(&text[run_start..i]));
}
out.push(Segment::Added(id));
i += len;
run_start = i;
} else {
let ch_len = utf8_char_len(bytes[i]);
i += ch_len;
}
}
if run_start < text.len() {
out.push(Segment::Text(&text[run_start..]));
}
out
}
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 bytes: Vec<u8> = Vec::new();
for &id in ids {
if skip_special && self.special_ids.contains(&id) {
continue;
}
self.token_bytes(id, &mut bytes)?;
}
String::from_utf8(bytes)
.map_err(|e| FocrError::FormatMismatch(format!("decode: invalid UTF-8: {e}")))
}
fn token_bytes(&self, id: u32, out: &mut Vec<u8>) -> FocrResult<bool> {
let tok = self
.id_to_token
.get(&id)
.ok_or_else(|| FocrError::FormatMismatch(format!("decode: unknown token id {id}")))?;
if self.added_by_content.contains_key(tok) {
out.extend_from_slice(tok.as_bytes());
return Ok(true);
}
let start = out.len();
for c in tok.chars() {
let Some(b) = pretok::char_to_byte(c) else {
out.truncate(start);
return Err(FocrError::FormatMismatch(format!(
"decode: token id {id} has non-byte-level symbol {c:?}"
)));
};
out.push(b);
}
Ok(false)
}
pub fn token_to_id(&self, content: &str) -> Option<u32> {
self.added_by_content
.get(content)
.copied()
.or_else(|| self.vocab.get(content).copied())
}
pub fn id_to_token(&self, id: u32) -> Option<&str> {
self.id_to_token.get(&id).map(String::as_str)
}
pub fn vocab_size(&self) -> usize {
self.vocab.len()
}
pub fn scheme(&self) -> PretokScheme {
self.scheme
}
pub fn bos_id(&self) -> u32 {
match self.scheme {
PretokScheme::DeepSeekV2 => special::BOS,
PretokScheme::SmolLm2 => special_smollm2::BOS,
PretokScheme::Gpt2 => special_opt::BOS_EOS,
}
}
pub fn eos_id(&self) -> u32 {
match self.scheme {
PretokScheme::DeepSeekV2 => special::EOS,
PretokScheme::SmolLm2 => special_smollm2::END_OF_UTTERANCE,
PretokScheme::Gpt2 => special_opt::BOS_EOS,
}
}
pub fn pad_id(&self) -> u32 {
match self.scheme {
PretokScheme::DeepSeekV2 => special::PAD,
PretokScheme::SmolLm2 => special_smollm2::PAD,
PretokScheme::Gpt2 => special_opt::PAD,
}
}
pub fn image_id(&self) -> u32 {
match self.scheme {
PretokScheme::DeepSeekV2 => special::IMAGE,
PretokScheme::SmolLm2 => special_smollm2::IMAGE,
PretokScheme::Gpt2 => special_opt::IMG_PAD,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct StreamDecoder {
pending: Vec<u8>,
}
impl StreamDecoder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, tok: &Tokenizer, id: u32, out: &mut String) -> FocrResult<()> {
let start = self.pending.len();
if tok.token_bytes(id, &mut self.pending)? {
let literal = self.pending.split_off(start);
self.flush(out);
out.push_str(&String::from_utf8_lossy(&literal));
} else {
self.emit_complete(out);
}
Ok(())
}
pub fn flush(&mut self, out: &mut String) {
out.push_str(&String::from_utf8_lossy(&self.pending));
self.pending.clear();
}
fn emit_complete(&mut self, out: &mut String) {
let mut at = 0;
while at < self.pending.len() {
match std::str::from_utf8(&self.pending[at..]) {
Ok(text) => {
out.push_str(text);
at = self.pending.len();
}
Err(e) => {
let valid = at + e.valid_up_to();
out.push_str(&String::from_utf8_lossy(&self.pending[at..valid]));
let Some(bad) = e.error_len() else {
at = valid;
break;
};
out.push(char::REPLACEMENT_CHARACTER);
at = valid + bad;
}
}
}
self.pending.drain(..at);
}
}
enum Segment<'a> {
Text(&'a str),
Added(u32),
}
pub(super) fn utf8_char_len(lead: u8) -> usize {
if lead < 0x80 {
1
} else if lead >> 5 == 0b110 {
2
} else if lead >> 4 == 0b1110 {
3
} else if lead >> 3 == 0b11110 {
4
} else {
1 }
}
#[allow(dead_code)]
const UNICODE_TABLE_REGEN: () = ();
#[cfg(test)]
pub(crate) mod tests {
use super::*;
pub(super) fn tiny_json() -> String {
r#"{
"version": "1.0",
"added_tokens": [
{"id": 128815, "content": "<image>", "special": true},
{"id": 100, "content": "<|x|>", "special": false}
],
"normalizer": {"type":"Sequence","normalizers":[]},
"model": {
"type": "BPE",
"vocab": {
"a": 0, "b": 1, "c": 2, "d": 3, "e": 4, "f": 5,
"Ġ": 6, "ab": 7, "abc": 8, "Ġa": 9, "Ġd": 10
},
"merges": [
["a", "b"],
["ab", "c"],
["Ġ", "a"],
["Ġ", "d"]
]
}
}"#
.to_string()
}
fn tk() -> Tokenizer {
Tokenizer::from_json_bytes(tiny_json().as_bytes()).expect("tiny tokenizer loads")
}
#[test]
fn loads_and_validates_pinned_ids() {
let t = tk();
assert_eq!(t.token_to_id("<image>"), Some(special::IMAGE));
assert_eq!(t.image_id(), 128815);
assert_eq!(t.bos_id(), 0);
assert_eq!(t.eos_id(), 1);
assert_eq!(t.vocab_size(), 11);
}
#[test]
fn pinned_id_mismatch_is_format_error() {
let bad = tiny_json().replace("\"id\": 128815", "\"id\": 999");
let err = Tokenizer::from_json_bytes(bad.as_bytes()).unwrap_err();
assert!(matches!(err, FocrError::FormatMismatch(_)), "got {err:?}");
}
#[test]
fn bpe_merge_ordering() {
let t = tk();
assert_eq!(t.encode("abc").unwrap(), vec![8]);
assert_eq!(t.encode("ab").unwrap(), vec![7]);
assert_eq!(t.encode("ba").unwrap(), vec![1, 0]);
}
#[test]
fn merge_priority_is_rank_not_position() {
let t = tk();
assert_eq!(t.encode(" a").unwrap(), vec![9]);
}
#[test]
fn special_token_splitting() {
let t = tk();
assert_eq!(t.encode("ab<image>c").unwrap(), vec![7, 128815, 2]);
assert_eq!(t.encode("a<|x|>b").unwrap(), vec![0, 100, 1]);
let ids = t.encode("<image>").unwrap();
assert_eq!(ids, vec![128815]);
}
#[test]
fn round_trip_encode_decode() {
let t = tk();
let ids = t.encode("abc").unwrap();
assert_eq!(t.decode(&ids).unwrap(), "abc");
let ids2 = t.encode(" a").unwrap();
assert_eq!(t.decode(&ids2).unwrap(), " a");
let ids3 = t.encode("ab<image>c").unwrap();
assert_eq!(t.decode(&ids3).unwrap(), "ab<image>c");
}
#[test]
fn decode_skip_special_drops_specials() {
let t = tk();
let ids = t.encode("ab<image>c").unwrap();
assert_eq!(t.decode_skip_special(&ids).unwrap(), "abc");
let ids2 = t.encode("a<|x|>b").unwrap();
assert_eq!(t.decode_skip_special(&ids2).unwrap(), "a<|x|>b");
}
#[test]
fn byte_level_non_ascii_round_trips_through_bytes() {
let json = r#"{
"added_tokens": [],
"model": {"type":"BPE",
"vocab": {"Ã": 0, "©": 1},
"merges": []
}
}"#;
let t = Tokenizer::from_json_bytes(json.as_bytes()).unwrap();
let ids = t.encode("é").unwrap(); assert_eq!(ids, vec![0, 1]);
assert_eq!(t.decode(&ids).unwrap(), "é");
}
#[test]
fn legacy_joined_merge_format_is_accepted() {
let json = r#"{
"added_tokens": [],
"model": {"type":"BPE",
"vocab": {"a":0,"b":1,"ab":2},
"merges": ["a b"]
}
}"#;
let t = Tokenizer::from_json_bytes(json.as_bytes()).unwrap();
assert_eq!(t.encode("ab").unwrap(), vec![2]);
}
#[test]
fn decode_unknown_id_errors() {
let t = tk();
let err = t.decode(&[424242]).unwrap_err();
assert!(matches!(err, FocrError::FormatMismatch(_)), "got {err:?}");
}
#[test]
fn from_file_missing_is_model_not_found() {
let err = Tokenizer::from_file(Path::new("/nonexistent/tokenizer.json")).unwrap_err();
assert!(matches!(err, FocrError::ModelNotFound(_)), "got {err:?}");
}
#[test]
fn empty_input_encodes_empty() {
let t = tk();
assert_eq!(t.encode("").unwrap(), Vec::<u32>::new());
assert_eq!(t.decode(&[]).unwrap(), "");
}
pub(super) fn load_real() -> Option<Tokenizer> {
let path = std::env::var("FOCR_TOKENIZER_JSON").unwrap_or_else(|_| {
concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/truth-pack/snapshots/tokenizer.json"
)
.to_string()
});
let path = Path::new(&path);
if !path.is_file() {
eprintln!(
"SKIP baidu tokenizer conformance: {} absent \
(scripts/fetch_sources.sh or FOCR_TOKENIZER_JSON)",
path.display()
);
return None;
}
Some(Tokenizer::from_file(path).expect("pinned tokenizer.json must load"))
}
#[test]
fn real_vocab_anchors() {
let Some(t) = load_real() else {
return;
};
assert_eq!(t.vocab_size(), 128000);
assert_eq!(
t.encode("<|begin▁of▁sentence|>").unwrap(),
vec![special::BOS]
);
assert_eq!(
t.encode("<|end▁of▁sentence|>").unwrap(),
vec![special::EOS]
);
assert_eq!(t.encode("<|▁pad▁|>").unwrap(), vec![special::PAD]);
assert_eq!(t.encode("<image>").unwrap(), vec![special::IMAGE]);
assert_eq!(t.encode("<|grounding|>").unwrap(), vec![special::GROUNDING]);
assert_eq!(t.encode("<|User|>").unwrap(), vec![special::USER]);
assert_eq!(t.encode("<|Assistant|>").unwrap(), vec![special::ASSISTANT]);
assert_ne!(t.encode("<|User|>").unwrap(), vec![special::USER]);
}
fn fixture_mismatches(t: &Tokenizer, cases: &[serde_json::Value]) -> usize {
let mut mismatches = 0usize;
for rec in cases {
let text = rec["text"].as_str().unwrap();
let want: Vec<u32> = rec["ids"]
.as_array()
.unwrap()
.iter()
.map(|x| x.as_u64().unwrap() as u32)
.collect();
let got = t.encode(text).unwrap();
if got != want {
let pos = got
.iter()
.zip(&want)
.position(|(a, b)| a != b)
.unwrap_or_else(|| got.len().min(want.len()));
eprintln!(
"ENC MISMATCH {{\"case\": {text:?}, \"len\": {}, \"mismatch_pos\": {pos}}}\n \
got {got:?}\n want {want:?}",
want.len()
);
mismatches += 1;
}
let want_decoded = rec["decoded"].as_str().unwrap();
let got_decoded = t.decode(&want).unwrap();
if got_decoded != want_decoded {
eprintln!(
"DEC MISMATCH {{\"case\": {text:?}, \"mismatch_pos\": \"none\"}}\n \
got {got_decoded:?}\n want {want_decoded:?}"
);
mismatches += 1;
}
if let Some(want_skipped) = rec["decoded_skip_special"].as_str() {
let got_skipped = t.decode_skip_special(&want).unwrap();
if got_skipped != want_skipped {
eprintln!(
"SKIP-SPECIAL DEC MISMATCH {{\"case\": {text:?}}}\n \
got {got_skipped:?}\n want {want_skipped:?}"
);
mismatches += 1;
}
}
}
mismatches
}
#[test]
fn baidu_token_id_conformance_gate() {
let Some(t) = load_real() else {
return;
};
const EXPECTED: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/tokenizer_baidu/expected.json"
));
let v: serde_json::Value = serde_json::from_str(EXPECTED).unwrap();
assert_eq!(
v["_meta"]["tokenizer_json_sha256"].as_str().unwrap(),
"a02f8fd5228c90256bb4f6554c34a579d48f909e5beb232dc4afad870b55a8b4",
"fixture was generated against a different tokenizer.json pin"
);
let cases = v["fixtures"].as_array().expect("fixtures array");
let num_cases = v["_meta"]["num_cases"].as_u64().unwrap() as usize;
assert_eq!(cases.len(), num_cases, "fixture _meta.num_cases drift");
assert!(
cases.len() >= 100,
"conformance corpus must stay >= 100 cases"
);
let mismatches = fixture_mismatches(&t, cases);
assert_eq!(
mismatches, 0,
"tok_id_mismatch_count must be 0 (got {mismatches})"
);
}
const PAGE: u32 = special::PAGE;
const MULTI: [(u32, &[u8]); 6] = [
(2000, "中".as_bytes()),
(2001, &[0xE4, 0xB8]),
(2002, &[0xAD, b'a']),
(2003, &[0xF0, 0x9F]),
(2004, &[0xA6, 0x80]),
(2005, "é".as_bytes()),
];
fn byte_symbols() -> [char; 256] {
let mut sym = ['\0'; 256];
for c in (0..0x144).filter_map(char::from_u32) {
if let Some(b) = pretok::char_to_byte(c) {
sym[b as usize] = c;
}
}
sym
}
pub(crate) fn byte_id(b: u8) -> u32 {
1000 + u32::from(b)
}
pub(crate) fn byte_tokenizer() -> Tokenizer {
byte_tokenizer_with(&[(special::IMAGE, "<image>", true), (PAGE, "<PAGE>", false)])
}
fn byte_tokenizer_with(added: &[(u32, &str, bool)]) -> Tokenizer {
let sym = byte_symbols();
let spell = |bytes: &[u8]| bytes.iter().map(|&b| sym[b as usize]).collect::<String>();
let mut vocab = serde_json::Map::new();
for b in 0..=255u8 {
vocab.insert(spell(&[b]), byte_id(b).into());
}
for (id, bytes) in MULTI {
vocab.insert(spell(bytes), id.into());
}
let added: Vec<serde_json::Value> = added
.iter()
.map(|&(id, content, special)| {
serde_json::json!({"id": id, "content": content, "special": special})
})
.collect();
let json = serde_json::json!({
"version": "1.0",
"added_tokens": added,
"normalizer": {"type": "Sequence", "normalizers": []},
"model": {"type": "BPE", "vocab": vocab, "merges": []}
});
Tokenizer::from_json_bytes(json.to_string().as_bytes()).expect("byte tokenizer loads")
}
fn byte_ids(text: &str) -> Vec<u32> {
text.bytes().map(byte_id).collect()
}
fn lossy_segments(t: &Tokenizer, ids: &[u32]) -> String {
let (mut out, mut seg) = (String::new(), Vec::new());
for &id in ids {
let piece = t.id_to_token(id).expect("known id");
if t.added_by_content.contains_key(piece) {
out.push_str(&String::from_utf8_lossy(&seg));
seg.clear();
out.push_str(piece);
} else {
seg.extend(piece.chars().map(|c| pretok::char_to_byte(c).unwrap()));
}
}
out.push_str(&String::from_utf8_lossy(&seg));
out
}
fn stream(t: &Tokenizer, ids: &[u32]) -> (Vec<String>, String) {
let mut d = StreamDecoder::new();
let emits = ids
.iter()
.map(|&id| {
let mut out = String::new();
d.push(t, id, &mut out).unwrap();
out
})
.collect();
let mut tail = String::new();
d.flush(&mut tail);
(emits, tail)
}
#[test]
fn stream_decoder_emits_split_characters_once_complete() {
let t = byte_tokenizer();
let cases: [(&str, Vec<u32>, &[&str]); 6] = [
("中 as 3 byte tokens", byte_ids("中"), &["", "", "中"]),
("🦀 as 4 byte tokens", byte_ids("🦀"), &["", "", "", "🦀"]),
("🦀 as 2 halves", vec![2003, 2004], &["", "🦀"]),
("a token across a boundary", vec![2001, 2002], &["", "中a"]),
(
"whole tokens",
vec![2000, 2005, byte_id(b'x')],
&["中", "é", "x"],
),
(
"split, then ASCII, then split",
[
byte_ids("é")[..1].to_vec(),
vec![byte_id(0xA9), byte_id(b'!')],
byte_ids("中"),
]
.concat(),
&["", "é", "!", "", "", "中"],
),
];
for (what, ids, want) in cases {
let (emits, tail) = stream(&t, &ids);
assert_eq!(emits, want, "{what}");
assert_eq!(tail, "", "{what}");
assert_eq!(emits.concat(), t.decode(&ids).unwrap(), "{what}");
}
}
#[test]
fn stream_decoder_ends_a_segment_at_an_added_token() {
let t = byte_tokenizer();
let ids = [byte_id(0xE4), PAGE, byte_id(b'a'), 2000];
let (emits, tail) = stream(&t, &ids);
assert_eq!(emits, ["", "\u{FFFD}<PAGE>", "a", "中"]);
assert_eq!(tail, "");
assert_eq!(emits.concat(), lossy_segments(&t, &ids));
assert!(t.decode(&ids).is_err(), "the strict decode rejects it");
let ids = [byte_id(0xFF), byte_id(b'a'), byte_id(0xE4), byte_id(b'b')];
let (emits, _) = stream(&t, &ids);
assert_eq!(emits, ["\u{FFFD}", "a", "", "\u{FFFD}b"]);
let (emits, tail) = stream(&t, &[byte_id(b'c'), 2003]);
assert_eq!(
(emits.concat(), tail.as_str()),
("c".to_string(), "\u{FFFD}")
);
let mut d = StreamDecoder::new();
let mut out = String::new();
d.push(&t, byte_id(0xE4), &mut out).unwrap();
assert!(d.push(&t, 7, &mut out).is_err());
d.push(&t, byte_id(0xB8), &mut out).unwrap();
d.push(&t, byte_id(0xAD), &mut out).unwrap();
assert_eq!(out, "中");
}
#[test]
fn stream_decoder_equals_the_per_segment_lossy_decode() {
let t = byte_tokenizer();
let mut state = 0x5eed_0a11_u64;
let mut next = |n: u64| {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
(state >> 33) % n
};
let mut checked_invalid = 0;
for _ in 0..3000 {
let len = 1 + next(24) as usize;
let ids: Vec<u32> = (0..len)
.map(|_| match next(10) {
0 => PAGE,
1 => special::IMAGE,
2 | 3 => MULTI[next(MULTI.len() as u64) as usize].0,
4 => byte_id(0xC0 + next(0x38) as u8),
5 | 6 => byte_id(0x80 + next(0x40) as u8),
_ => byte_id(b'a' + next(26) as u8),
})
.collect();
let full = lossy_segments(&t, &ids);
let (emits, tail) = stream(&t, &ids);
assert_eq!(format!("{}{tail}", emits.concat()), full, "{ids:?}");
let mut d = StreamDecoder::new();
let mut so_far = String::new();
for (i, &id) in ids.iter().enumerate() {
d.push(&t, id, &mut so_far).unwrap();
assert!(full.starts_with(&so_far), "{ids:?} at {i}");
let mut flushed = so_far.clone();
d.clone().flush(&mut flushed);
assert_eq!(flushed, lossy_segments(&t, &ids[..=i]), "{ids:?} at {i}");
}
match t.decode(&ids) {
Ok(strict) => assert_eq!(strict, full, "{ids:?}"),
Err(_) => checked_invalid += 1,
}
}
assert!(
checked_invalid > 100,
"too few invalid sequences: {checked_invalid}"
);
}
#[test]
fn stream_decoder_reassembles_real_cjk_splits() {
let Some(t) = load_real() else {
return;
};
let text = "<PAGE>\n第一页:饕餮盛宴\n魑魅魍魉,犇骉鱻羴。\n<PAGE>\n第二页:垚与赑屃 🦀";
let ids = t.encode(text).unwrap();
let (emits, tail) = stream(&t, &ids);
assert_eq!(emits.concat(), text);
assert_eq!(tail, "");
let split_prefixes = (1..ids.len())
.filter(|&n| t.decode(&ids[..n]).is_err())
.count();
assert!(split_prefixes > 0, "no character was split across tokens");
}
fn load_real_uocr2() -> Option<Tokenizer> {
use sha2::Digest as _;
let path = std::env::var_os("FOCR_UOCR2_TOKENIZER_JSON")
.map(std::path::PathBuf::from)
.or_else(|| {
std::env::var_os("FOCR_UOCR2_SOURCE_DIR")
.map(|dir| std::path::PathBuf::from(dir).join("tokenizer.json"))
});
let Some(path) = path else {
eprintln!(
"SKIP uocr2 tokenizer conformance: set FOCR_UOCR2_TOKENIZER_JSON \
(or FOCR_UOCR2_SOURCE_DIR) to the pinned v2 tokenizer.json"
);
return None;
};
let bytes =
std::fs::read(&path).unwrap_or_else(|error| panic!("read {}: {error}", path.display()));
assert_eq!(
format!("{:x}", sha2::Sha256::digest(&bytes)),
UNLIMITED_OCR_V2_TOKENIZER_SHA256,
"{} is not the pinned Unlimited-OCR-2 tokenizer.json",
path.display()
);
Some(Tokenizer::from_json_bytes(&bytes).expect("the pinned v2 tokenizer.json loads"))
}
use crate::native_engine::model_arch::{
UNLIMITED_OCR_V2_TOKENIZER_SHA256, UNLIMITED_OCR_V2_TOKENS,
};
fn ids_of(record: &serde_json::Value) -> Vec<u32> {
record["ids"]
.as_array()
.expect("ids array")
.iter()
.map(|id| u32::try_from(id.as_u64().expect("id")).expect("u32 id"))
.collect()
}
#[test]
fn uocr2_token_id_conformance_gate() {
const EXPECTED: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/tokenizer_baidu/expected_uocr2.json"
));
let started = std::time::Instant::now();
let v: serde_json::Value = serde_json::from_str(EXPECTED).unwrap();
let meta = &v["_meta"];
assert_eq!(
meta["tokenizer_json_sha256"],
UNLIMITED_OCR_V2_TOKENIZER_SHA256
);
assert_eq!(meta["model_id"], "unlimited-ocr-2");
assert_eq!(meta["tokenizers_version"], "0.21.4");
assert_eq!(meta["max_token_id"], special::CHILD);
let cases = v["fixtures"].as_array().expect("fixtures array");
assert_eq!(Some(cases.len() as u64), meta["num_cases"].as_u64());
for pages in [1, 3, 20] {
let prompt = format!("{}Multi page parsing.", "<PAGE><image>".repeat(pages));
let record = cases
.iter()
.find(|record| record["text"] == prompt.as_str())
.unwrap_or_else(|| panic!("the {pages}-page prompt is in the golden"));
assert_eq!(
ids_of(record)[..2 * pages],
[special::PAGE, special::IMAGE].repeat(pages)[..]
);
}
let Some(t) = load_real_uocr2() else {
return;
};
let mismatches = fixture_mismatches(&t, cases);
assert_eq!(
mismatches, 0,
"tok_id_mismatch_count must be 0 (got {mismatches})"
);
for (content, id) in meta["anchor_ids"].as_object().expect("anchors") {
let id = u32::try_from(id.as_u64().expect("anchor id")).expect("u32 id");
assert_eq!(t.encode(content).unwrap(), vec![id], "{content}");
}
for (content, id) in meta["non_special_added_ids"]
.as_object()
.expect("v2 tokens")
{
let id = u32::try_from(id.as_u64().expect("v2 id")).expect("u32 id");
assert_eq!(t.decode_skip_special(&[id]).unwrap(), *content);
}
for &(id, content) in UNLIMITED_OCR_V2_TOKENS {
assert_eq!(t.token_to_id(content), Some(id), "{content}");
assert_eq!(t.id_to_token(id), Some(content));
}
assert_eq!(t.vocab_size(), 128_000);
assert_eq!(t.id_to_token(special::CHILD), Some("<CHILD>"));
assert_eq!(
t.id_to_token(special::CHILD + 1),
None,
"128830 is the highest id"
);
assert!(t.decode(&[special::CHILD + 1]).is_err());
println!(
"{}",
serde_json::json!({
"schema_version": 1, "ts": started.elapsed().as_secs_f64(),
"test": "uocr2_token_id_conformance_gate", "case": "expected_uocr2",
"run_seq": 0, "event": "result", "result": "pass",
"elapsed_us": started.elapsed().as_micros() as u64,
"cases": cases.len(), "tok_id_mismatch_count": mismatches,
})
);
}
#[test]
fn v1_tokenizer_has_no_page_token() {
let Some(t) = load_real() else {
return;
};
assert_eq!(t.token_to_id("<PAGE>"), None);
assert!(!t.encode("<PAGE>").unwrap().contains(&special::PAGE));
assert_eq!(t.id_to_token(special::PAGE), None);
}
struct SplitMix64(u64);
impl SplitMix64 {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n as u64) as usize
}
}
const UOCR2_FUZZ_TOKENS: [(&str, u32); 5] = [
("<PAGE>", special::PAGE),
("<BLOCK>", special::BLOCK),
("<FORK>", special::FORK),
("<CHILD>", special::CHILD),
("<image>", special::IMAGE),
];
const UOCR2_NEAR_MISSES: [&str; 12] = [
"<PAGE", "PAGE>", "<page>", "< PAGE>", "<PAGE >", "<PAG", "E>", "<BLOC", "K>", "<FOR",
"<CHIL", "D>",
];
const UOCR2_FUZZ_CHARS: [char; 17] = [
'a', 'Z', '1', '9', ' ', '\n', '\t', '中', '文', 'é', '🦀', '<', '>', 'P', 'A', 'G', 'E',
];
fn uocr2_fuzz_string(rng: &mut SplitMix64) -> String {
let mut text = String::new();
for _ in 0..1 + rng.below(12) {
match rng.below(3) {
0 => text.push_str(UOCR2_FUZZ_TOKENS[rng.below(UOCR2_FUZZ_TOKENS.len())].0),
1 => text.push_str(UOCR2_NEAR_MISSES[rng.below(UOCR2_NEAR_MISSES.len())]),
_ => {
for _ in 0..1 + rng.below(6) {
text.push(UOCR2_FUZZ_CHARS[rng.below(UOCR2_FUZZ_CHARS.len())]);
}
}
}
}
text
}
fn check_added_token_properties(t: &Tokenizer, text: &str) -> Result<(), String> {
let ids = t.encode(text).map_err(|error| format!("encode: {error}"))?;
let decoded = t.decode(&ids).map_err(|error| format!("decode: {error}"))?;
if decoded != text {
return Err(format!("decode(encode) is {decoded:?}"));
}
for (content, id) in UOCR2_FUZZ_TOKENS {
let want = text.matches(content).count();
let got = ids.iter().filter(|&&x| x == id).count();
if got != want {
return Err(format!(
"{content}: {got} ids for {want} occurrences in {ids:?}"
));
}
}
let skipped = t
.decode_skip_special(&ids)
.map_err(|error| format!("skip: {error}"))?;
if skipped != text.replace("<image>", "") {
return Err(format!("skip-special decode is {skipped:?}"));
}
let mut joined = String::new();
for segment in t.split_on_added(text) {
match segment {
Segment::Text(run) => {
if !matches!(t.split_on_added(run)[..], [Segment::Text(again)] if again == run)
{
return Err(format!("text segment {run:?} splits again"));
}
joined.push_str(run);
}
Segment::Added(id) => {
let content = t.id_to_token(id).ok_or("unknown added id")?;
if !matches!(t.split_on_added(content)[..], [Segment::Added(again)] if again == id)
{
return Err(format!("added token {content:?} does not split to itself"));
}
joined.push_str(content);
}
}
}
if joined != text {
return Err(format!("segments rejoin to {joined:?}"));
}
Ok(())
}
fn run_added_token_fuzz(t: &Tokenizer, test: &str, seed: u64, iterations: usize) {
let started = std::time::Instant::now();
let mut rng = SplitMix64(seed);
for iteration in 0..iterations {
let text = uocr2_fuzz_string(&mut rng);
if let Err(why) = check_added_token_properties(t, &text) {
panic!("{test}: seed {seed:#x} iteration {iteration}: {text:?}: {why}");
}
}
println!(
"{}",
serde_json::json!({
"schema_version": 1, "ts": started.elapsed().as_secs_f64(), "test": test,
"case": "added_token_fuzz", "run_seq": 0, "event": "result", "result": "pass",
"elapsed_us": started.elapsed().as_micros() as u64, "seed": seed,
"iterations": iterations,
})
);
}
#[test]
fn uocr2_added_tokens_stay_whole_under_fuzz() {
let t = byte_tokenizer_with(&[
(special::IMAGE, "<image>", true),
(special::PAGE, "<PAGE>", false),
(special::BLOCK, "<BLOCK>", false),
(special::FORK, "<FORK>", false),
(special::CHILD, "<CHILD>", false),
]);
assert_eq!(t.encode("<PAGE>").unwrap(), vec![special::PAGE]);
assert_eq!(t.decode_skip_special(&[special::PAGE]).unwrap(), "<PAGE>");
run_added_token_fuzz(
&t,
"uocr2_added_tokens_stay_whole_under_fuzz",
0x0A4_F022,
2_000,
);
}
#[test]
fn uocr2_real_tokenizer_keeps_added_tokens_whole_under_fuzz() {
let Some(t) = load_real_uocr2() else {
return;
};
run_added_token_fuzz(
&t,
"uocr2_real_tokenizer_keeps_added_tokens_whole_under_fuzz",
0x0A4_BEEF,
1_000,
);
}
fn tiny_smollm2_json() -> String {
r#"{
"version": "1.0",
"added_tokens": [
{"id": 49190, "content": "<image>", "special": true},
{"id": 49279, "content": "<end_of_utterance>", "special": true}
],
"normalizer": null,
"pre_tokenizer": {
"type": "Sequence",
"pretokenizers": [
{"type": "Digits", "individual_digits": true},
{"type": "ByteLevel", "add_prefix_space": false, "trim_offsets": true, "use_regex": true}
]
},
"post_processor": null,
"model": {
"type": "BPE",
"vocab": {
"a": 0, "b": 1, "c": 2, "1": 3, "2": 4,
"Ġ": 5, "ab": 6, "12": 7, "'": 8, "s": 9, "'s": 10
},
"merges": [
["a", "b"],
["1", "2"],
["'", "s"]
]
}
}"#
.to_string()
}
fn tk_smollm2() -> Tokenizer {
Tokenizer::from_json_bytes(tiny_smollm2_json().as_bytes()).expect("tiny smollm2 loads")
}
#[test]
fn smollm2_scheme_is_detected_from_pre_tokenizer() {
let t = tk_smollm2();
assert_eq!(t.scheme(), PretokScheme::SmolLm2);
assert_eq!(tk().scheme(), PretokScheme::DeepSeekV2);
}
#[test]
fn smollm2_scheme_ids() {
let t = tk_smollm2();
assert_eq!(t.bos_id(), special_smollm2::BOS); assert_eq!(t.eos_id(), special_smollm2::END_OF_UTTERANCE); assert_eq!(t.pad_id(), special_smollm2::PAD); assert_eq!(t.image_id(), special_smollm2::IMAGE); let b = tk();
assert_eq!(b.bos_id(), special::BOS);
assert_eq!(b.eos_id(), special::EOS);
assert_eq!(b.image_id(), special::IMAGE);
}
#[test]
fn smollm2_digits_encode_individually() {
let t = tk_smollm2();
assert_eq!(t.encode("12").unwrap(), vec![3, 4]);
let baidu_style = tiny_smollm2_json()
.replace(
r#""pre_tokenizer": {
"type": "Sequence",
"pretokenizers": [
{"type": "Digits", "individual_digits": true},
{"type": "ByteLevel", "add_prefix_space": false, "trim_offsets": true, "use_regex": true}
]
},"#,
"",
)
.replace("<image>", "<img2>");
assert!(
!baidu_style.contains("pre_tokenizer"),
"test fixture drifted: the pre_tokenizer block was not stripped"
);
let b = Tokenizer::from_json_bytes(baidu_style.as_bytes()).expect("loads");
assert_eq!(b.scheme(), PretokScheme::DeepSeekV2);
assert_eq!(b.encode("12").unwrap(), vec![7]);
}
#[test]
fn smollm2_contraction_merges() {
let t = tk_smollm2();
assert_eq!(t.encode("ab's").unwrap(), vec![6, 10]);
}
#[test]
fn smollm2_added_tokens_split() {
let t = tk_smollm2();
assert_eq!(
t.encode("ab<image>ab").unwrap(),
vec![6, special_smollm2::IMAGE, 6]
);
assert_eq!(
t.decode_skip_special(&t.encode("ab<image>ab").unwrap())
.unwrap(),
"abab"
);
}
#[test]
fn smollm2_wrong_image_pin_is_rejected() {
let bad = tiny_smollm2_json().replace("\"id\": 49190", "\"id\": 999");
let err = Tokenizer::from_json_bytes(bad.as_bytes()).unwrap_err();
assert!(matches!(err, FocrError::FormatMismatch(_)), "got {err:?}");
}
#[test]
fn unknown_pre_tokenizer_is_rejected() {
let bad = tiny_smollm2_json().replace(
r#"{"type": "Digits", "individual_digits": true}"#,
r#"{"type": "Whitespace"}"#,
);
let err = Tokenizer::from_json_bytes(bad.as_bytes()).unwrap_err();
assert!(matches!(err, FocrError::FormatMismatch(_)), "got {err:?}");
let bad2 = tiny_smollm2_json().replace(
r#"{
"type": "Sequence",
"pretokenizers": [
{"type": "Digits", "individual_digits": true},
{"type": "ByteLevel", "add_prefix_space": false, "trim_offsets": true, "use_regex": true}
]
}"#,
r#"{"type": "ByteLevel", "add_prefix_space": true, "use_regex": true}"#,
);
let err2 = Tokenizer::from_json_bytes(bad2.as_bytes()).unwrap_err();
assert!(matches!(err2, FocrError::FormatMismatch(_)), "got {err2:?}");
}
pub(super) fn load_real_smolvlm2() -> Option<Tokenizer> {
let path = std::env::var("FOCR_SMOLVLM2_TOKENIZER_JSON")
.ok()
.or_else(|| {
std::env::var("FOCR_SMOLVLM2_DIR")
.ok()
.map(|d| format!("{d}/tokenizer.json"))
});
let Some(path) = path else {
eprintln!(
"SKIP smolvlm2 tokenizer conformance: set FOCR_SMOLVLM2_TOKENIZER_JSON \
(or FOCR_SMOLVLM2_DIR) to the pinned tokenizer.json"
);
return None;
};
let path = Path::new(&path);
if !path.is_file() {
eprintln!(
"SKIP smolvlm2 tokenizer conformance: {} absent",
path.display()
);
return None;
}
Some(Tokenizer::from_file(path).expect("pinned smolvlm2 tokenizer.json must load"))
}
#[test]
fn smolvlm2_real_vocab_anchors() {
let Some(t) = load_real_smolvlm2() else {
return;
};
assert_eq!(t.scheme(), PretokScheme::SmolLm2);
assert_eq!(t.vocab_size(), 49152);
assert_eq!(t.encode("<|endoftext|>").unwrap(), vec![0]);
assert_eq!(t.encode("<|im_start|>").unwrap(), vec![1]);
assert_eq!(t.encode("<|im_end|>").unwrap(), vec![2]);
assert_eq!(
t.encode("<global-img>").unwrap(),
vec![special_smollm2::GLOBAL_IMG]
);
assert_eq!(
t.encode("<fake_token_around_image>").unwrap(),
vec![special_smollm2::FAKE_AROUND_IMAGE]
);
assert_eq!(t.encode("<image>").unwrap(), vec![special_smollm2::IMAGE]);
assert_eq!(
t.encode("<end_of_utterance>").unwrap(),
vec![special_smollm2::END_OF_UTTERANCE]
);
assert_eq!(t.encode("<row_1_col_1>").unwrap(), vec![49153]);
assert_eq!(t.encode("<row_6_col_6>").unwrap(), vec![49188]);
}
#[test]
fn smolvlm2_token_id_conformance_gate() {
let Some(t) = load_real_smolvlm2() else {
return;
};
const EXPECTED: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/tokenizer_smolvlm2/expected.json"
));
let v: serde_json::Value = serde_json::from_str(EXPECTED).unwrap();
assert_eq!(
v["_meta"]["tokenizer_json_sha256"].as_str().unwrap(),
"5ece781dc8d2b2f3e2f289ca0ae50b17cfc27dd27bfe7971bb8241e0b964331a",
"fixture was generated against a different tokenizer.json pin"
);
let cases = v["fixtures"].as_array().expect("fixtures array");
let num_cases = v["_meta"]["num_cases"].as_u64().unwrap() as usize;
assert_eq!(cases.len(), num_cases, "fixture _meta.num_cases drift");
assert!(
cases.len() >= 100,
"conformance corpus must stay >= 100 cases"
);
let mut mismatches = 0usize;
for rec in cases {
let text = rec["text"].as_str().unwrap();
let want: Vec<u32> = rec["ids"]
.as_array()
.unwrap()
.iter()
.map(|x| x.as_u64().unwrap() as u32)
.collect();
let got = t.encode(text).unwrap();
if got != want {
let pos = got
.iter()
.zip(&want)
.position(|(a, b)| a != b)
.unwrap_or_else(|| got.len().min(want.len()));
eprintln!(
"ENC MISMATCH {{\"case\": {text:?}, \"len\": {}, \"mismatch_pos\": {pos}}}\n \
got {got:?}\n want {want:?}",
want.len()
);
mismatches += 1;
}
let want_decoded = rec["decoded"].as_str().unwrap();
let got_decoded = t.decode(&want).unwrap();
if got_decoded != want_decoded {
eprintln!(
"DEC MISMATCH {{\"case\": {text:?}}}\n \
got {got_decoded:?}\n want {want_decoded:?}"
);
mismatches += 1;
}
}
assert_eq!(
mismatches, 0,
"tok_id_mismatch_count must be 0 (got {mismatches})"
);
}
#[test]
fn opt_files_load_and_scheme_dispatch() {
let vocab = r#"{"a":0,"b":1,"c":2,"1":3,"2":4,"Ġ":5,"ab":6,"12":7}"#.as_bytes();
let merges = b"#version: 0.2\na b\n1 2\n";
let added = br#"{"<imgpad>":50265,"<img>":50266,"</img>":50267,"<Number>":50268}"#;
let t = Tokenizer::from_opt_files(vocab, merges, added).expect("opt files load");
assert_eq!(t.scheme(), PretokScheme::Gpt2);
assert_eq!(t.encode("12").unwrap(), vec![7]);
assert_eq!(t.encode("ab").unwrap(), vec![6]);
assert_eq!(
t.encode("ab<imgpad>ab").unwrap(),
vec![6, special_opt::IMG_PAD, 6]
);
assert_eq!(
t.decode_skip_special(&t.encode("ab<Number>").unwrap())
.unwrap(),
"ab"
);
assert_eq!(t.bos_id(), special_opt::BOS_EOS);
assert_eq!(t.eos_id(), special_opt::BOS_EOS);
assert_eq!(t.pad_id(), special_opt::PAD);
assert_eq!(t.image_id(), special_opt::IMG_PAD);
let bad = br#"{"<imgpad>":999}"#;
let err = Tokenizer::from_opt_files(vocab, merges, bad).unwrap_err();
assert!(matches!(err, FocrError::FormatMismatch(_)), "got {err:?}");
let err = Tokenizer::from_opt_dir(Path::new("/nonexistent")).unwrap_err();
assert!(matches!(err, FocrError::ModelNotFound(_)), "got {err:?}");
}
#[test]
fn onechart_token_id_conformance_gate() {
let Ok(dir) = std::env::var("FOCR_ONECHART_DIR") else {
eprintln!("SKIP onechart tokenizer conformance: set FOCR_ONECHART_DIR");
return;
};
let t = Tokenizer::from_opt_dir(Path::new(&dir)).expect("onechart tokenizer loads");
assert_eq!(t.vocab_size(), 50_265);
const EXPECTED: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/tokenizer_onechart/expected.json"
));
let v: serde_json::Value = serde_json::from_str(EXPECTED).unwrap();
let cases = v["fixtures"].as_object().expect("fixtures object");
assert!(cases.len() >= 25, "conformance corpus shrank");
let mut mismatches = 0usize;
for (text, want) in cases {
let want: Vec<u32> = want
.as_array()
.unwrap()
.iter()
.map(|x| x.as_u64().unwrap() as u32)
.collect();
let got = t.encode(text).unwrap();
if got != want {
eprintln!("ENC MISMATCH {text:?}\n got {got:?}\n want {want:?}");
mismatches += 1;
}
}
eprintln!("[D9] {} cases, {mismatches} mismatches", cases.len());
assert_eq!(mismatches, 0, "tok_id_mismatch_count must be 0");
}
}