use std::collections::HashSet;
use crate::generate::{earliest_stop_match, floor_char_boundary};
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum StopStep {
Emit(String),
Matched(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) = earliest_stop_match(&self.pending, &self.stops) {
let out = self.pending[..cut].to_string();
self.pending.clear();
return StopStep::Matched(out);
}
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 {
let bytes = pending.as_bytes();
let longest = stops
.iter()
.map(|s| s.len().saturating_sub(1))
.max()
.unwrap_or(0);
let max_k = longest.min(bytes.len());
(1..=max_k)
.rev()
.find(|&k| {
let tail = &bytes[bytes.len() - k..];
stops
.iter()
.any(|s| s.len() > k && s.as_bytes().starts_with(tail))
})
.unwrap_or(0)
}
#[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(String::new()));
}
#[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("keep this".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("say ".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(String::new()));
}
#[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()));
}
}