use std::collections::HashSet;
use crate::generate::{earliest_stop_match, floor_char_boundary};
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum StopStep {
Emit(String),
Matched { text: String, stop: String },
}
pub(crate) fn resolve_stop_tokens(
stops: &[String],
encode: impl Fn(&str) -> Vec<usize>,
) -> Vec<usize> {
let mut ids = Vec::new();
for stop in stops {
if stop.is_empty() {
continue;
}
let encoded = encode(stop);
if encoded.len() == 1 && !ids.contains(&encoded[0]) {
ids.push(encoded[0]);
}
}
ids
}
pub(crate) struct StopMatcher {
stops: Vec<String>,
stop_tokens: HashSet<usize>,
pending: String,
}
impl StopMatcher {
pub(crate) fn new(stops: &[String], stop_tokens: &[usize]) -> Self {
StopMatcher {
stops: stops.iter().filter(|s| !s.is_empty()).cloned().collect(),
stop_tokens: stop_tokens.iter().copied().collect(),
pending: String::new(),
}
}
pub(crate) fn is_stop_token(&self, token: usize) -> bool {
self.stop_tokens.contains(&token)
}
pub(crate) fn push(&mut self, piece: &str) -> StopStep {
if self.stops.is_empty() {
return StopStep::Emit(piece.to_string());
}
self.pending.push_str(piece);
if let Some((cut, stop)) = earliest_stop_match(&self.pending, &self.stops) {
let matched = StopStep::Matched {
text: self.pending[..cut].to_string(),
stop: stop.to_string(),
};
self.pending.clear();
return matched;
}
let keep = partial_suffix_len(&self.pending, &self.stops);
let split = floor_char_boundary(&self.pending, self.pending.len() - keep);
let out: String = self.pending.drain(..split).collect();
StopStep::Emit(out)
}
pub(crate) fn flush(&mut self) -> String {
std::mem::take(&mut self.pending)
}
}
fn partial_suffix_len(pending: &str, stops: &[String]) -> usize {
ferrox_edge::stop_prefix_holdback(pending, stops)
}
#[cfg(test)]
mod tests {
use super::*;
fn stops(list: &[&str]) -> Vec<String> {
list.iter().map(|s| s.to_string()).collect()
}
fn matcher(list: &[&str]) -> StopMatcher {
StopMatcher::new(&stops(list), &[])
}
#[test]
fn a_partial_match_is_never_emitted() {
let mut m = matcher(&["</tool_call>"]);
assert_eq!(m.push("answer "), StopStep::Emit("answer ".into()));
for piece in ["<", "/", "tool", "_ca"] {
assert_eq!(
m.push(piece),
StopStep::Emit(String::new()),
"a growing partial match escaped at {piece:?}"
);
}
assert_eq!(
m.push("ll>"),
StopStep::Matched {
text: String::new(),
stop: "</tool_call>".into()
}
);
}
#[test]
fn text_that_cannot_match_is_not_withheld() {
let mut m = matcher(&["<|im_end|>"]);
assert_eq!(m.push("hello"), StopStep::Emit("hello".into()));
assert_eq!(m.push(" world"), StopStep::Emit(" world".into()));
assert_eq!(
m.flush(),
"",
"nothing should still be held when nothing could match"
);
}
#[test]
fn an_abandoned_partial_match_is_released_immediately() {
let mut m = matcher(&["<|im_end|>"]);
assert_eq!(m.push("a<|im"), StopStep::Emit("a".into()));
assert_eq!(m.push("_x"), StopStep::Emit("<|im_x".into()));
assert_eq!(m.flush(), "");
}
#[test]
fn a_stop_in_the_middle_of_a_piece_cuts_it_there() {
let mut m = matcher(&["STOP"]);
assert_eq!(
m.push("keep thisSTOPdrop this"),
StopStep::Matched {
text: "keep this".into(),
stop: "STOP".into()
}
);
assert_eq!(m.flush(), "", "everything after the stop is gone");
}
#[test]
fn the_leftmost_stop_wins_when_several_could_match() {
let mut m = matcher(&["world", "hello"]);
assert_eq!(
m.push("say hello world"),
StopStep::Matched {
text: "say ".into(),
stop: "hello".into()
}
);
}
#[test]
fn a_pending_partial_is_released_when_generation_ends_otherwise() {
let mut m = matcher(&["<|im_end|>"]);
assert_eq!(m.push("done<|im"), StopStep::Emit("done".into()));
assert_eq!(m.flush(), "<|im");
assert_eq!(m.flush(), "", "flushing twice must not duplicate it");
}
#[test]
fn no_stop_sequences_means_no_buffering_at_all() {
let mut m = matcher(&[]);
assert_eq!(m.push("<|im"), StopStep::Emit("<|im".into()));
assert_eq!(m.flush(), "");
}
#[test]
fn an_empty_stop_string_is_ignored() {
let mut m = matcher(&[""]);
assert_eq!(m.push("hello"), StopStep::Emit("hello".into()));
}
#[test]
fn multibyte_text_is_never_cut_mid_character() {
let mut m = matcher(&["éx"]);
assert_eq!(m.push("aé"), StopStep::Emit("a".into()));
assert_eq!(m.push("y"), StopStep::Emit("éy".into()));
let mut m = matcher(&["ありがとう"]);
assert_eq!(m.push("あり"), StopStep::Emit(String::new()));
assert_eq!(
m.push("がとう"),
StopStep::Matched {
text: String::new(),
stop: "ありがとう".into()
}
);
}
#[test]
fn partial_suffix_length_is_the_longest_real_partial() {
let s = stops(&["abc"]);
assert_eq!(partial_suffix_len("xxab", &s), 2);
assert_eq!(partial_suffix_len("xxa", &s), 1);
assert_eq!(partial_suffix_len("xxb", &s), 0);
assert_eq!(partial_suffix_len("abc", &s), 0);
let s = stops(&["ab", "abcd"]);
assert_eq!(partial_suffix_len("xabc", &s), 3);
}
#[test]
fn a_single_token_stop_string_becomes_a_stop_token() {
let encode = |text: &str| match text {
"<|im_end|>" => vec![100usize],
"hello world" => vec![1, 2, 3],
_ => vec![],
};
let ids = resolve_stop_tokens(&stops(&["<|im_end|>", "hello world"]), encode);
assert_eq!(
ids,
vec![100],
"only the single-token stop is a token-level stop"
);
}
#[test]
fn duplicate_and_empty_stop_strings_do_not_duplicate_ids() {
let encode = |text: &str| match text {
"<|end|>" => vec![7usize],
_ => vec![],
};
let ids = resolve_stop_tokens(&stops(&["<|end|>", "<|end|>", ""]), encode);
assert_eq!(ids, vec![7]);
}
#[test]
fn a_stop_token_is_matched_by_id_not_by_how_it_renders() {
let m = StopMatcher::new(&stops(&["<|im_end|>"]), &[100]);
assert!(m.is_stop_token(100));
assert!(!m.is_stop_token(101));
let mut m = StopMatcher::new(&stops(&["<|im_end|>"]), &[100]);
assert_eq!(m.push(""), StopStep::Emit(String::new()));
assert_eq!(
m.flush(),
"",
"the text layer has nothing to go on -- that is layer 1's job"
);
}
#[test]
fn stop_tokens_work_without_any_stop_strings() {
let mut m = StopMatcher::new(&[], &[42]);
assert!(m.is_stop_token(42));
assert!(!m.is_stop_token(43));
assert_eq!(m.push("free text"), StopStep::Emit("free text".into()));
}
}