use rustc_hash::{FxHashMap, FxHashSet};
use serde_json::Value;
use thiserror::Error;
use super::hf_json::components::{find_added_token, parse_special_tokens};
#[derive(Debug, Error)]
pub enum PolicyError {
#[error("post_processor Sequence composes several segment-placing processors ({0}) — refusing to guess where the second sequence goes")]
UnsupportedPairComposition(String),
#[error("this tokenizer defines no pair template — refusing to concatenate the two sequences without the separator the model expects")]
NoPairTemplate,
#[error("special token {token:?} at byte offset {offset} is not in the caller's allow-list")]
DisallowedSpecial { token: String, offset: usize },
}
#[derive(Debug, Clone, Copy)]
pub enum SpecialMode<'a> {
All,
Ordinary,
Allow(&'a FxHashSet<String>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpecialDecode {
Skip,
Render,
}
pub(super) const EOS_CANDIDATES: &[&str] =
&["</s>", "<eos>", "<|endoftext|>", "<|end_of_text|>", "[SEP]"];
#[derive(Debug, Clone, PartialEq, Eq)]
enum Segment {
Special(u32),
A,
B,
}
#[derive(Debug, Clone)]
enum PairTemplate {
Defined(Vec<Segment>),
Absent,
Ambiguous(String),
}
#[derive(Debug, Clone)]
struct Template {
single: Vec<Segment>,
pair: PairTemplate,
}
impl Default for Template {
fn default() -> Self {
Self {
single: vec![Segment::A],
pair: PairTemplate::Absent,
}
}
}
impl Template {
fn contributes(&self) -> bool {
self.single != [Segment::A] || matches!(self.pair, PairTemplate::Defined(_))
}
}
#[derive(Debug, Clone, Default)]
pub struct SpecialPolicy {
template: Template,
eos_id: Option<u32>,
named: FxHashMap<String, u32>,
}
impl SpecialPolicy {
pub fn apply_single(&self, ids: Vec<u32>) -> Vec<u32> {
if self.template.single == [Segment::A] {
return ids;
}
let mut out = Vec::with_capacity(ids.len() + self.template.single.len());
render(&self.template.single, &ids, &[], &mut out);
out
}
pub fn apply_pair(&self, a: &[u32], b: &[u32]) -> Result<Vec<u32>, PolicyError> {
let pair = match &self.template.pair {
PairTemplate::Defined(pair) => pair,
PairTemplate::Absent => return Err(PolicyError::NoPairTemplate),
PairTemplate::Ambiguous(names) => {
return Err(PolicyError::UnsupportedPairComposition(names.clone()))
}
};
let mut out = Vec::with_capacity(a.len() + b.len() + pair.len());
render(pair, a, b, &mut out);
Ok(out)
}
pub fn eos_token_id(&self) -> Option<u32> {
self.eos_id
}
pub fn is_eos(&self, id: u32) -> bool {
self.eos_id == Some(id)
}
pub fn special_token_id(&self, name: &str) -> Option<u32> {
self.named.get(name).copied()
}
pub fn special_tokens(&self) -> &FxHashMap<String, u32> {
&self.named
}
pub(super) fn boundary(
bos: Option<u32>,
eos: Option<u32>,
eos_id: Option<u32>,
named: FxHashMap<String, u32>,
) -> Self {
let mut single = Vec::with_capacity(3);
single.extend(bos.map(Segment::Special));
single.push(Segment::A);
single.extend(eos.map(Segment::Special));
Self {
template: Template {
single,
pair: PairTemplate::Absent,
},
eos_id,
named,
}
}
pub(super) fn cls_sep(
cls: u32,
sep: u32,
eos_id: Option<u32>,
named: FxHashMap<String, u32>,
) -> Self {
Self {
template: cls_sep_segments(Some(cls), Some(sep), PairShape::Bert),
eos_id,
named,
}
}
pub fn single_overhead(&self) -> usize {
self.template
.single
.iter()
.filter(|s| matches!(s, Segment::Special(_)))
.count()
}
}
fn render(segments: &[Segment], a: &[u32], b: &[u32], out: &mut Vec<u32>) {
for segment in segments {
match segment {
Segment::Special(id) => out.push(*id),
Segment::A => out.extend_from_slice(a),
Segment::B => out.extend_from_slice(b),
}
}
}
pub(super) fn parse(root: &Value) -> Result<SpecialPolicy, PolicyError> {
Ok(SpecialPolicy {
template: parse_template(root.get("post_processor"))?,
eos_id: find_added_token(root, EOS_CANDIDATES),
named: parse_special_tokens(root).into_id_map(),
})
}
fn parse_template(pp: Option<&Value>) -> Result<Template, PolicyError> {
let Some(pp) = pp else {
return Ok(Template::default());
};
match pp.get("type").and_then(Value::as_str) {
Some("BertProcessing") => Ok(cls_sep_template(pp, PairShape::Bert)),
Some("RobertaProcessing") => Ok(cls_sep_template(pp, PairShape::Roberta)),
Some("TemplateProcessing") => Ok(parse_template_processing(pp)),
Some("Sequence") => parse_sequence(pp),
_ => Ok(Template::default()),
}
}
enum PairShape {
Bert,
Roberta,
}
fn cls_sep_template(pp: &Value, shape: PairShape) -> Template {
let id = |k: &str| {
pp.get(k)
.and_then(|p| p.get(1))
.and_then(Value::as_u64)
.map(|n| n as u32)
};
cls_sep_segments(id("cls"), id("sep"), shape)
}
fn cls_sep_segments(cls: Option<u32>, sep: Option<u32>, shape: PairShape) -> Template {
let mut single = Vec::with_capacity(3);
single.extend(cls.map(Segment::Special));
single.push(Segment::A);
single.extend(sep.map(Segment::Special));
let pair = match (cls, sep) {
(Some(cls), Some(sep)) => PairTemplate::Defined(match shape {
PairShape::Bert => vec![
Segment::Special(cls),
Segment::A,
Segment::Special(sep),
Segment::B,
Segment::Special(sep),
],
PairShape::Roberta => vec![
Segment::Special(cls),
Segment::A,
Segment::Special(sep),
Segment::Special(sep),
Segment::B,
Segment::Special(sep),
],
}),
_ => PairTemplate::Absent,
};
Template { single, pair }
}
fn parse_template_processing(pp: &Value) -> Template {
let resolve = |tok: &str| -> Option<u32> {
pp.get("special_tokens")
.and_then(|m| m.get(tok))
.and_then(|e| e.get("ids"))
.and_then(Value::as_array)
.and_then(|a| a.first())
.and_then(Value::as_u64)
.map(|n| n as u32)
};
let segments = |key: &str| -> Option<Vec<Segment>> {
let items = pp.get(key).and_then(Value::as_array)?;
let mut out = Vec::with_capacity(items.len());
for item in items {
if let Some(seq) = item.get("Sequence") {
match seq.get("id").and_then(Value::as_str) {
Some("B") => out.push(Segment::B),
_ => out.push(Segment::A),
}
} else if let Some(id) = item
.get("SpecialToken")
.and_then(|s| s.get("id"))
.and_then(Value::as_str)
.and_then(&resolve)
{
out.push(Segment::Special(id));
}
}
Some(out)
};
Template {
single: segments("single").unwrap_or_else(|| vec![Segment::A]),
pair: match segments("pair") {
Some(segs) => PairTemplate::Defined(segs),
None => PairTemplate::Absent,
},
}
}
fn parse_sequence(pp: &Value) -> Result<Template, PolicyError> {
let mut single = vec![Segment::A];
let mut pair = PairTemplate::Absent;
let mut contributors: Vec<String> = Vec::new();
if let Some(list) = pp.get("processors").and_then(Value::as_array) {
for sub in list {
let t = parse_template(Some(sub))?;
if t.contributes() {
contributors.push(
sub.get("type")
.and_then(Value::as_str)
.unwrap_or("<untyped>")
.to_string(),
);
pair = t.pair.clone();
}
single = substitute(&single, &t.single);
}
}
if contributors.len() > 1 {
pair = PairTemplate::Ambiguous(contributors.join(", "));
}
Ok(Template { single, pair })
}
fn substitute(outer: &[Segment], inner: &[Segment]) -> Vec<Segment> {
let mut out = Vec::with_capacity(outer.len() + inner.len());
for segment in outer {
match segment {
Segment::A => out.extend_from_slice(inner),
other => out.push(other.clone()),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn policy(json: &str) -> Result<SpecialPolicy, PolicyError> {
let root: Value = serde_json::from_str(json).expect("valid json");
parse(&root)
}
#[test]
fn no_post_processor_leaves_content_untouched() {
let p = policy("{}").expect("parses");
assert_eq!(p.apply_single(vec![7, 8]), vec![7, 8]);
assert!(matches!(
p.apply_pair(&[7], &[8]),
Err(PolicyError::NoPairTemplate)
));
}
#[test]
fn bert_processing_synthesizes_both_templates() {
let p = policy(
r#"{"post_processor": {"type": "BertProcessing",
"cls": ["[CLS]", 1], "sep": ["[SEP]", 2]}}"#,
)
.expect("parses");
assert_eq!(p.apply_single(vec![3]), vec![1, 3, 2]);
assert_eq!(p.apply_pair(&[3], &[4]).expect("pair"), vec![1, 3, 2, 4, 2]);
}
#[test]
fn roberta_processing_doubles_the_separator() {
let p = policy(
r#"{"post_processor": {"type": "RobertaProcessing",
"cls": ["<s>", 0], "sep": ["</s>", 2]}}"#,
)
.expect("parses");
assert_eq!(p.apply_single(vec![5]), vec![0, 5, 2]);
assert_eq!(
p.apply_pair(&[5], &[6]).expect("pair"),
vec![0, 5, 2, 2, 6, 2]
);
}
#[test]
fn template_processing_reads_the_pair_array() {
let p = policy(
r#"{"post_processor": {"type": "TemplateProcessing",
"single": [
{"SpecialToken": {"id": "<s>", "type_id": 0}},
{"Sequence": {"id": "A", "type_id": 0}}
],
"pair": [
{"SpecialToken": {"id": "<s>", "type_id": 0}},
{"Sequence": {"id": "A", "type_id": 0}},
{"SpecialToken": {"id": "</s>", "type_id": 0}},
{"Sequence": {"id": "B", "type_id": 1}}
],
"special_tokens": {
"<s>": {"id": "<s>", "ids": [1], "tokens": ["<s>"]},
"</s>": {"id": "</s>", "ids": [2], "tokens": ["</s>"]}
}}}"#,
)
.expect("parses");
assert_eq!(p.apply_single(vec![9]), vec![1, 9]);
assert_eq!(p.apply_pair(&[9], &[10]).expect("pair"), vec![1, 9, 2, 10]);
}
#[test]
fn sequence_with_one_contributor_composes() {
let p = policy(
r#"{"post_processor": {"type": "Sequence", "processors": [
{"type": "ByteLevel", "add_prefix_space": true},
{"type": "BertProcessing", "cls": ["[CLS]", 1], "sep": ["[SEP]", 2]}
]}}"#,
)
.expect("parses");
assert_eq!(p.apply_single(vec![3]), vec![1, 3, 2]);
assert_eq!(p.apply_pair(&[3], &[4]).expect("pair"), vec![1, 3, 2, 4, 2]);
}
#[test]
fn sequence_with_two_contributors_loads_and_refuses_only_on_pair() {
let p = policy(
r#"{"post_processor": {"type": "Sequence", "processors": [
{"type": "BertProcessing", "cls": ["[CLS]", 1], "sep": ["[SEP]", 2]},
{"type": "RobertaProcessing", "cls": ["<s>", 3], "sep": ["</s>", 4]}
]}}"#,
)
.expect("parses despite pair ambiguity");
assert_eq!(p.apply_single(vec![99]), vec![1, 3, 99, 4, 2]);
assert!(matches!(
p.apply_pair(&[99], &[100]),
Err(PolicyError::UnsupportedPairComposition(_))
));
}
#[test]
fn eos_and_named_tokens_come_from_added_tokens() {
let p = policy(
r#"{"added_tokens": [
{"id": 1, "content": "[CLS]", "special": true},
{"id": 2, "content": "[SEP]", "special": true}
]}"#,
)
.expect("parses");
assert_eq!(p.special_token_id("[CLS]"), Some(1));
assert_eq!(p.eos_token_id(), Some(2));
assert!(p.is_eos(2));
assert!(!p.is_eos(1));
}
#[test]
fn cls_sep_from_ids_matches_the_json_path() {
let from_json = policy(
r#"{"post_processor": {"type": "BertProcessing",
"cls": ["[CLS]", 101], "sep": ["[SEP]", 102]}}"#,
)
.expect("parses");
let from_ids = SpecialPolicy::cls_sep(101, 102, Some(102), FxHashMap::default());
assert_eq!(
from_ids.apply_single(vec![7592, 2088]),
vec![101, 7592, 2088, 102]
);
assert_eq!(
from_json.apply_single(vec![7592, 2088]),
from_ids.apply_single(vec![7592, 2088])
);
assert_eq!(
from_json.apply_pair(&[7592], &[9119]).expect("pair"),
from_ids.apply_pair(&[7592], &[9119]).expect("pair")
);
}
#[test]
fn single_overhead_counts_only_the_special_slots() {
assert_eq!(SpecialPolicy::default().single_overhead(), 0);
let bos_only = SpecialPolicy::boundary(Some(1), None, None, FxHashMap::default());
assert_eq!(bos_only.single_overhead(), 1);
assert_eq!(
SpecialPolicy::cls_sep(101, 102, None, FxHashMap::default()).single_overhead(),
2
);
}
#[test]
fn absent_eos_is_none() {
let p = policy(r#"{"added_tokens": [{"id": 0, "content": "<unk>"}]}"#).expect("parses");
assert_eq!(p.eos_token_id(), None);
assert!(!p.is_eos(0));
}
}