use super::EOG_TOKEN_TEXTS;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SpecialTokens {
AsText,
Parse,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SpecialKind {
Control,
UserDefined,
Unknown,
}
impl SpecialKind {
fn is_parsed(self, mode: SpecialTokens) -> bool {
match mode {
SpecialTokens::Parse => true,
SpecialTokens::AsText => self == SpecialKind::UserDefined,
}
}
}
const GGML_TOKEN_TYPE_UNKNOWN: i64 = 2;
const GGML_TOKEN_TYPE_CONTROL: i64 = 3;
const GGML_TOKEN_TYPE_USER_DEFINED: i64 = 4;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct SpecialToken {
pub text: String,
pub id: u32,
pub kind: SpecialKind,
}
pub(crate) enum TextOrSpecial<'a> {
Text(&'a str),
Special(u32),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct SpecialTokenTable {
entries: Vec<SpecialToken>,
}
impl SpecialTokenTable {
pub fn from_gguf(file: &impl ferrox_gguf::TensorSource, id_to_token: &[String]) -> Self {
let mut kinds: Vec<Option<SpecialKind>> = vec![None; id_to_token.len()];
if let Some(ferrox_gguf::GgufValue::Array(items)) =
file.metadata("tokenizer.ggml.token_type")
{
for (kind, v) in kinds.iter_mut().zip(items) {
let ty = match v {
ferrox_gguf::GgufValue::I32(t) => *t as i64,
ferrox_gguf::GgufValue::U32(t) => *t as i64,
_ => continue,
};
*kind = match ty {
GGML_TOKEN_TYPE_CONTROL => Some(SpecialKind::Control),
GGML_TOKEN_TYPE_USER_DEFINED => Some(SpecialKind::UserDefined),
GGML_TOKEN_TYPE_UNKNOWN => Some(SpecialKind::Unknown),
_ => None,
};
}
}
for (id, text) in id_to_token.iter().enumerate() {
if EOG_TOKEN_TEXTS.contains(&text.as_str()) {
kinds[id] = Some(SpecialKind::Control);
}
}
let has = |t: &str| id_to_token.iter().any(|x| x == t);
if has("<|end|>")
&& ((has("<|return|>") && has("<|call|>")) || (has("<|calls|>") && has("<|flush|>")))
{
for (id, text) in id_to_token.iter().enumerate() {
if text == "<|end|>" {
kinds[id] = Some(SpecialKind::UserDefined);
}
}
}
if has("<|tool_response>") && has("</s>") {
for (id, text) in id_to_token.iter().enumerate() {
if text == "</s>" {
kinds[id] = None;
}
}
}
Self::from_entries(
kinds
.into_iter()
.enumerate()
.filter_map(|(id, kind)| Some((id_to_token[id].as_str(), id as u32, kind?))),
)
}
pub fn from_entries<'a>(
entries: impl IntoIterator<Item = (&'a str, u32, SpecialKind)>,
) -> Self {
let mut entries: Vec<SpecialToken> = entries
.into_iter()
.filter(|(text, _, _)| !text.is_empty())
.map(|(text, id, kind)| SpecialToken {
text: text.to_string(),
id,
kind,
})
.collect();
entries.sort_by_key(|e| std::cmp::Reverse(e.text.len()));
SpecialTokenTable { entries }
}
pub fn split<'a>(&self, text: &'a str, mode: SpecialTokens) -> Vec<TextOrSpecial<'a>> {
let mut fragments = vec![TextOrSpecial::Text(text)];
for special in self.entries.iter().filter(|s| s.kind.is_parsed(mode)) {
let mut next = Vec::with_capacity(fragments.len());
for fragment in fragments {
match fragment {
TextOrSpecial::Special(id) => next.push(TextOrSpecial::Special(id)),
TextOrSpecial::Text(run) => {
let mut rest = run;
while let Some(at) = rest.find(special.text.as_str()) {
if at > 0 {
next.push(TextOrSpecial::Text(&rest[..at]));
}
next.push(TextOrSpecial::Special(special.id));
rest = &rest[at + special.text.len()..];
}
if !rest.is_empty() {
next.push(TextOrSpecial::Text(rest));
}
}
}
}
fragments = next;
}
fragments
}
}
#[cfg(test)]
mod tests {
use super::*;
use ferrox_gguf::{GgufError, GgufValue, TensorInfo, TensorSource};
fn text_of<'a>(seg: &TextOrSpecial<'a>) -> Option<&'a str> {
match seg {
TextOrSpecial::Text(t) => Some(t),
TextOrSpecial::Special(_) => None,
}
}
fn table(specials: &[(&str, u32)]) -> SpecialTokenTable {
SpecialTokenTable::from_entries(
specials
.iter()
.map(|&(t, id)| (t, id, SpecialKind::Control)),
)
}
#[test]
fn an_empty_table_returns_the_whole_text_unsplit() {
let segs = table(&[]).split("hello world", SpecialTokens::Parse);
assert_eq!(segs.len(), 1);
assert_eq!(text_of(&segs[0]), Some("hello world"));
}
#[test]
fn splits_around_a_single_special_token_in_the_middle() {
let segs = table(&[("<|sep|>", 99)]).split("before<|sep|>after", SpecialTokens::Parse);
assert_eq!(segs.len(), 3);
assert_eq!(text_of(&segs[0]), Some("before"));
assert!(matches!(segs[1], TextOrSpecial::Special(99)));
assert_eq!(text_of(&segs[2]), Some("after"));
}
#[test]
fn multiple_occurrences_and_multiple_distinct_specials_all_split() {
let segs = table(&[("<a>", 1), ("<b>", 2)]).split("<a>x<b>y<a>", SpecialTokens::Parse);
let ids: Vec<u32> = segs
.iter()
.filter_map(|s| match s {
TextOrSpecial::Special(id) => Some(*id),
_ => None,
})
.collect();
assert_eq!(ids, vec![1, 2, 1]);
let texts: Vec<&str> = segs.iter().filter_map(text_of).collect();
assert_eq!(texts, vec!["x", "y"]);
}
#[test]
fn the_longest_special_is_carved_out_before_a_prefix_of_it() {
let segs = table(&[("<s>", 1), ("<s>x", 2)]).split("<s>x", SpecialTokens::Parse);
assert_eq!(segs.len(), 1);
assert!(matches!(segs[0], TextOrSpecial::Special(2)));
}
#[test]
fn no_match_at_all_returns_the_whole_text_as_one_segment() {
let segs = table(&[("<|zzz|>", 5)]).split("nothing here", SpecialTokens::Parse);
assert_eq!(segs.len(), 1);
assert_eq!(text_of(&segs[0]), Some("nothing here"));
}
#[test]
fn as_text_leaves_control_and_unknown_markers_as_prose_but_still_parses_user_defined() {
let t = SpecialTokenTable::from_entries(vec![
("<|im_end|>", 7, SpecialKind::Control),
("<unk>", 0, SpecialKind::Unknown),
("<|user|>", 9, SpecialKind::UserDefined),
]);
let segs = t.split("a<|im_end|>b<unk>c<|user|>d", SpecialTokens::AsText);
let texts: Vec<&str> = segs.iter().filter_map(text_of).collect();
assert_eq!(texts, vec!["a<|im_end|>b<unk>c", "d"]);
assert!(matches!(segs[1], TextOrSpecial::Special(9)));
let segs = t.split("a<|im_end|>b<unk>c<|user|>d", SpecialTokens::Parse);
let ids: Vec<u32> = segs
.iter()
.filter_map(|s| match s {
TextOrSpecial::Special(id) => Some(*id),
_ => None,
})
.collect();
assert_eq!(ids, vec![7, 0, 9]);
}
struct MetaOnly(std::collections::HashMap<String, GgufValue>);
impl TensorSource for MetaOnly {
fn metadata(&self, key: &str) -> Option<&GgufValue> {
self.0.get(key)
}
fn find_tensor(&self, _name: &str) -> Option<&TensorInfo> {
None
}
fn tensor_bytes(&self, name: &str) -> Result<&[u8], GgufError> {
Err(GgufError::TensorNotFound(name.to_string()))
}
fn tensor_mapped_range(
&self,
name: &str,
) -> Result<
(
std::sync::Arc<ferrox_gguf::MmapHandle>,
std::ops::Range<usize>,
),
GgufError,
> {
Err(GgufError::TensorNotFound(name.to_string()))
}
}
fn vocab(tokens: &[(&str, i32)]) -> (MetaOnly, Vec<String>) {
let mut m = std::collections::HashMap::new();
m.insert(
"tokenizer.ggml.token_type".to_string(),
GgufValue::Array(tokens.iter().map(|&(_, ty)| GgufValue::I32(ty)).collect()),
);
let id_to_token = tokens.iter().map(|&(t, _)| t.to_string()).collect();
(MetaOnly(m), id_to_token)
}
#[test]
fn a_normal_typed_entry_shaped_like_a_marker_is_not_special() {
let (file, ids) = vocab(&[("<", 1), ("s", 1), (">", 1), ("<s>", 1), ("<|im_end|>", 3)]);
let t = SpecialTokenTable::from_gguf(&file, &ids);
let segs = t.split("<s><|im_end|>", SpecialTokens::Parse);
let texts: Vec<&str> = segs.iter().filter_map(text_of).collect();
assert_eq!(texts, vec!["<s>"]);
assert!(matches!(segs[1], TextOrSpecial::Special(4)));
}
#[test]
fn an_end_of_generation_text_is_control_even_when_the_file_says_normal() {
let (file, ids) = vocab(&[("<|im_start|>", 1), ("<|im_end|>", 1)]);
let t = SpecialTokenTable::from_gguf(&file, &ids);
assert_eq!(
t.entries,
vec![SpecialToken {
text: "<|im_end|>".to_string(),
id: 1,
kind: SpecialKind::Control
}]
);
}
#[test]
fn user_defined_and_unknown_types_are_special_and_normal_byte_and_unused_are_not() {
let (file, ids) = vocab(&[
("<unk>", 2),
("<ctl>", 3),
("<usr>", 4),
("<unused>", 5),
("<0x00>", 6),
("word", 1),
]);
let t = SpecialTokenTable::from_gguf(&file, &ids);
let kinds: Vec<(u32, SpecialKind)> = t.entries.iter().map(|e| (e.id, e.kind)).collect();
assert_eq!(
kinds,
vec![
(0, SpecialKind::Unknown),
(1, SpecialKind::Control),
(2, SpecialKind::UserDefined)
]
);
}
#[test]
fn gemma4_style_end_of_sentence_is_demoted_beside_tool_response() {
let (file, ids) = vocab(&[("</s>", 3), ("<|tool_response>", 3)]);
let t = SpecialTokenTable::from_gguf(&file, &ids);
assert_eq!(t.entries.iter().map(|e| e.id).collect::<Vec<_>>(), vec![1]);
}
#[test]
fn harmony_end_is_user_defined_when_return_and_call_are_present() {
let (file, ids) = vocab(&[("<|end|>", 3), ("<|return|>", 3), ("<|call|>", 3)]);
let t = SpecialTokenTable::from_gguf(&file, &ids);
let end = t.entries.iter().find(|e| e.text == "<|end|>").unwrap();
assert_eq!(end.kind, SpecialKind::UserDefined);
let segs = t.split("x<|end|>y", SpecialTokens::AsText);
assert!(matches!(segs[1], TextOrSpecial::Special(0)));
}
#[test]
fn a_file_without_token_types_has_only_the_by_name_specials() {
let file = MetaOnly(std::collections::HashMap::new());
let ids: Vec<String> = ["a", "<|eot_id|>", "b"]
.iter()
.map(|s| s.to_string())
.collect();
let t = SpecialTokenTable::from_gguf(&file, &ids);
assert_eq!(t.entries.iter().map(|e| e.id).collect::<Vec<_>>(), vec![1]);
}
}