mod common;
use xgrammar::{TokenizerInfo, VocabType};
const TEST_TOKENIZER_CASES: &[(&str, VocabType, bool)] = &[
("luodian/llama-7b-hf", VocabType::ByteFallback, true),
("meta-llama/Llama-2-7b-chat-hf", VocabType::ByteFallback, true),
("meta-llama/Meta-Llama-3-8B-Instruct", VocabType::ByteLevel, false),
("meta-llama/Meta-Llama-3.1-8B-Instruct", VocabType::ByteLevel, false),
("NousResearch/Hermes-2-Theta-Llama-3-70B", VocabType::ByteLevel, false),
("NousResearch/Hermes-3-Llama-3.1-8B", VocabType::ByteLevel, false),
("google/gemma-2b-it", VocabType::ByteFallback, false),
("CohereForAI/aya-23-8B", VocabType::ByteLevel, false),
("deepseek-ai/DeepSeek-Coder-V2-Instruct", VocabType::ByteLevel, false),
("deepseek-ai/DeepSeek-V2-Chat-0628", VocabType::ByteLevel, false),
("deepseek-ai/deepseek-coder-7b-instruct-v1.5", VocabType::ByteLevel, false),
("microsoft/phi-2", VocabType::ByteLevel, false),
("microsoft/Phi-3-mini-4k-instruct", VocabType::ByteFallback, true),
("microsoft/Phi-3.5-mini-instruct", VocabType::ByteFallback, true),
("Qwen/Qwen1.5-4B-Chat", VocabType::ByteLevel, false),
("meta-llama/Llama-3.2-1B", VocabType::ByteLevel, false),
("google/gemma-2-2b-it", VocabType::ByteFallback, false),
("deepseek-ai/DeepSeek-V2.5", VocabType::ByteLevel, false),
("Qwen/Qwen2.5-1.5B", VocabType::ByteLevel, false),
("mistralai/Mixtral-8x22B-Instruct-v0.1", VocabType::ByteFallback, true),
("deepseek-ai/DeepSeek-R1", VocabType::ByteLevel, false),
("deepseek-ai/DeepSeek-R1-Distill-Qwen-7B", VocabType::ByteLevel, false),
("deepseek-ai/DeepSeek-R1-Distill-Llama-8B", VocabType::ByteLevel, false),
("LGAI-EXAONE/EXAONE-3.5-7.8B-Instruct", VocabType::ByteLevel, false),
("LGAI-EXAONE/EXAONE-4.0-32B-FP8", VocabType::ByteLevel, false),
];
fn assert_metadata(
tokenizer_info: &TokenizerInfo,
expected_vocab_type: VocabType,
expected_add_prefix_space: bool,
) {
assert_eq!(tokenizer_info.get_vocab_type(), expected_vocab_type);
assert_eq!(tokenizer_info.get_add_prefix_space(), expected_add_prefix_space);
}
#[test]
#[ignore = "Ignored by default to avoid frequent HF hub downloads"]
fn test_tokenizer_info() {
for &(tokenizer_id, vocab_type, add_prefix_space) in TEST_TOKENIZER_CASES {
tracing::info!("Testing tokenizer: {}", tokenizer_id);
let tokenizer = common::load_tokenizer(tokenizer_id).expect("Failed to load tokenizer");
let tokenizer_info = TokenizerInfo::from_pretrained(tokenizer_id, None, None, None)
.expect("Failed to get tokenizer info");
assert_metadata(&tokenizer_info, vocab_type, add_prefix_space);
assert_eq!(tokenizer.get_vocab_size(true), tokenizer_info.get_vocab_size() as usize);
}
}
#[test]
fn test_tokenizer_info_serialize_roundtrip() {
let tokenizer_info = TokenizerInfo::from_pretrained("openai/gpt-oss-20b", None, None, None)
.expect("Failed to load tokenizer info");
let json = tokenizer_info.serialize_json();
assert!(!json.is_empty());
let restored = TokenizerInfo::deserialize_json(&json).expect("deserialization should succeed");
assert_eq!(restored.get_vocab_size(), tokenizer_info.get_vocab_size());
assert_eq!(restored.get_vocab_type(), tokenizer_info.get_vocab_type());
assert_eq!(restored.get_add_prefix_space(), tokenizer_info.get_add_prefix_space());
assert_eq!(json, restored.serialize_json());
let original_vocab = tokenizer_info.get_decoded_vocab();
let restored_vocab = restored.get_decoded_vocab();
assert_eq!(original_vocab.len(), restored_vocab.len());
for (i, (original, restored)) in original_vocab.iter().zip(restored_vocab.iter()).enumerate() {
let expected = match original.find('\0') {
Some(nul_idx) => &original[..nul_idx],
None => original.as_str(),
};
assert_eq!(restored.as_str(), expected, "vocab mismatch at token id {i}");
}
}
#[test]
fn test_tokenizer_info_deserialize_error() {
use xgrammar::XGrammarErr;
let Err(XGrammarErr::InvalidJson(err_msg)) = TokenizerInfo::deserialize_json("not json") else {
panic!("Expected InvalidJson");
};
assert!(err_msg.contains("Invalid JSON error"), "unexpected message: {err_msg}");
}
#[test]
fn test_tokenizer_info_deserialize_untyped_error() {
use xgrammar::XGrammarErr;
let Err(XGrammarErr::TokenizerInfoError(_)) = TokenizerInfo::deserialize_json("[1, 2, 3]")
else {
panic!("Expected TokenizerInfoError");
};
}