use crate::policy::parser::reasoning::{ReasoningFormat, ReasoningParser};
pub(crate) fn count(
format: Option<ReasoningFormat>,
prompt_opened_reasoning: bool,
ids: &[usize],
mut decode: impl FnMut(&[usize]) -> String,
) -> Option<usize> {
let format = format?;
let pieces: Vec<String> = ids
.iter()
.map(|id| decode(std::slice::from_ref(id)))
.collect();
let raw: String = pieces.concat();
let mut parser = ReasoningParser::new(format, prompt_opened_reasoning, true);
let head = parser.push(&raw);
let tail = parser.flush();
let content_len = head.content.len() + tail.content.len();
let boundary = raw.len().saturating_sub(content_len);
let mut consumed = 0usize;
for (index, piece) in pieces.iter().enumerate() {
if consumed >= boundary {
return Some(index);
}
consumed += piece.len();
}
Some(pieces.len())
}
#[cfg(test)]
mod tests {
use super::*;
fn decoder(pieces: &'static [&'static str]) -> impl FnMut(&[usize]) -> String {
move |ids: &[usize]| ids.iter().map(|i| pieces[*i]).collect()
}
fn count_pieces(pieces: &'static [&'static str]) -> Option<usize> {
let ids: Vec<usize> = (0..pieces.len()).collect();
count(Some(ReasoningFormat::Think), false, &ids, decoder(pieces))
}
#[test]
fn thinking_is_charged_to_reasoning_and_the_answer_is_not() {
let n = count_pieces(&["<think>", "a", "b", "</think>", "answer"]);
assert_eq!(n, Some(4));
}
#[test]
fn a_buffered_run_is_still_reasoning() {
let n = count_pieces(&["<think>", "a", "<", "/th", "ink>", "answer"]);
assert_eq!(n, Some(5), "the split marker's tokens are reasoning");
}
#[test]
fn a_model_that_never_thinks_spends_no_reasoning_tokens() {
assert_eq!(count_pieces(&["hello", " world"]), Some(0));
}
#[test]
fn no_format_means_no_number_rather_than_zero() {
let ids = [0usize, 1];
assert_eq!(count(None, false, &ids, decoder(&["a", "b"])), None);
}
#[test]
fn an_answer_that_never_stopped_thinking_is_all_reasoning() {
let n = count_pieces(&["<think>", "a", "b", "c"]);
assert_eq!(n, Some(4));
}
}