use super::*;
use crate::core::policy::PolicyError;
use crate::core::tokenize::{Tokenize, TokenizeError};
use crate::core::{AnyTokenizer, Backend};
#[test]
fn dispatches_bpe_byte_level() {
let json = r#"{
"added_tokens": [{"id": 2, "content": "<|endoftext|>", "special": true}],
"normalizer": null,
"pre_tokenizer": {"type": "ByteLevel", "add_prefix_space": false},
"model": {"type": "BPE", "vocab": {"a": 0, "Ġa": 1}, "merges": []}
}"#;
let tok = from_json_bytes(json.as_bytes()).expect("bpe ok");
assert_eq!(tok.family(), "BPE");
assert!(matches!(tok.backend(), Backend::Bpe(_)));
if let Backend::Bpe(t) = tok.backend() {
assert_eq!(t.encode_with_special("<|endoftext|>"), vec![2]);
}
}
#[test]
fn bpe_split_regex_is_read_from_json() {
let json = r#"{
"added_tokens": [],
"pre_tokenizer": {"type": "Sequence", "pretokenizers": [
{"type": "Split", "pattern": {"Regex": "\\w+|[^\\w\\s]+"}, "behavior": "Isolated"},
{"type": "ByteLevel", "add_prefix_space": false}
]},
"model": {"type": "BPE", "vocab": {"a": 0, "Ġa": 1}, "merges": []}
}"#;
let tok = from_json_bytes(json.as_bytes()).expect("bpe ok");
assert!(matches!(tok.backend(), Backend::Bpe(_)));
}
#[test]
fn bpe_applies_nfc_normalizer_to_content() {
let json = r#"{
"added_tokens": [],
"normalizer": {"type": "NFC"},
"pre_tokenizer": {"type": "ByteLevel", "add_prefix_space": false},
"model": {"type": "BPE",
"vocab": {"a": 0, "Ã": 1, "©": 2, "é": 3},
"merges": ["Ã ©"]}
}"#;
let tok = from_json_bytes(json.as_bytes()).expect("bpe ok");
if let Backend::Bpe(t) = tok.backend() {
let precomposed = t.encode("é"); let combining = t.encode("e\u{301}"); assert_eq!(precomposed, combining);
} else {
panic!("expected BPE backend");
}
}
#[test]
fn rejects_non_byte_level_keys_when_byte_level() {
let json = r#"{
"pre_tokenizer": {"type": "ByteLevel"},
"model": {"type": "BPE", "vocab": {"你好": 0}, "merges": []}
}"#;
let err = from_json_bytes(json.as_bytes());
assert!(matches!(err, Err(HfJsonError::InvalidByteLevel(_))));
}
#[test]
fn bpe_honors_merge_order_independent_of_ids() {
let json = r#"{
"pre_tokenizer": {"type": "ByteLevel"},
"model": {"type": "BPE",
"vocab": {"a":0,"b":1,"c":2,"bc":4,"ab":5},
"merges": [["a","b"], ["b","c"]]}
}"#;
let Backend::Bpe(t) = from_json_bytes(json.as_bytes()).unwrap().into_backend() else {
panic!("bpe");
};
assert_eq!(t.encode("abc"), vec![5, 2]);
}
#[test]
fn bpe_metaspace_pretokenizer_folds_spaces_into_underscore_prefix() {
let json = r#"{
"pre_tokenizer": {"type": "Metaspace", "prepend_scheme": "first"},
"model": {"type": "BPE",
"vocab": {"▁hello": 10, "▁world": 11, "hello": 12, "world": 13},
"merges": []}
}"#;
let Backend::Bpe(t) = from_json_bytes(json.as_bytes()).unwrap().into_backend() else {
panic!("expected BPE backend");
};
assert_eq!(t.encode("hello world"), vec![10, 11]);
}
#[test]
fn bpe_metaspace_pretokenizer_omits_prefix_when_prepend_scheme_never() {
let json = r#"{
"pre_tokenizer": {"type": "Metaspace", "prepend_scheme": "never"},
"model": {"type": "BPE",
"vocab": {"▁hello": 10, "▁world": 11, "hello": 12, "world": 13},
"merges": []}
}"#;
let Backend::Bpe(t) = from_json_bytes(json.as_bytes()).unwrap().into_backend() else {
panic!("expected BPE backend");
};
assert_eq!(t.encode("hello world"), vec![12, 11]);
}
#[test]
fn bpe_metaspace_merges_multibyte_underscore_instead_of_byte_fallback() {
let mut vocab = String::new();
for b in 0..256u32 {
vocab.push_str(&format!("\"<0x{b:02X}>\": {b}, "));
}
vocab.push_str(
r#""h": 256, "e": 257, "l": 258, "o": 259, "▁": 260,
"he": 261, "▁he": 262, "ll": 263, "llo": 264"#,
);
let json = format!(
r#"{{
"pre_tokenizer": {{"type": "Metaspace", "prepend_scheme": "first"}},
"model": {{"type": "BPE", "byte_fallback": true,
"vocab": {{{vocab}}},
"merges": [["h","e"], ["▁","he"], ["l","l"], ["ll","o"]]}}
}}"#
);
let Backend::Bpe(t) = from_json_bytes(json.as_bytes()).unwrap().into_backend() else {
panic!("expected BPE backend");
};
let ids = t.encode("hello");
assert_eq!(ids, vec![262, 264], "expected [▁he, llo], got {ids:?}");
for byte in [0xE2u32, 0x96, 0x81] {
assert!(!ids.contains(&byte), "`▁` shattered into <0x{byte:02X}>");
}
}
#[test]
fn bpe_null_pre_tokenizer_runs_the_model_over_the_whole_string() {
for pre_tokenizer_member in [r#""pre_tokenizer": null,"#, ""] {
let json = format!(
r#"{{
"added_tokens": [],
{pre_tokenizer_member}
"normalizer": {{"type": "Sequence", "normalizers": [
{{"type": "Prepend", "prepend": "▁"}},
{{"type": "Replace", "pattern": {{"String": " "}}, "content": "▁"}}
]}},
"model": {{"type": "BPE",
"vocab": {{"▁a": 264, "▁": 28705, "a": 28708}},
"merges": [["▁", "a"]]}}
}}"#
);
let Backend::Bpe(t) = from_json_bytes(json.as_bytes())
.expect("a document without a pre_tokenizer loads")
.into_backend()
else {
panic!("expected BPE backend");
};
assert_eq!(t.encode("a"), vec![264], "with {pre_tokenizer_member:?}");
assert_eq!(t.encode(" a"), vec![28705, 264]);
assert_eq!(t.encode("a "), vec![264, 28705]);
}
}
#[test]
fn bpe_metaspace_prepends_before_leading_non_space_whitespace() {
let json = r#"{
"added_tokens": [],
"pre_tokenizer": {"type": "Metaspace", "replacement": "▁",
"prepend_scheme": "first", "split": false},
"model": {"type": "BPE", "byte_fallback": true, "unk_token": "<unk>",
"vocab": {"<unk>": 0, "▁a": 1032, "<0x09>": 780, "<0x0A>": 781,
"▁": 29473, "a": 29476},
"merges": [["▁", "a"]]}
}"#;
let Backend::Bpe(t) = from_json_bytes(json.as_bytes())
.expect("the metaspace document loads")
.into_backend()
else {
panic!("expected BPE backend");
};
assert_eq!(t.encode("\n\n\n"), vec![29473, 781, 781, 781]);
assert_eq!(t.encode("\ta"), vec![29473, 780, 29476]);
assert_eq!(t.encode("a"), vec![1032]);
assert_eq!(t.encode(" a"), vec![1032]);
assert_eq!(t.encode(" a"), vec![29473, 1032]);
assert_eq!(t.encode("a "), vec![1032, 29473]);
}
#[test]
fn unigram_uses_viterbi_not_greedy() {
let json = r#"{
"pre_tokenizer": {"type": "Metaspace"},
"model": {"type": "Unigram", "unk_id": 0, "vocab": [
["<unk>", 0.0], ["</s>", 0.0],
["▁abc", -1.0], ["▁ab", -5.0], ["c", -1.0],
["▁a", -3.0], ["b", -3.0]
]}
}"#;
let Backend::Unigram(t) = from_json_bytes(json.as_bytes()).unwrap().into_backend() else {
panic!("unigram");
};
assert_eq!(t.encode("abc"), vec![2]);
}
#[test]
fn dispatches_unigram() {
let json = r#"{
"added_tokens": [
{"id": 0, "content": "<unk>", "special": true},
{"id": 1, "content": "</s>", "special": true}
],
"model": {
"type": "Unigram",
"unk_id": 0,
"vocab": [["<unk>", 0.0], ["</s>", 0.0], ["a", -1.0], ["b", -2.0]]
}
}"#;
let tok = from_json_bytes(json.as_bytes()).expect("unigram ok");
assert_eq!(tok.family(), "Unigram");
assert!(matches!(tok.backend(), Backend::Unigram(_)));
}
#[test]
fn unigram_without_eos_loads() {
let json = r#"{
"added_tokens": [],
"model": {"type": "Unigram", "unk_id": 0, "vocab": [["<unk>", 0.0], ["a", 0.0]]}
}"#;
let tok = from_json_bytes(json.as_bytes()).expect("loads");
assert_eq!(tok.family(), "Unigram");
}
#[test]
fn dispatches_wordpiece_with_lowercasing() {
let json = r###"{
"added_tokens": [],
"normalizer": {"type": "BertNormalizer", "lowercase": true, "strip_accents": null},
"pre_tokenizer": {"type": "BertPreTokenizer"},
"model": {
"type": "WordPiece",
"unk_token": "[UNK]",
"continuing_subword_prefix": "##",
"max_input_chars_per_word": 100,
"vocab": {"[UNK]": 0, "[CLS]": 1, "[SEP]": 2, "hello": 3, "##world": 4, "world": 5}
}
}"###;
let tok = from_json_bytes(json.as_bytes()).expect("wordpiece ok");
assert_eq!(tok.family(), "WordPiece");
let Backend::WordPiece(t) = tok.backend() else {
panic!("expected wordpiece");
};
assert_eq!(t.encode("HELLO"), vec![3]);
assert_eq!(t.encode("hello"), vec![3]);
}
#[test]
fn unigram_applies_replace_normalizer_in_order() {
let json = r#"{
"normalizer": {"type": "Sequence", "normalizers": [
{"type": "Replace", "pattern": {"String": "''"}, "content": "x"}
]},
"pre_tokenizer": {"type": "Metaspace"},
"added_tokens": [{"id": 1, "content": "</s>", "special": true}],
"model": {"type": "Unigram", "unk_id": 0, "vocab": [
["<unk>", 0.0], ["</s>", 0.0], ["▁x", -1.0], ["▁", -2.0], ["x", -3.0]
]}
}"#;
let Backend::Unigram(t) = from_json_bytes(json.as_bytes()).unwrap().into_backend() else {
panic!("unigram");
};
assert_eq!(t.encode("''"), vec![2]);
}
#[test]
fn wordpiece_custom_continuation_prefix() {
let json = r###"{
"model": {"type": "WordPiece", "unk_token": "[UNK]",
"continuing_subword_prefix": "@@", "max_input_chars_per_word": 100,
"vocab": {"[UNK]": 0, "foo": 1, "@@bar": 2}}
}"###;
let Backend::WordPiece(t) = from_json_bytes(json.as_bytes()).unwrap().into_backend() else {
panic!("wordpiece");
};
assert_eq!(t.encode("foobar"), vec![1, 2]);
}
#[test]
fn bpe_matches_added_tokens_in_encode() {
let json = r#"{
"added_tokens": [{"id": 2, "content": "<sp>", "special": false}],
"pre_tokenizer": {"type": "ByteLevel", "add_prefix_space": false},
"model": {"type": "BPE", "vocab": {"a": 0, "b": 1, "<sp>": 2}, "merges": []}
}"#;
let tok = from_json_bytes(json.as_bytes()).unwrap();
assert_eq!(tok.encode("a<sp>b"), vec![0, 2, 1]);
}
#[test]
fn wordpiece_matches_added_tokens_in_input() {
let json = r###"{
"added_tokens": [{"id": 2, "content": "[SEP]", "special": true}],
"model": {"type": "WordPiece", "unk_token": "[UNK]",
"continuing_subword_prefix": "##", "max_input_chars_per_word": 100,
"vocab": {"[UNK]": 0, "a": 1, "[SEP]": 2, "b": 3}}
}"###;
let tok = from_json_bytes(json.as_bytes()).unwrap();
assert_eq!(tok.encode("a [SEP] b"), vec![1, 2, 3]);
}
#[test]
fn unigram_matches_added_tokens_in_input() {
let json = r#"{
"added_tokens": [{"id": 1, "content": "</s>", "special": true}],
"pre_tokenizer": {"type": "Metaspace"},
"model": {"type": "Unigram", "unk_id": 0, "vocab": [
["<unk>", 0.0], ["</s>", 0.0], ["▁a", -1.0], ["▁b", -1.0]
]}
}"#;
let tok = from_json_bytes(json.as_bytes()).unwrap();
assert_eq!(tok.encode("a</s>b"), vec![2, 1, 3]);
}
#[test]
fn post_processor_wraps_with_special_tokens() {
let tok = from_json_bytes(BERT_PAIR_JSON.as_bytes()).unwrap();
assert_eq!(tok.encode_raw("hi"), vec![3]); assert_eq!(tok.encode("hi"), vec![1, 3, 2]); }
const BERT_PAIR_JSON: &str = r###"{
"post_processor": {"type": "BertProcessing", "cls": ["[CLS]", 1], "sep": ["[SEP]", 2]},
"model": {"type": "WordPiece", "unk_token": "[UNK]",
"continuing_subword_prefix": "##", "max_input_chars_per_word": 100,
"vocab": {"[UNK]": 0, "[CLS]": 1, "[SEP]": 2, "hi": 3, "yo": 4}}
}"###;
#[test]
fn encode_pair_applies_the_bert_pair_template() {
let tok = from_json_bytes(BERT_PAIR_JSON.as_bytes()).unwrap();
assert_eq!(tok.encode_pair("hi", "yo").unwrap(), vec![1, 3, 2, 4, 2]);
}
#[test]
fn encode_pair_without_a_template_errors() {
let json = r#"{
"pre_tokenizer": {"type": "ByteLevel", "add_prefix_space": false},
"model": {"type": "BPE", "vocab": {"a": 0, "b": 1}, "merges": []}
}"#;
let tok = from_json_bytes(json.as_bytes()).unwrap();
assert!(matches!(
tok.encode_pair("a", "b"),
Err(PolicyError::NoPairTemplate)
));
}
#[test]
fn policy_exposes_named_specials_and_eos() {
let tok = from_json_bytes(
r###"{
"added_tokens": [
{"id": 1, "content": "[CLS]", "special": true},
{"id": 2, "content": "[SEP]", "special": true}
],
"model": {"type": "WordPiece", "unk_token": "[UNK]",
"continuing_subword_prefix": "##", "max_input_chars_per_word": 100,
"vocab": {"[UNK]": 0, "[CLS]": 1, "[SEP]": 2, "hi": 3}}
}"###
.as_bytes(),
)
.unwrap();
assert_eq!(tok.special_token_id("[CLS]"), Some(1));
assert_eq!(tok.eos_token_id(), Some(2));
assert!(tok.is_eos(2));
}
#[test]
fn decode_skips_special_keeps_nonspecial_added_tokens() {
let json = r#"{
"added_tokens": [
{"id": 2, "content": "<|end|>", "special": true},
{"id": 3, "content": "<sp>", "special": false}
],
"pre_tokenizer": {"type": "ByteLevel", "add_prefix_space": false},
"model": {"type": "BPE", "vocab": {"a": 0, "b": 1, "<|end|>": 2, "<sp>": 3}, "merges": []}
}"#;
let tok = from_json_bytes(json.as_bytes()).unwrap();
let ids = tok.encode("a<|end|><sp>b"); assert_eq!(ids, vec![0, 2, 3, 1]);
assert_eq!(Tokenize::decode(&tok, &ids).unwrap(), "a<sp>b");
}
#[test]
fn unsupported_model_type_errors() {
let json = r#"{"model": {"type": "Phantom", "vocab": {}}}"#;
let err = from_json_bytes(json.as_bytes());
assert!(matches!(err, Err(HfJsonError::UnsupportedModelType(t)) if t == "Phantom"));
}
#[test]
fn missing_model_errors() {
let err = from_json_bytes(b"{}");
assert!(matches!(err, Err(HfJsonError::MissingField("model"))));
}
#[test]
fn infers_unigram_without_model_type() {
let json = r#"{
"added_tokens": [{"id": 1, "content": "</s>", "special": true}],
"model": {"unk_id": 0, "vocab": [["<unk>", 0.0], ["</s>", 0.0], ["x", -1.0]]}
}"#;
let tok = from_json_bytes(json.as_bytes()).expect("inferred unigram");
assert_eq!(tok.family(), "Unigram");
}
#[test]
fn infers_wordpiece_without_model_type() {
let json = r#"{
"model": {
"unk_token": "[UNK]", "continuing_subword_prefix": "@@",
"max_input_chars_per_word": 100, "vocab": {"[UNK]": 0, "hi": 1}
}
}"#;
let tok = from_json_bytes(json.as_bytes()).expect("inferred wordpiece");
assert_eq!(tok.family(), "WordPiece");
}
#[test]
fn infers_bpe_without_model_type() {
let json = r#"{
"pre_tokenizer": {"type": "ByteLevel"},
"model": {"vocab": {"a": 0, "Ġa": 1}, "merges": []}
}"#;
let tok = from_json_bytes(json.as_bytes()).expect("inferred bpe");
assert_eq!(tok.family(), "BPE");
}
#[test]
fn unknown_normalizer_errors_not_silently_dropped() {
let json = r#"{
"normalizer": {"type": "Sequence", "normalizers": [
{"type": "NFC"},
{"type": "SomeFutureNormalizer"}
]},
"pre_tokenizer": {"type": "ByteLevel"},
"model": {"type": "BPE", "vocab": {"a": 0}, "merges": []}
}"#;
let err = from_json_bytes(json.as_bytes());
assert!(
matches!(&err, Err(HfJsonError::UnsupportedNormalizer(t)) if t.contains("SomeFutureNormalizer"))
);
}
#[test]
fn uncompilable_replace_regex_errors_not_literal() {
let json = r#"{
"normalizer": {"type": "Replace", "pattern": {"Regex": "(?P<"}, "content": "x"},
"pre_tokenizer": {"type": "ByteLevel"},
"model": {"type": "BPE", "vocab": {"a": 0}, "merges": []}
}"#;
let err = from_json_bytes(json.as_bytes());
assert!(matches!(&err, Err(HfJsonError::InvalidNormalizerRegex(_))));
}
#[test]
fn unknown_pretokenizer_without_recognized_split_errors() {
let json = r#"{
"pre_tokenizer": {"type": "UnicodeScripts"},
"model": {"type": "BPE", "vocab": {"a": 0}, "merges": []}
}"#;
let err = from_json_bytes(json.as_bytes());
assert!(
matches!(&err, Err(HfJsonError::UnsupportedPreTokenizer(t)) if t.contains("UnicodeScripts"))
);
}
#[test]
fn engine_handled_pretokenizer_without_bytelevel_still_loads() {
let json = r#"{
"pre_tokenizer": {"type": "Digits", "individual_digits": true},
"model": {"type": "BPE", "vocab": {"a": 0, "1": 1}, "merges": []}
}"#;
let tok = from_json_bytes(json.as_bytes()).expect("digits pre-tokenizer loads");
assert_eq!(tok.family(), "BPE");
}
#[test]
fn declared_but_empty_pretokenizer_sequence_does_not_split() {
for pre in [
"null",
r#"{"type": "Sequence", "pretokenizers": []}"#,
r#"{"type": "Sequence", "pretokenizers": [
{"type": "Sequence", "pretokenizers": []}
]}"#,
] {
let json = format!(
r#"{{
"pre_tokenizer": {pre},
"model": {{"type": "BPE", "unk_token": "<unk>",
"vocab": {{"<unk>": 0, "a": 1, "b": 2, "1": 3, "ab": 4, "ab1": 5}},
"merges": ["a b", "ab 1"]}}
}}"#
);
let tok = from_json_bytes(json.as_bytes()).expect("an empty pipeline is loadable");
assert_eq!(tok.encode("ab1ab1"), vec![5, 5], "with pre_tokenizer {pre}");
}
}
#[test]
fn declared_but_unreadable_pretokenizers_are_refused_not_treated_as_empty() {
for pre in [
r#"{"type": "Sequence"}"#,
r#"{"type": "Split", "behavior": "Isolated"}"#,
r#"{"foo": "bar"}"#,
r#"{"type": "SomeFuturePreTokenizer"}"#,
r#"{"type": "Sequence", "pretokenizers": [{"type": "Split", "behavior": "Isolated"}]}"#,
] {
let json = format!(
r#"{{
"pre_tokenizer": {pre},
"model": {{"type": "BPE", "vocab": {{"a": 0, "b": 1, "ab": 2}},
"merges": ["a b"]}}
}}"#
);
assert!(
matches!(
from_json_bytes(json.as_bytes()),
Err(HfJsonError::UnsupportedPreTokenizer(_))
),
"pre_tokenizer {pre} must be refused, not guessed at"
);
}
}
#[test]
fn unknown_pretokenizer_is_ok_when_split_is_anchored() {
let json = r#"{
"pre_tokenizer": {"type": "Sequence", "pretokenizers": [
{"type": "UnicodeScripts"},
{"type": "ByteLevel", "add_prefix_space": false}
]},
"model": {"type": "BPE", "vocab": {"a": 0, "Ġa": 1}, "merges": []}
}"#;
let tok = from_json_bytes(json.as_bytes()).expect("loads with anchored split");
assert_eq!(tok.family(), "BPE");
}
#[test]
fn added_token_lstrip_is_read_from_json_and_reaches_the_matcher() {
let json = r#"{
"added_tokens": [
{"id": 10, "content": "<mask>", "special": true, "lstrip": true, "rstrip": false},
{"id": 11, "content": "<pad>", "special": true, "lstrip": false, "rstrip": false}
],
"pre_tokenizer": {"type": "ByteLevel", "add_prefix_space": false},
"model": {"type": "BPE", "vocab": {"a": 0, "Ġ": 1, "b": 2}, "merges": []}
}"#;
let tok = from_json_bytes(json.as_bytes()).expect("bpe ok");
let Backend::Bpe(t) = tok.backend() else {
panic!("expected BPE backend");
};
assert_eq!(t.encode("a <mask>b"), vec![0, 10, 2]);
assert_eq!(t.encode("a <pad>b"), vec![0, 1, 11, 2]);
}
#[test]
fn vocab_entry_that_is_also_an_added_token_is_taken_literally() {
let json = r#"{
"added_tokens": [
{"id": 3, "content": "<|eos|>", "special": true},
{"id": 4, "content": "<|tool|>", "special": false}
],
"pre_tokenizer": {"type": "ByteLevel", "add_prefix_space": false},
"decoder": {"type": "ByteLevel"},
"model": {"type": "BPE",
"vocab": {"a": 0, "Ġ": 1, "b": 2, "<|eos|>": 3, "<|tool|>": 4},
"merges": []}
}"#;
let tok = from_json_bytes(json.as_bytes()).expect("loads despite literal vocab entries");
assert_eq!(tok.encode_raw("a<|tool|>b"), vec![0, 4, 2]);
assert_eq!(tok.encode_raw("<|eos|>"), vec![3]);
assert_eq!(
Tokenize::decode(&tok, &[0, 4, 2]).expect("decodes"),
"a<|tool|>b"
);
assert_eq!(Tokenize::decode(&tok, &[3]).expect("decodes"), "");
}
#[test]
fn non_added_token_vocab_entry_that_is_not_byte_level_still_errors() {
let json = r#"{
"added_tokens": [{"id": 3, "content": "<|eos|>", "special": true}],
"pre_tokenizer": {"type": "ByteLevel", "add_prefix_space": false},
"model": {"type": "BPE",
"vocab": {"a": 0, "<|eos|>": 3, "你好": 5},
"merges": []}
}"#;
let err = from_json_bytes(json.as_bytes());
assert!(matches!(&err, Err(HfJsonError::InvalidByteLevel(t)) if t == "你好"));
}
#[test]
fn added_token_id_disagreeing_with_the_vocab_id_errors() {
let json = r#"{
"added_tokens": [{"id": 7, "content": "<|eos|>", "special": true}],
"pre_tokenizer": {"type": "ByteLevel", "add_prefix_space": false},
"model": {"type": "BPE", "vocab": {"a": 0, "<|eos|>": 3}, "merges": []}
}"#;
let err = from_json_bytes(json.as_bytes());
assert!(matches!(
&err,
Err(HfJsonError::AddedTokenIdConflict {
vocab_id: 3,
added_id: 7,
..
})
));
}
fn byte_fallback_vocab_entries(start_id: u32) -> String {
(0u32..256)
.map(|b| format!(r#""<0x{b:02X}>": {}"#, start_id + b))
.collect::<Vec<_>>()
.join(", ")
}
#[test]
fn byte_fallback_true_emits_the_fallback_id_for_an_unrepresented_byte() {
let json = format!(
r#"{{
"model": {{"type": "BPE", "byte_fallback": true,
"vocab": {{"a": 0, "c": 1, {}}},
"merges": []}}
}}"#,
byte_fallback_vocab_entries(2)
);
let Backend::Bpe(t) = from_json_bytes(json.as_bytes())
.expect("loads with a complete byte_fallback table")
.into_backend()
else {
panic!("expected BPE backend");
};
assert_eq!(t.encode("abc"), vec![0, 100, 1]);
}
#[test]
fn byte_fallback_false_or_absent_still_drops_the_unrepresented_byte() {
for model_fragment in [
r#""byte_fallback": false, "vocab""#,
r#""vocab""#,
] {
let json = format!(
r#"{{
"model": {{"type": "BPE", {model_fragment}: {{"a": 0, "c": 1, {}}},
"merges": []}}
}}"#,
byte_fallback_vocab_entries(2)
);
let Backend::Bpe(t) = from_json_bytes(json.as_bytes())
.expect("loads")
.into_backend()
else {
panic!("expected BPE backend");
};
assert!(!t.has_byte_fallback());
assert_eq!(t.encode("abc"), vec![0, 1]);
}
}
#[test]
fn unk_fallback_is_not_gated_on_the_byte_fallback_flag() {
for model_fragment in [
r#""byte_fallback": true,"#,
r#""byte_fallback": false,"#,
"",
] {
let json = format!(
r#"{{
"model": {{"type": "BPE", {model_fragment} "unk_token": "<unk>",
"vocab": {{"<unk>": 0, "a": 1, "c": 2}},
"merges": []}}
}}"#
);
let Backend::Bpe(t) = from_json_bytes(json.as_bytes())
.expect("loads")
.into_backend()
else {
panic!("expected BPE backend");
};
assert!(t.has_byte_fallback());
assert_eq!(t.encode("abc"), vec![1, 0, 2]);
}
}
#[test]
fn fuse_unk_collapses_a_run_of_unks_into_one() {
let cases = [
("xxzxx", vec![0, 2, 0, 0, 0], vec![2, 0]),
("xzx", vec![2, 0, 0], vec![2, 0]),
("axyzb", vec![1, 0, 2, 0, 3], vec![1, 2, 0, 3]),
("ééé", vec![0, 0, 0], vec![0]),
("xax", vec![0, 1, 0], vec![0, 1, 0]),
];
for (fragment, fused) in [
(r#""fuse_unk": true,"#, true),
(r#""fuse_unk": false,"#, false),
("", false),
] {
let json = format!(
r#"{{
"model": {{"type": "BPE", "byte_fallback": true, {fragment}
"unk_token": "<unk>",
"vocab": {{"<unk>": 0, "a": 1, "<0x7A>": 2, "b": 3, "ab": 4}},
"merges": ["a b"]}}
}}"#
);
let Backend::Bpe(t) = from_json_bytes(json.as_bytes())
.expect("loads")
.into_backend()
else {
panic!("expected BPE backend");
};
for (text, unfused_ids, fused_ids) in &cases {
let expected = if fused { fused_ids } else { unfused_ids };
assert_eq!(&t.encode(text), expected, "{text:?} with {fragment:?}");
}
}
}
#[test]
fn byte_level_bpe_never_reports_a_fallback_even_with_a_resolvable_unk() {
let json = r#"{
"pre_tokenizer": {"type": "ByteLevel", "add_prefix_space": false},
"model": {"type": "BPE", "unk_token": "<|endoftext|>",
"vocab": {"<|endoftext|>": 0, "a": 1, "Ġa": 2},
"merges": []}
}"#;
let Backend::Bpe(t) = from_json_bytes(json.as_bytes())
.expect("loads")
.into_backend()
else {
panic!("expected BPE backend");
};
assert!(!t.has_byte_fallback());
}
#[test]
fn byte_fallback_flag_gates_only_the_byte_token_branch() {
for (flag, expected) in [(true, vec![1, 2]), (false, vec![1, 0])] {
let json = format!(
r#"{{
"model": {{"type": "BPE", "byte_fallback": {flag}, "unk_token": "<unk>",
"vocab": {{"<unk>": 0, "a": 1, "<0x7A>": 2}},
"merges": []}}
}}"#
);
let Backend::Bpe(t) = from_json_bytes(json.as_bytes())
.expect("loads")
.into_backend()
else {
panic!("expected BPE backend");
};
assert_eq!(t.encode("az"), expected, "byte_fallback: {flag}");
}
}
#[test]
fn byte_fallback_true_with_a_partial_table_loads_and_resolves_per_byte() {
let json = r#"{
"model": {"type": "BPE", "byte_fallback": true, "unk_token": "<unk>",
"vocab": {"<unk>": 0, "a": 1, "c": 2, "<0x78>": 3},
"merges": []}
}"#;
let Backend::Bpe(t) = from_json_bytes(json.as_bytes())
.expect("a partial byte_fallback set is not a load failure")
.into_backend()
else {
panic!("expected BPE backend");
};
assert!(t.has_byte_fallback());
assert_eq!(t.encode("abxbc"), vec![1, 3, 0, 0, 2]);
}
#[test]
fn declaring_one_more_byte_token_flips_only_that_byte() {
let json = r#"{
"model": {"type": "BPE", "byte_fallback": true, "unk_token": "<unk>",
"vocab": {"<unk>": 0, "a": 1, "c": 2, "<0x78>": 3, "<0x62>": 4},
"merges": []}
}"#;
let Backend::Bpe(t) = from_json_bytes(json.as_bytes())
.expect("loads")
.into_backend()
else {
panic!("expected BPE backend");
};
assert_eq!(t.encode("abxbc"), vec![1, 4, 3, 4, 2]);
}
#[test]
fn byte_fallback_true_without_byte_tokens_or_unk_drops_the_byte() {
let json = r#"{
"model": {"type": "BPE", "byte_fallback": true,
"vocab": {"a": 1, "c": 2}, "merges": []}
}"#;
let Backend::Bpe(t) = from_json_bytes(json.as_bytes())
.expect("loads")
.into_backend()
else {
panic!("expected BPE backend");
};
assert!(!t.has_byte_fallback());
assert_eq!(t.encode("abc"), vec![1, 2]);
}
#[test]
fn byte_fallback_honors_a_non_default_unk_token_spelling() {
let json = r#"{
"model": {"type": "BPE", "byte_fallback": true, "unk_token": "[MISSING]",
"vocab": {"[MISSING]": 9, "a": 1, "c": 2},
"merges": []}
}"#;
let Backend::Bpe(t) = from_json_bytes(json.as_bytes())
.expect("loads")
.into_backend()
else {
panic!("expected BPE backend");
};
assert_eq!(t.encode("abc"), vec![1, 9, 2]);
}
#[test]
fn merges_over_byte_fallback_tokens_fire_as_huggingface_orders_them() {
let json = r#"{
"model": {"type": "BPE", "byte_fallback": true, "unk_token": "<unk>",
"vocab": {"<unk>": 0, "a": 1, "b": 2, "<0x7A>": 3,
"<0x7A>b": 4, "a<0x7A>": 5, "<0x7A><0x7A>": 6},
"merges": [["<0x7A>", "b"], ["a", "<0x7A>"], ["<0x7A>", "<0x7A>"]]}
}"#;
let Backend::Bpe(t) = from_json_bytes(json.as_bytes())
.expect("loads")
.into_backend()
else {
panic!("expected BPE backend");
};
assert_eq!(t.encode("zb"), vec![4]);
assert_eq!(t.encode("az"), vec![5]);
assert_eq!(t.encode("zz"), vec![6]);
assert_eq!(t.encode("zbz"), vec![4, 3]);
}
#[test]
fn byte_fallback_ids_decode_to_bytes_agreeing_with_the_declared_decoder() {
let json = r#"{
"decoder": {"type": "ByteFallback"},
"model": {"type": "BPE", "byte_fallback": true, "unk_token": "<unk>",
"vocab": {"<unk>": 0, "a": 1, "c": 2,
"<0xF0>": 3, "<0x90>": 4, "<0x8D>": 5, "<0x88>": 6},
"merges": []}
}"#;
let tok = from_json_bytes(json.as_bytes()).expect("loads");
assert!(tok.declares_decoder());
let Backend::Bpe(bpe) = tok.backend() else {
panic!("expected BPE backend");
};
let ids = bpe.encode("a𐍈c");
assert_eq!(ids, vec![1, 3, 4, 5, 6, 2]);
let bare = bpe.decode(&ids).expect("the bare backend decodes");
assert_eq!(bare, "a𐍈c");
assert_eq!(
tok.decode(&ids).expect("the declared pipeline decodes"),
bare
);
}
fn stream_in_chunks(tok: &AnyTokenizer, ids: &[u32], chunk: usize) -> String {
let mut decoder = tok.streaming_decoder().expect("this document streams");
let mut out = String::new();
for group in ids.chunks(chunk.max(1)) {
let emitted = decoder.add_tokens(group).expect("the ids are all known");
out.push_str(&emitted.unwrap_or_default());
}
out.push_str(&decoder.flush());
out
}
fn streams_like_decode(json: &str, ids: &[u32]) -> String {
let tok = from_json_bytes(json.as_bytes()).expect("the document loads");
let expected = tok.decode(ids).expect("whole-sequence decode succeeds");
for chunk in 1..=ids.len().max(1) {
assert_eq!(
stream_in_chunks(&tok, ids, chunk),
expected,
"streamed in chunks of {chunk} over {ids:?}"
);
}
expected
}
const BYTE_LEVEL_JSON: &str = r#"{
"added_tokens": [{"id": 5, "content": "<|end|>", "special": true}],
"pre_tokenizer": {"type": "ByteLevel", "add_prefix_space": false},
"decoder": {"type": "ByteLevel"},
"model": {"type": "BPE",
"vocab": {"a": 0, "Ġ": 1, "Ã": 2, "©": 3, "b": 4},
"merges": []}
}"#;
const METASPACE_JSON: &str = r#"{
"added_tokens": [{"id": 9, "content": "<s>", "special": true}],
"pre_tokenizer": {"type": "Metaspace", "prepend_scheme": "always"},
"decoder": {"type": "Metaspace", "prepend_scheme": "always"},
"model": {"type": "BPE",
"vocab": {"▁Hello": 10, "▁world": 11, "▁": 12},
"merges": []}
}"#;
const WORDPIECE_JSON: &str = r###"{
"added_tokens": [
{"id": 1, "content": "[CLS]", "special": true},
{"id": 2, "content": "[SEP]", "special": true}
],
"normalizer": {"type": "BertNormalizer", "lowercase": false, "strip_accents": null},
"pre_tokenizer": {"type": "BertPreTokenizer"},
"decoder": {"type": "WordPiece", "prefix": "##", "cleanup": true},
"model": {"type": "WordPiece", "unk_token": "[UNK]",
"continuing_subword_prefix": "##", "max_input_chars_per_word": 100,
"vocab": {"[UNK]": 0, "[CLS]": 1, "[SEP]": 2,
"hello": 3, "##world": 4, ",": 5, "world": 6}}
}"###;
const MISTRAL_JSON: &str = r#"{
"added_tokens": [{"id": 1, "content": "<s>", "special": true}],
"pre_tokenizer": {"type": "Metaspace", "prepend_scheme": "first"},
"decoder": {"type": "Sequence", "decoders": [
{"type": "Replace", "pattern": {"String": "▁"}, "content": " "},
{"type": "ByteFallback"},
{"type": "Fuse"},
{"type": "Strip", "content": " ", "start": 1, "stop": 0}
]},
"model": {"type": "BPE", "byte_fallback": true, "unk_token": "<unk>",
"vocab": {"<unk>": 0, "<s>": 1, "▁Hi": 2,
"<0xE2>": 3, "<0x82>": 4, "<0xAC>": 5,
"▁a": 6, "<0x80>": 7, "▁b": 8, "▁": 9},
"merges": []}
}"#;
#[test]
fn declared_pipelines_stream_exactly_as_they_decode() {
assert_eq!(streams_like_decode(BYTE_LEVEL_JSON, &[0, 1, 2, 3]), "a é");
assert_eq!(
streams_like_decode(BYTE_LEVEL_JSON, &[0, 5, 2, 3, 4]),
"aéb"
);
assert_eq!(
streams_like_decode(METASPACE_JSON, &[10, 11]),
"Hello world"
);
assert_eq!(
streams_like_decode(METASPACE_JSON, &[9, 10, 11]),
"Hello world"
);
assert_eq!(streams_like_decode(METASPACE_JSON, &[12, 10]), " Hello");
assert_eq!(streams_like_decode(WORDPIECE_JSON, &[3, 4]), "helloworld");
assert_eq!(
streams_like_decode(WORDPIECE_JSON, &[1, 3, 5, 6, 2]),
"hello, world"
);
assert_eq!(streams_like_decode(MISTRAL_JSON, &[1, 2, 3, 4, 5]), "Hi€");
assert_eq!(streams_like_decode(MISTRAL_JSON, &[6, 7, 8]), "a\u{fffd} b");
assert_eq!(streams_like_decode(MISTRAL_JSON, &[9, 6]), " a");
}
#[test]
fn a_pipeline_that_cannot_stream_is_refused_rather_than_approximated() {
let json = r#"{
"decoder": {"type": "BPEDecoder", "suffix": "</w>"},
"model": {"type": "BPE", "vocab": {"hello</w>": 0, "world</w>": 1}, "merges": []}
}"#;
let tok = from_json_bytes(json.as_bytes()).expect("the document loads");
assert!(tok.declares_decoder());
let err = tok
.streaming_decoder()
.err()
.expect("BPEDecoder cannot stream");
assert!(
matches!(err, TokenizeError::UnstreamableDecoder("BPEDecoder")),
"unexpected error: {err}"
);
assert_eq!(tok.decode(&[0, 1]).expect("decodes"), "hello world");
}
#[test]
fn a_document_with_no_declared_decoder_delegates_to_the_backend() {
let json = r###"{
"added_tokens": [
{"id": 1, "content": "[CLS]", "special": true},
{"id": 2, "content": "[SEP]", "special": true}
],
"pre_tokenizer": {"type": "BertPreTokenizer"},
"model": {"type": "WordPiece", "unk_token": "[UNK]",
"continuing_subword_prefix": "##", "max_input_chars_per_word": 100,
"vocab": {"[UNK]": 0, "[CLS]": 1, "[SEP]": 2,
"hello": 3, "##world": 4, ",": 5, "world": 6}}
}"###;
let tok = from_json_bytes(json.as_bytes()).expect("the document loads");
assert!(!tok.declares_decoder());
assert_eq!(streams_like_decode(json, &[3, 4]), "helloworld");
assert_eq!(streams_like_decode(json, &[1, 3, 5, 6, 2]), "hello, world");
}
#[test]
fn raw_byte_fast_path_agrees_with_the_mapped_path() {
const VOCAB: &str = r#"{"a": 0, "Ġa": 1, "b": 2, "Ġb": 3, "Ġ": 4, "Ċ": 5,
"é": 6, "Ġé": 7, "ab": 8, "Ġab": 9}"#;
let piped = format!(
r#"{{"added_tokens": [],
"pre_tokenizer": {{"type": "Sequence", "pretokenizers": [
{{"type": "Split", "pattern": {{"Regex": " ?[^\\s]+|\\s"}}, "behavior": "Isolated"}},
{{"type": "ByteLevel", "add_prefix_space": false}}
]}},
"model": {{"type": "BPE", "vocab": {VOCAB}, "merges": []}}}}"#
);
let bare = format!(
r#"{{"added_tokens": [],
"pre_tokenizer": {{"type": "ByteLevel", "add_prefix_space": false, "use_regex": true}},
"model": {{"type": "BPE", "vocab": {VOCAB}, "merges": []}}}}"#
);
let piped = from_json_bytes(piped.as_bytes()).expect("piped loads");
let bare = from_json_bytes(bare.as_bytes()).expect("bare loads");
for text in [
"a", " a", "a b", " a b", "ab", " ab", "\u{e9}", " \u{e9}", "a\nb", " a", "",
] {
assert_eq!(
piped.encode_raw(text),
bare.encode_raw(text),
"fast path disagrees with the mapped path for {text:?}"
);
}
assert_eq!(piped.encode_raw(" a"), vec![1]);
assert_eq!(piped.encode_raw("a"), vec![0]);
assert_eq!(piped.encode_raw(" \u{e9}"), vec![7]);
}