1use super::EOG_TOKEN_TEXTS;
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum SpecialTokens {
57 AsText,
61 Parse,
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub(crate) enum SpecialKind {
70 Control,
72 UserDefined,
74 Unknown,
76}
77
78impl SpecialKind {
79 fn is_parsed(self, mode: SpecialTokens) -> bool {
82 match mode {
83 SpecialTokens::Parse => true,
84 SpecialTokens::AsText => self == SpecialKind::UserDefined,
85 }
86 }
87}
88
89const GGML_TOKEN_TYPE_UNKNOWN: i64 = 2;
96const GGML_TOKEN_TYPE_CONTROL: i64 = 3;
97const GGML_TOKEN_TYPE_USER_DEFINED: i64 = 4;
98
99#[derive(Clone, Debug, PartialEq, Eq)]
100pub(crate) struct SpecialToken {
101 pub text: String,
102 pub id: u32,
103 pub kind: SpecialKind,
104}
105
106pub(crate) enum TextOrSpecial<'a> {
109 Text(&'a str),
110 Special(u32),
111}
112
113#[derive(Clone, Debug, Default, PartialEq, Eq)]
116pub(crate) struct SpecialTokenTable {
117 entries: Vec<SpecialToken>,
120}
121
122impl SpecialTokenTable {
123 pub fn from_gguf(file: &impl ferrox_gguf::TensorSource, id_to_token: &[String]) -> Self {
126 let mut kinds: Vec<Option<SpecialKind>> = vec![None; id_to_token.len()];
127
128 if let Some(ferrox_gguf::GgufValue::Array(items)) =
129 file.metadata("tokenizer.ggml.token_type")
130 {
131 for (kind, v) in kinds.iter_mut().zip(items) {
132 let ty = match v {
133 ferrox_gguf::GgufValue::I32(t) => *t as i64,
134 ferrox_gguf::GgufValue::U32(t) => *t as i64,
135 _ => continue,
136 };
137 *kind = match ty {
138 GGML_TOKEN_TYPE_CONTROL => Some(SpecialKind::Control),
139 GGML_TOKEN_TYPE_USER_DEFINED => Some(SpecialKind::UserDefined),
140 GGML_TOKEN_TYPE_UNKNOWN => Some(SpecialKind::Unknown),
141 _ => None,
142 };
143 }
144 }
145
146 for (id, text) in id_to_token.iter().enumerate() {
152 if EOG_TOKEN_TEXTS.contains(&text.as_str()) {
153 kinds[id] = Some(SpecialKind::Control);
154 }
155 }
156
157 let has = |t: &str| id_to_token.iter().any(|x| x == t);
160 if has("<|end|>")
161 && ((has("<|return|>") && has("<|call|>")) || (has("<|calls|>") && has("<|flush|>")))
162 {
163 for (id, text) in id_to_token.iter().enumerate() {
164 if text == "<|end|>" {
165 kinds[id] = Some(SpecialKind::UserDefined);
166 }
167 }
168 }
169 if has("<|tool_response>") && has("</s>") {
172 for (id, text) in id_to_token.iter().enumerate() {
173 if text == "</s>" {
174 kinds[id] = None;
175 }
176 }
177 }
178
179 Self::from_entries(
180 kinds
181 .into_iter()
182 .enumerate()
183 .filter_map(|(id, kind)| Some((id_to_token[id].as_str(), id as u32, kind?))),
184 )
185 }
186
187 pub fn from_entries<'a>(
192 entries: impl IntoIterator<Item = (&'a str, u32, SpecialKind)>,
193 ) -> Self {
194 let mut entries: Vec<SpecialToken> = entries
195 .into_iter()
196 .filter(|(text, _, _)| !text.is_empty())
198 .map(|(text, id, kind)| SpecialToken {
199 text: text.to_string(),
200 id,
201 kind,
202 })
203 .collect();
204 entries.sort_by_key(|e| std::cmp::Reverse(e.text.len()));
206 SpecialTokenTable { entries }
207 }
208
209 pub fn split<'a>(&self, text: &'a str, mode: SpecialTokens) -> Vec<TextOrSpecial<'a>> {
218 let mut fragments = vec![TextOrSpecial::Text(text)];
219 for special in self.entries.iter().filter(|s| s.kind.is_parsed(mode)) {
220 let mut next = Vec::with_capacity(fragments.len());
221 for fragment in fragments {
222 match fragment {
223 TextOrSpecial::Special(id) => next.push(TextOrSpecial::Special(id)),
224 TextOrSpecial::Text(run) => {
225 let mut rest = run;
226 while let Some(at) = rest.find(special.text.as_str()) {
227 if at > 0 {
228 next.push(TextOrSpecial::Text(&rest[..at]));
229 }
230 next.push(TextOrSpecial::Special(special.id));
231 rest = &rest[at + special.text.len()..];
232 }
233 if !rest.is_empty() {
234 next.push(TextOrSpecial::Text(rest));
235 }
236 }
237 }
238 }
239 fragments = next;
240 }
241 fragments
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use ferrox_gguf::{GgufError, GgufValue, TensorInfo, TensorSource};
249
250 fn text_of<'a>(seg: &TextOrSpecial<'a>) -> Option<&'a str> {
251 match seg {
252 TextOrSpecial::Text(t) => Some(t),
253 TextOrSpecial::Special(_) => None,
254 }
255 }
256
257 fn table(specials: &[(&str, u32)]) -> SpecialTokenTable {
258 SpecialTokenTable::from_entries(
259 specials
260 .iter()
261 .map(|&(t, id)| (t, id, SpecialKind::Control)),
262 )
263 }
264
265 #[test]
266 fn an_empty_table_returns_the_whole_text_unsplit() {
267 let segs = table(&[]).split("hello world", SpecialTokens::Parse);
268 assert_eq!(segs.len(), 1);
269 assert_eq!(text_of(&segs[0]), Some("hello world"));
270 }
271
272 #[test]
273 fn splits_around_a_single_special_token_in_the_middle() {
274 let segs = table(&[("<|sep|>", 99)]).split("before<|sep|>after", SpecialTokens::Parse);
275 assert_eq!(segs.len(), 3);
276 assert_eq!(text_of(&segs[0]), Some("before"));
277 assert!(matches!(segs[1], TextOrSpecial::Special(99)));
278 assert_eq!(text_of(&segs[2]), Some("after"));
279 }
280
281 #[test]
282 fn multiple_occurrences_and_multiple_distinct_specials_all_split() {
283 let segs = table(&[("<a>", 1), ("<b>", 2)]).split("<a>x<b>y<a>", SpecialTokens::Parse);
284 let ids: Vec<u32> = segs
285 .iter()
286 .filter_map(|s| match s {
287 TextOrSpecial::Special(id) => Some(*id),
288 _ => None,
289 })
290 .collect();
291 assert_eq!(ids, vec![1, 2, 1]);
292 let texts: Vec<&str> = segs.iter().filter_map(text_of).collect();
293 assert_eq!(texts, vec!["x", "y"]);
294 }
295
296 #[test]
299 fn the_longest_special_is_carved_out_before_a_prefix_of_it() {
300 let segs = table(&[("<s>", 1), ("<s>x", 2)]).split("<s>x", SpecialTokens::Parse);
301 assert_eq!(segs.len(), 1);
302 assert!(matches!(segs[0], TextOrSpecial::Special(2)));
303 }
304
305 #[test]
306 fn no_match_at_all_returns_the_whole_text_as_one_segment() {
307 let segs = table(&[("<|zzz|>", 5)]).split("nothing here", SpecialTokens::Parse);
308 assert_eq!(segs.len(), 1);
309 assert_eq!(text_of(&segs[0]), Some("nothing here"));
310 }
311
312 #[test]
316 fn as_text_leaves_control_and_unknown_markers_as_prose_but_still_parses_user_defined() {
317 let t = SpecialTokenTable::from_entries(vec![
318 ("<|im_end|>", 7, SpecialKind::Control),
319 ("<unk>", 0, SpecialKind::Unknown),
320 ("<|user|>", 9, SpecialKind::UserDefined),
321 ]);
322 let segs = t.split("a<|im_end|>b<unk>c<|user|>d", SpecialTokens::AsText);
323 let texts: Vec<&str> = segs.iter().filter_map(text_of).collect();
324 assert_eq!(texts, vec!["a<|im_end|>b<unk>c", "d"]);
325 assert!(matches!(segs[1], TextOrSpecial::Special(9)));
326
327 let segs = t.split("a<|im_end|>b<unk>c<|user|>d", SpecialTokens::Parse);
328 let ids: Vec<u32> = segs
329 .iter()
330 .filter_map(|s| match s {
331 TextOrSpecial::Special(id) => Some(*id),
332 _ => None,
333 })
334 .collect();
335 assert_eq!(ids, vec![7, 0, 9]);
336 }
337
338 struct MetaOnly(std::collections::HashMap<String, GgufValue>);
339
340 impl TensorSource for MetaOnly {
341 fn metadata(&self, key: &str) -> Option<&GgufValue> {
342 self.0.get(key)
343 }
344 fn find_tensor(&self, _name: &str) -> Option<&TensorInfo> {
345 None
346 }
347 fn tensor_bytes(&self, name: &str) -> Result<&[u8], GgufError> {
348 Err(GgufError::TensorNotFound(name.to_string()))
349 }
350 fn tensor_mapped_range(
351 &self,
352 name: &str,
353 ) -> Result<
354 (
355 std::sync::Arc<ferrox_gguf::MmapHandle>,
356 std::ops::Range<usize>,
357 ),
358 GgufError,
359 > {
360 Err(GgufError::TensorNotFound(name.to_string()))
361 }
362 }
363
364 fn vocab(tokens: &[(&str, i32)]) -> (MetaOnly, Vec<String>) {
365 let mut m = std::collections::HashMap::new();
366 m.insert(
367 "tokenizer.ggml.token_type".to_string(),
368 GgufValue::Array(tokens.iter().map(|&(_, ty)| GgufValue::I32(ty)).collect()),
369 );
370 let id_to_token = tokens.iter().map(|&(t, _)| t.to_string()).collect();
371 (MetaOnly(m), id_to_token)
372 }
373
374 #[test]
379 fn a_normal_typed_entry_shaped_like_a_marker_is_not_special() {
380 let (file, ids) = vocab(&[("<", 1), ("s", 1), (">", 1), ("<s>", 1), ("<|im_end|>", 3)]);
381 let t = SpecialTokenTable::from_gguf(&file, &ids);
382 let segs = t.split("<s><|im_end|>", SpecialTokens::Parse);
383 let texts: Vec<&str> = segs.iter().filter_map(text_of).collect();
384 assert_eq!(texts, vec!["<s>"]);
385 assert!(matches!(segs[1], TextOrSpecial::Special(4)));
386 }
387
388 #[test]
392 fn an_end_of_generation_text_is_control_even_when_the_file_says_normal() {
393 let (file, ids) = vocab(&[("<|im_start|>", 1), ("<|im_end|>", 1)]);
394 let t = SpecialTokenTable::from_gguf(&file, &ids);
395 assert_eq!(
396 t.entries,
397 vec![SpecialToken {
398 text: "<|im_end|>".to_string(),
399 id: 1,
400 kind: SpecialKind::Control
401 }]
402 );
403 }
404
405 #[test]
406 fn user_defined_and_unknown_types_are_special_and_normal_byte_and_unused_are_not() {
407 let (file, ids) = vocab(&[
408 ("<unk>", 2),
409 ("<ctl>", 3),
410 ("<usr>", 4),
411 ("<unused>", 5),
412 ("<0x00>", 6),
413 ("word", 1),
414 ]);
415 let t = SpecialTokenTable::from_gguf(&file, &ids);
416 let kinds: Vec<(u32, SpecialKind)> = t.entries.iter().map(|e| (e.id, e.kind)).collect();
417 assert_eq!(
418 kinds,
419 vec![
420 (0, SpecialKind::Unknown),
421 (1, SpecialKind::Control),
422 (2, SpecialKind::UserDefined)
423 ]
424 );
425 }
426
427 #[test]
430 fn gemma4_style_end_of_sentence_is_demoted_beside_tool_response() {
431 let (file, ids) = vocab(&[("</s>", 3), ("<|tool_response>", 3)]);
432 let t = SpecialTokenTable::from_gguf(&file, &ids);
433 assert_eq!(t.entries.iter().map(|e| e.id).collect::<Vec<_>>(), vec![1]);
434 }
435
436 #[test]
439 fn harmony_end_is_user_defined_when_return_and_call_are_present() {
440 let (file, ids) = vocab(&[("<|end|>", 3), ("<|return|>", 3), ("<|call|>", 3)]);
441 let t = SpecialTokenTable::from_gguf(&file, &ids);
442 let end = t.entries.iter().find(|e| e.text == "<|end|>").unwrap();
443 assert_eq!(end.kind, SpecialKind::UserDefined);
444 let segs = t.split("x<|end|>y", SpecialTokens::AsText);
445 assert!(matches!(segs[1], TextOrSpecial::Special(0)));
446 }
447
448 #[test]
449 fn a_file_without_token_types_has_only_the_by_name_specials() {
450 let file = MetaOnly(std::collections::HashMap::new());
451 let ids: Vec<String> = ["a", "<|eot_id|>", "b"]
452 .iter()
453 .map(|s| s.to_string())
454 .collect();
455 let t = SpecialTokenTable::from_gguf(&file, &ids);
456 assert_eq!(t.entries.iter().map(|e| e.id).collect::<Vec<_>>(), vec![1]);
457 }
458}