use crate::{ParserResult, ReasoningParser};
fn overlap(s: &str, delim: &str) -> usize {
let max = delim.len().min(s.len());
for i in (1..=max).rev() {
if !delim.is_char_boundary(i) {
continue; }
if s.ends_with(&delim[..i]) {
return i;
}
}
0
}
#[derive(Default, Debug, Clone)]
pub struct BasicReasoningParser {
think_start_token: String,
think_end_token: String,
_in_reasoning: bool,
stream_reasoning: bool,
_buffer: String,
stripped_think_start: bool,
recover_dangling_end: bool,
tool_start_token: Option<String>,
}
impl BasicReasoningParser {
pub fn new(
think_start_token: String,
think_end_token: String,
force_reasoning: bool,
stream_reasoning: bool,
) -> Self {
Self {
think_start_token,
think_end_token,
_in_reasoning: force_reasoning,
stream_reasoning,
_buffer: String::new(),
stripped_think_start: false,
recover_dangling_end: false,
tool_start_token: None,
}
}
pub fn with_tool_start_token(mut self, token: impl Into<String>) -> Self {
self.tool_start_token = Some(token.into());
self
}
pub fn with_dangling_end_recovery(mut self) -> Self {
self.recover_dangling_end = true;
self
}
}
impl ReasoningParser for BasicReasoningParser {
fn set_in_reasoning(&mut self, in_reasoning: bool) {
self._in_reasoning = in_reasoning;
if in_reasoning {
self.stripped_think_start = true;
}
}
fn detect_and_parse_reasoning(&mut self, text: &str, _token_ids: &[u32]) -> ParserResult {
let has_think_tag = text.contains(&self.think_start_token);
let supports_dangling_end_recovery = !self.think_end_token.is_empty();
let has_dangling_end = supports_dangling_end_recovery
&& !has_think_tag
&& text.contains(&self.think_end_token);
let in_reasoning = self._in_reasoning || has_think_tag || has_dangling_end;
if !in_reasoning {
return ParserResult {
normal_text: text.to_string(),
reasoning_text: String::new(),
};
}
let has_tool_start = self
.tool_start_token
.as_deref()
.is_some_and(|tok| text.contains(tok));
if self._in_reasoning
&& !has_think_tag
&& !text.contains(&self.think_end_token)
&& !has_tool_start
{
return ParserResult {
normal_text: String::new(),
reasoning_text: text.to_string(),
};
}
let mut reasoning_parts = Vec::new();
let mut normal_parts = Vec::new();
let mut cursor = 0;
let mut exited_on_tool_start = false;
let mut currently_reasoning = (self._in_reasoning && !has_think_tag) || has_dangling_end;
while cursor < text.len() {
if currently_reasoning {
if text[cursor..].starts_with(&self.think_start_token) {
cursor += self.think_start_token.len();
}
let end_offset = text[cursor..].find(&self.think_end_token);
let tool_offset = self
.tool_start_token
.as_deref()
.and_then(|tok| text[cursor..].find(tok));
match (end_offset, tool_offset) {
(Some(e), Some(t)) if t < e => {
reasoning_parts.push(&text[cursor..cursor + t]);
normal_parts.push(&text[cursor + t..]);
cursor = text.len();
currently_reasoning = false;
exited_on_tool_start = true;
}
(Some(e), _) => {
reasoning_parts.push(&text[cursor..cursor + e]);
cursor += e + self.think_end_token.len();
currently_reasoning = false;
}
(None, Some(t)) => {
reasoning_parts.push(&text[cursor..cursor + t]);
normal_parts.push(&text[cursor + t..]);
cursor = text.len();
currently_reasoning = false;
exited_on_tool_start = true;
}
(None, None) => {
reasoning_parts.push(&text[cursor..]);
cursor = text.len();
}
}
} else {
let start_offset = text[cursor..].find(&self.think_start_token);
let end_offset = text[cursor..].find(&self.think_end_token);
match (start_offset, end_offset) {
(Some(s), Some(e)) if s <= e => {
normal_parts.push(&text[cursor..cursor + s]);
cursor += s + self.think_start_token.len();
currently_reasoning = true;
}
(Some(s), None) => {
normal_parts.push(&text[cursor..cursor + s]);
cursor += s + self.think_start_token.len();
currently_reasoning = true;
}
(_, Some(e)) => {
normal_parts.push(&text[cursor..cursor + e]);
cursor += e + self.think_end_token.len();
}
(None, None) => {
normal_parts.push(&text[cursor..]);
cursor = text.len();
}
}
}
}
let joined_reasoning_text = reasoning_parts.join("");
let reasoning_text = if exited_on_tool_start {
joined_reasoning_text.trim_start().to_string()
} else {
joined_reasoning_text.trim().to_string()
};
let normal_text = normal_parts.join("").trim().to_string();
ParserResult {
normal_text,
reasoning_text,
}
}
fn parse_reasoning_streaming_incremental(
&mut self,
text: &str,
_token_ids: &[u32],
) -> ParserResult {
self._buffer.push_str(text);
let mut accumulated_normal = String::new();
let mut accumulated_reasoning = String::new();
loop {
let current_text = self._buffer.clone();
if !self.stripped_think_start
&& current_text.starts_with(self.think_start_token.as_str())
{
self._buffer = current_text[self.think_start_token.len()..].to_string();
self.stripped_think_start = true;
self._in_reasoning = true;
continue;
}
if !self.stripped_think_start
&& self._in_reasoning
&& !current_text.is_empty()
&& self.think_start_token.starts_with(current_text.as_str())
{
break;
}
if self._in_reasoning {
let end_idx = current_text.find(self.think_end_token.as_str());
let tool_idx = self
.tool_start_token
.as_deref()
.and_then(|tok| current_text.find(tok));
let force_exit_idx = match (end_idx, tool_idx) {
(Some(e), Some(t)) if t < e => Some(t),
(None, Some(t)) => Some(t),
_ => None,
};
if let Some(tool_at) = force_exit_idx {
accumulated_reasoning.push_str(¤t_text[..tool_at]);
accumulated_normal.push_str(¤t_text[tool_at..]);
self._buffer.clear();
self._in_reasoning = false;
self.stripped_think_start = false;
break;
}
if let Some(end_idx) = end_idx {
accumulated_reasoning.push_str(¤t_text[..end_idx]);
let after_end = end_idx + self.think_end_token.len();
self._buffer = current_text[after_end..].to_string();
self._in_reasoning = false;
self.stripped_think_start = false; continue; } else {
if self.stream_reasoning {
let ol_end = overlap(¤t_text, &self.think_end_token);
let ol_tool = self
.tool_start_token
.as_deref()
.map(|tok| overlap(¤t_text, tok))
.unwrap_or(0);
let ol = ol_end.max(ol_tool);
if ol >= 2 {
let safe_end = current_text.len() - ol;
if safe_end > 0 {
accumulated_reasoning.push_str(¤t_text[..safe_end]);
}
self._buffer = current_text[safe_end..].to_string();
} else {
accumulated_reasoning.push_str(¤t_text);
self._buffer.clear();
}
}
break;
}
} else {
let think_pos = current_text.find(self.think_start_token.as_str());
let end_pos = current_text.find(self.think_end_token.as_str());
if let Some(start_pos) = think_pos {
let start_before_end = match end_pos {
Some(end_pos) => start_pos <= end_pos,
None => true,
};
if start_before_end {
accumulated_normal.push_str(¤t_text[..start_pos]);
let after_start = start_pos + self.think_start_token.len();
self._buffer = current_text[after_start..].to_string();
self._in_reasoning = true;
self.stripped_think_start = true;
continue;
}
}
if let Some(end_pos) = end_pos {
if self.recover_dangling_end {
accumulated_reasoning.push_str(¤t_text[..end_pos]);
} else {
accumulated_normal.push_str(¤t_text[..end_pos]);
}
let after_end = end_pos + self.think_end_token.len();
self._buffer = current_text[after_end..].to_string();
self._in_reasoning = false;
self.stripped_think_start = false;
continue;
}
let ol_start = overlap(¤t_text, &self.think_start_token);
let ol_end = overlap(¤t_text, &self.think_end_token);
let ol = ol_start.max(ol_end);
if ol >= 2 {
let safe_end = current_text.len() - ol;
if safe_end > 0 {
if self.recover_dangling_end && ol_end > ol_start {
accumulated_reasoning.push_str(¤t_text[..safe_end]);
} else {
accumulated_normal.push_str(¤t_text[..safe_end]);
}
}
self._buffer = current_text[safe_end..].to_string();
} else {
accumulated_normal.push_str(¤t_text);
self._buffer.clear();
}
break;
}
}
ParserResult {
normal_text: accumulated_normal,
reasoning_text: accumulated_reasoning,
}
}
fn finish_reasoning_stream(&mut self) -> ParserResult {
if self._buffer.is_empty() {
return ParserResult::default();
}
let buffered = std::mem::take(&mut self._buffer);
if self._in_reasoning {
ParserResult {
normal_text: String::new(),
reasoning_text: buffered,
}
} else {
ParserResult {
normal_text: buffered,
reasoning_text: String::new(),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[test] fn test_detect_and_parse_reasoning_reasoning() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let result =
parser.detect_and_parse_reasoning("<think>with reasoning</think> and more text.", &[]);
assert_eq!(result.normal_text, "and more text.");
assert_eq!(result.reasoning_text, "with reasoning");
}
#[test] fn test_detect_and_parse_reasoning_reasoning_no_reasoning() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let result = parser.detect_and_parse_reasoning("This is a test without reasoning.", &[]);
assert_eq!(result.normal_text, "This is a test without reasoning.");
assert_eq!(result.reasoning_text, "");
}
#[test] fn test_detect_and_parse_reasoning_reasoning_truncated_reasoning() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let result = parser.detect_and_parse_reasoning("<think>with truncated reasoning", &[]);
assert_eq!(result.normal_text, "");
assert_eq!(result.reasoning_text, "with truncated reasoning");
}
#[test] fn test_parse_reasoning_streaming_incremental() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let result = parser.parse_reasoning_streaming_incremental("<thi", &[]);
assert_eq!(result.normal_text, "");
assert_eq!(result.reasoning_text, "");
}
#[test] fn test_parse_reasoning_streaming_incremental_complete() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let result = parser.parse_reasoning_streaming_incremental(
"<think>with reasoning</think> and more text.",
&[],
);
assert_eq!(result.normal_text, " and more text.");
assert_eq!(result.reasoning_text, "with reasoning");
}
#[test] fn test_parse_reasoning_streaming_incremental_no_end_token() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), true, true);
let result = parser.parse_reasoning_streaming_incremental("<think>with reasoning", &[]);
assert_eq!(result.normal_text, "");
assert_eq!(result.reasoning_text, "with reasoning");
}
#[test] fn test_detect_and_parse_reasoning_multiple_reasoning_blocks() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let result = parser.detect_and_parse_reasoning(
"<think>first reasoning</think> middle <think>second reasoning</think> end",
&[],
);
assert_eq!(result.normal_text, "middle end");
assert_eq!(result.reasoning_text, "first reasoningsecond reasoning");
}
#[test] fn test_streaming_multiple_reasoning_blocks() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, false);
let result1 = parser
.parse_reasoning_streaming_incremental("<think>first reasoning</think> middle", &[]);
assert_eq!(result1.normal_text, " middle");
assert_eq!(result1.reasoning_text, "first reasoning");
let result2 = parser
.parse_reasoning_streaming_incremental(" <think>second reasoning</think> end", &[]);
assert_eq!(result2.reasoning_text, "second reasoning");
assert_eq!(result2.normal_text, " end"); }
#[test] fn test_partial_token_matching_opening_tag() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let result1 = parser.parse_reasoning_streaming_incremental("<th", &[]);
assert_eq!(result1.normal_text, "");
assert_eq!(result1.reasoning_text, "");
let result2 = parser.parse_reasoning_streaming_incremental(
"ink>reasoning content</think> normal text",
&[],
);
assert_eq!(result2.normal_text, " normal text");
assert_eq!(result2.reasoning_text, "reasoning content");
}
#[test] fn test_partial_token_matching_closing_tag() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, false);
let result1 =
parser.parse_reasoning_streaming_incremental("<think>reasoning content</th", &[]);
assert_eq!(result1.normal_text, "");
assert_eq!(result1.reasoning_text, "");
let result2 = parser.parse_reasoning_streaming_incremental("ink> normal text", &[]);
assert_eq!(result2.normal_text, " normal text");
assert_eq!(result2.reasoning_text, "reasoning content");
}
#[test] fn test_buffer_state_persistence_across_calls() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, false);
let result1 = parser.parse_reasoning_streaming_incremental("<th", &[]);
assert_eq!(result1.normal_text, "");
assert_eq!(result1.reasoning_text, "");
let result2 = parser.parse_reasoning_streaming_incremental("ink>part1 ", &[]);
assert_eq!(result2.normal_text, "");
assert_eq!(result2.reasoning_text, "");
let result3 = parser.parse_reasoning_streaming_incremental("part2 ", &[]);
assert_eq!(result3.normal_text, "");
assert_eq!(result3.reasoning_text, "");
let result4 = parser.parse_reasoning_streaming_incremental("part3</think> normal", &[]);
assert_eq!(result4.normal_text, " normal");
assert_eq!(result4.reasoning_text, "part1 part2 part3");
}
#[test] fn test_streaming_with_stream_reasoning_enabled() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let result1 = parser.parse_reasoning_streaming_incremental("<think>reasoning ", &[]);
assert_eq!(result1.normal_text, "");
assert_eq!(result1.reasoning_text, "reasoning ");
let result2 = parser.parse_reasoning_streaming_incremental("content ", &[]);
assert_eq!(result2.normal_text, "");
assert_eq!(result2.reasoning_text, "content ");
let result3 = parser.parse_reasoning_streaming_incremental("more</think> normal", &[]);
assert_eq!(result3.normal_text, " normal");
assert_eq!(result3.reasoning_text, "more");
}
#[test] fn test_nested_reasoning_blocks() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let result = parser.detect_and_parse_reasoning(
"<think>outer <think>inner</think> reasoning</think> normal",
&[],
);
assert_eq!(result.reasoning_text, "outer <think>inner");
assert_eq!(result.normal_text, "reasoning normal");
}
#[test] fn test_malformed_missing_closing_tag() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let result = parser.detect_and_parse_reasoning("<think>reasoning without closing tag", &[]);
assert_eq!(result.normal_text, "");
assert_eq!(result.reasoning_text, "reasoning without closing tag");
}
#[test] fn test_malformed_stray_closing_tag() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let result = parser.detect_and_parse_reasoning("normal text</think> more normal", &[]);
assert_eq!(result.normal_text, "more normal");
assert_eq!(result.reasoning_text, "normal text");
}
#[test] fn test_kimi_unicode_dangling_close_marker_recovers() {
let mut parser =
BasicReasoningParser::new("◁think▷".to_string(), "◁/think▷".to_string(), false, true);
let result = parser.detect_and_parse_reasoning("normal◁/think▷answer", &[]);
assert_eq!(result.reasoning_text, "normal");
assert_eq!(result.normal_text, "answer");
assert!(
!result.normal_text.contains('◁') && !result.reasoning_text.contains('◁'),
"unicode marker must be stripped, not leaked; got normal={:?} reasoning={:?}",
result.normal_text,
result.reasoning_text
);
}
#[test] fn test_dangling_end_recovery_is_family_agnostic() {
let mut parser =
BasicReasoningParser::new("<R>".to_string(), "</R>".to_string(), false, true);
let result = parser.detect_and_parse_reasoning("prefix</R>tail", &[]);
assert_eq!(result.reasoning_text, "prefix");
assert_eq!(result.normal_text, "tail");
assert!(
!result.normal_text.contains("</R>"),
"custom closer must be stripped, not leaked; got normal={:?}",
result.normal_text
);
}
#[test] fn test_malformed_multiple_opening_tags() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let result = parser
.detect_and_parse_reasoning("<think>first <think>second reasoning</think> normal", &[]);
assert_eq!(result.reasoning_text, "first <think>second reasoning");
assert_eq!(result.normal_text, "normal");
}
#[test] fn test_empty_reasoning_block() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let result = parser.detect_and_parse_reasoning("<think></think> normal text", &[]);
assert_eq!(result.normal_text, "normal text");
assert_eq!(result.reasoning_text, "");
}
#[test] fn test_whitespace_only_reasoning_block() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let result = parser.detect_and_parse_reasoning("<think> \n\t </think> normal text", &[]);
assert_eq!(result.normal_text, "normal text");
assert_eq!(result.reasoning_text, ""); }
#[test] fn test_force_reasoning_mode() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), true, true);
let result = parser.detect_and_parse_reasoning("no think tags here", &[]);
assert_eq!(result.normal_text, "");
assert_eq!(result.reasoning_text, "no think tags here");
}
#[test]
fn test_force_reasoning_with_literal_think_prefix_does_not_leak() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), true, true);
let result = parser.detect_and_parse_reasoning("before <think>thinking</think> after", &[]);
assert_eq!(result.reasoning_text, "thinking");
assert_eq!(result.normal_text, "before after");
assert!(
!result.reasoning_text.contains("<think>"),
"literal <think> must be stripped, not absorbed; got {:?}",
result.reasoning_text
);
}
#[test]
fn test_force_reasoning_with_multiple_literal_spans() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), true, true);
let result = parser.detect_and_parse_reasoning(
"<think>first</think> middle <think>second</think> done",
&[],
);
assert_eq!(result.reasoning_text, "firstsecond");
assert_eq!(result.normal_text, "middle done");
}
#[test] fn test_streaming_stray_close_between_two_reasoning_spans() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let input = "<think>checking the weather</think>It is sunny.</think> Have a nice day.<think>double-checking</think> Confirmed.";
let r = parser.parse_reasoning_streaming_incremental(input, &[]);
assert!(
!r.normal_text.contains("</think>"),
"stray </think> must not leak into normal_text; got {:?}",
r.normal_text
);
assert_eq!(r.normal_text, "It is sunny. Have a nice day. Confirmed.");
assert_eq!(r.reasoning_text, "checking the weatherdouble-checking");
}
#[test] fn test_streaming_reset_state_after_complete_block() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let result1 =
parser.parse_reasoning_streaming_incremental("<think>reasoning</think> normal", &[]);
assert_eq!(result1.normal_text, " normal");
assert_eq!(result1.reasoning_text, "reasoning");
let result2 = parser.parse_reasoning_streaming_incremental(" more normal text", &[]);
assert_eq!(result2.normal_text, " more normal text");
assert_eq!(result2.reasoning_text, "");
let result3 = parser
.parse_reasoning_streaming_incremental(" <think>new reasoning</think> final", &[]);
assert_eq!(result3.reasoning_text, "new reasoning");
assert_eq!(result3.normal_text, " final");
let mut parser2 =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 = parser2.parse_reasoning_streaming_incremental("<think>first</think> normal", &[]);
assert_eq!(r1.reasoning_text, "first");
assert_eq!(r1.normal_text, " normal");
let r2 = parser2.parse_reasoning_streaming_incremental(" between", &[]);
assert_eq!(r2.normal_text, " between");
assert_eq!(r2.reasoning_text, "");
let r3 = parser2.parse_reasoning_streaming_incremental("<think>second</think> final", &[]);
assert_eq!(r3.reasoning_text, "second");
assert_eq!(r3.normal_text, " final");
}
#[test] fn test_post_reasoning_angle_bracket_not_buffered() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 =
parser.parse_reasoning_streaming_incremental("<think>reasoning content</think>", &[]);
assert_eq!(r1.reasoning_text, "reasoning content");
assert_eq!(r1.normal_text, "");
let r2 = parser.parse_reasoning_streaming_incremental("<", &[]);
assert_eq!(r2.normal_text, "<");
assert_eq!(r2.reasoning_text, "");
let r3 = parser.parse_reasoning_streaming_incremental("invoke name=\"get_weather\">", &[]);
assert_eq!(r3.normal_text, "invoke name=\"get_weather\">");
assert_eq!(r3.reasoning_text, "");
}
#[test] fn test_post_reasoning_tool_call_xml_preserved() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 = parser.parse_reasoning_streaming_incremental("<think>let me check", &[]);
assert_eq!(r1.reasoning_text, "let me check");
let r2 = parser.parse_reasoning_streaming_incremental("</think>", &[]);
assert_eq!(r2.normal_text, "");
assert_eq!(r2.reasoning_text, "");
let r3 = parser.parse_reasoning_streaming_incremental("<minimax:tool_call>", &[]);
assert_eq!(r3.normal_text, "<minimax:tool_call>");
let r4 = parser.parse_reasoning_streaming_incremental("\n", &[]);
assert_eq!(r4.normal_text, "\n");
let r5 = parser.parse_reasoning_streaming_incremental("<", &[]);
assert_eq!(r5.normal_text, "<");
let r6 = parser.parse_reasoning_streaming_incremental("invoke name=\"get_weather\">", &[]);
assert_eq!(r6.normal_text, "invoke name=\"get_weather\">");
}
#[test] fn test_interleaved_streaming_across_chunks() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 = parser.parse_reasoning_streaming_incremental("<think>thought 1</think>", &[]);
assert_eq!(r1.reasoning_text, "thought 1");
assert_eq!(r1.normal_text, "");
let r2 = parser.parse_reasoning_streaming_incremental(" answer 1 ", &[]);
assert_eq!(r2.normal_text, " answer 1 ");
assert_eq!(r2.reasoning_text, "");
let r3 = parser.parse_reasoning_streaming_incremental("<think>thought 2</think>", &[]);
assert_eq!(r3.reasoning_text, "thought 2");
assert_eq!(r3.normal_text, "");
let r4 = parser.parse_reasoning_streaming_incremental(" answer 2", &[]);
assert_eq!(r4.normal_text, " answer 2");
assert_eq!(r4.reasoning_text, "");
let r5 = parser.parse_reasoning_streaming_incremental("<think>thought 3</think>", &[]);
assert_eq!(r5.reasoning_text, "thought 3");
assert_eq!(r5.normal_text, "");
let r6 = parser.parse_reasoning_streaming_incremental(" final answer", &[]);
assert_eq!(r6.normal_text, " final answer");
assert_eq!(r6.reasoning_text, "");
}
#[test] fn test_three_reasoning_blocks_non_streaming() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let result = parser.detect_and_parse_reasoning(
"<think>A</think> one <think>B</think> two <think>C</think> three",
&[],
);
assert_eq!(result.reasoning_text, "ABC");
assert_eq!(result.normal_text, "one two three");
}
#[test] fn test_streaming_transition_chunk() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 = parser.parse_reasoning_streaming_incremental("<think>first", &[]);
assert_eq!(r1.reasoning_text, "first");
let r2 = parser.parse_reasoning_streaming_incremental("</think> middle <think>second", &[]);
assert_eq!(r2.reasoning_text, "second");
assert_eq!(r2.normal_text, " middle ");
let r3 = parser.parse_reasoning_streaming_incremental(" more</think> end", &[]);
assert_eq!(r3.reasoning_text, " more");
assert_eq!(r3.normal_text, " end");
}
#[test] fn test_interleaved_with_force_reasoning() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), true, true);
let r1 = parser.parse_reasoning_streaming_incremental("initial reasoning", &[]);
assert_eq!(r1.reasoning_text, "initial reasoning");
assert_eq!(r1.normal_text, "");
let r2 = parser.parse_reasoning_streaming_incremental("</think> answer", &[]);
assert_eq!(r2.reasoning_text, "");
assert_eq!(r2.normal_text, " answer");
let r3 =
parser.parse_reasoning_streaming_incremental("<think>second thought</think> done", &[]);
assert_eq!(r3.reasoning_text, "second thought");
assert_eq!(r3.normal_text, " done");
}
#[test] fn test_interleaved_partial_think_tag_between_blocks() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 = parser.parse_reasoning_streaming_incremental("<think>first</think> normal", &[]);
assert_eq!(r1.reasoning_text, "first");
assert_eq!(r1.normal_text, " normal");
let r2 = parser.parse_reasoning_streaming_incremental("<th", &[]);
assert_eq!(r2.normal_text, "");
assert_eq!(r2.reasoning_text, "");
let r3 = parser.parse_reasoning_streaming_incremental("ink>second</think> end", &[]);
assert_eq!(r3.reasoning_text, "second");
assert_eq!(r3.normal_text, " end");
}
#[test] fn test_lone_angle_bracket_between_reasoning_blocks() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 = parser.parse_reasoning_streaming_incremental("<think>thought</think>", &[]);
assert_eq!(r1.reasoning_text, "thought");
let r2 = parser.parse_reasoning_streaming_incremental("<", &[]);
assert_eq!(r2.normal_text, "<");
assert_eq!(r2.reasoning_text, "");
let r3 = parser.parse_reasoning_streaming_incremental("tool_call>", &[]);
assert_eq!(r3.normal_text, "tool_call>");
assert_eq!(r3.reasoning_text, "");
let r4 =
parser.parse_reasoning_streaming_incremental("<think>more thought</think> done", &[]);
assert_eq!(r4.reasoning_text, "more thought");
assert_eq!(r4.normal_text, " done");
}
#[test] fn test_force_reasoning_stream_false_buffers_until_end_token() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), true, false);
let r1 = parser.parse_reasoning_streaming_incremental("chunk one", &[]);
assert_eq!(r1.reasoning_text, "");
assert_eq!(r1.normal_text, "");
let r2 = parser.parse_reasoning_streaming_incremental(" chunk two", &[]);
assert_eq!(r2.reasoning_text, "");
assert_eq!(r2.normal_text, "");
let r3 = parser.parse_reasoning_streaming_incremental("</think> answer", &[]);
assert_eq!(r3.reasoning_text, "chunk one chunk two");
assert_eq!(r3.normal_text, " answer");
}
#[test] fn test_multiple_full_blocks_in_single_streaming_chunk() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 = parser.parse_reasoning_streaming_incremental(
"<think>A</think> mid <think>B</think> end",
&[],
);
assert_eq!(r1.reasoning_text, "AB");
assert_eq!(r1.normal_text, " mid end");
let r2 = parser.parse_reasoning_streaming_incremental("", &[]);
assert_eq!(r2.reasoning_text, "");
assert_eq!(r2.normal_text, "");
}
#[test] fn test_partial_end_token_stream_reasoning_true() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 = parser.parse_reasoning_streaming_incremental("<think>reasoning", &[]);
assert_eq!(r1.reasoning_text, "reasoning");
assert_eq!(r1.normal_text, "");
let r2 = parser.parse_reasoning_streaming_incremental("</th", &[]);
assert_eq!(r2.reasoning_text, "");
assert_eq!(r2.normal_text, "");
let r3 = parser.parse_reasoning_streaming_incremental("ink> normal", &[]);
assert_eq!(r3.reasoning_text, "");
assert_eq!(r3.normal_text, " normal");
}
#[test] fn test_empty_string_input_various_states() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 = parser.parse_reasoning_streaming_incremental("", &[]);
assert_eq!(r1.reasoning_text, "");
assert_eq!(r1.normal_text, "");
parser.parse_reasoning_streaming_incremental("<think>content", &[]);
let r2 = parser.parse_reasoning_streaming_incremental("", &[]);
assert_eq!(r2.reasoning_text, "");
assert_eq!(r2.normal_text, "");
parser.parse_reasoning_streaming_incremental("</think>", &[]);
let r3 = parser.parse_reasoning_streaming_incremental("", &[]);
assert_eq!(r3.reasoning_text, "");
assert_eq!(r3.normal_text, "");
}
#[test] fn test_force_reasoning_stream_false_multiple_blocks() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), true, false);
let r1 =
parser.parse_reasoning_streaming_incremental("initial reasoning</think> normal1 ", &[]);
assert_eq!(r1.reasoning_text, "initial reasoning");
assert_eq!(r1.normal_text, " normal1 ");
let r2 = parser
.parse_reasoning_streaming_incremental("<think>second block</think> normal2", &[]);
assert_eq!(r2.reasoning_text, "second block");
assert_eq!(r2.normal_text, " normal2");
}
#[test] fn test_glm5_pattern_a_burst_single_chunk() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 = parser.parse_reasoning_streaming_incremental(
"<think>T1</think><tool_call>A</tool_call><think>T2</think><tool_call>B</tool_call>",
&[],
);
assert_eq!(r1.reasoning_text, "T1T2");
assert_eq!(
r1.normal_text,
"<tool_call>A</tool_call><tool_call>B</tool_call>"
);
let r2 = parser.parse_reasoning_streaming_incremental("", &[]);
assert_eq!(r2.reasoning_text, "");
assert_eq!(r2.normal_text, "");
}
#[test] fn test_tool_call_xml_between_reasoning_blocks_streaming() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 = parser.parse_reasoning_streaming_incremental("<think>T1</think>", &[]);
assert_eq!(r1.reasoning_text, "T1");
assert_eq!(r1.normal_text, "");
let r2 = parser.parse_reasoning_streaming_incremental("<tool_call>A</tool_call>", &[]);
assert_eq!(r2.normal_text, "<tool_call>A</tool_call>");
assert_eq!(r2.reasoning_text, "");
let r3 = parser.parse_reasoning_streaming_incremental("<think>T2</think>", &[]);
assert_eq!(r3.reasoning_text, "T2");
assert_eq!(r3.normal_text, "");
let r4 = parser.parse_reasoning_streaming_incremental("<tool_call>B</tool_call>", &[]);
assert_eq!(r4.normal_text, "<tool_call>B</tool_call>");
assert_eq!(r4.reasoning_text, "");
}
#[test] fn test_mid_string_partial_opening_tag_batched() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 = parser.parse_reasoning_streaming_incremental("Hello world <th", &[]);
assert_eq!(r1.normal_text, "Hello world ");
assert_eq!(r1.reasoning_text, "");
let r2 = parser
.parse_reasoning_streaming_incremental("ink>reasoning content</think> answer", &[]);
assert_eq!(r2.reasoning_text, "reasoning content");
assert_eq!(r2.normal_text, " answer");
}
#[test] fn test_batched_tag_boundary_split() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 = parser.parse_reasoning_streaming_incremental("The answer is <thi", &[]);
assert_eq!(r1.normal_text, "The answer is ");
assert_eq!(r1.reasoning_text, "");
let r2 = parser.parse_reasoning_streaming_incremental("nk>let me think</think>42", &[]);
assert_eq!(r2.reasoning_text, "let me think");
assert_eq!(r2.normal_text, "42");
}
#[test] fn test_mid_string_partial_closing_tag_stream_reasoning_false() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, false);
let r1 =
parser.parse_reasoning_streaming_incremental("<think>reasoning content and </th", &[]);
assert_eq!(r1.normal_text, "");
assert_eq!(r1.reasoning_text, "");
let r2 = parser.parse_reasoning_streaming_incremental("ink> normal text", &[]);
assert_eq!(r2.reasoning_text, "reasoning content and ");
assert_eq!(r2.normal_text, " normal text");
}
#[test] fn test_mid_string_partial_closing_tag_stream_reasoning_true() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 =
parser.parse_reasoning_streaming_incremental("<think>reasoning content and </th", &[]);
assert_eq!(r1.reasoning_text, "reasoning content and ");
assert_eq!(r1.normal_text, "");
let r2 = parser.parse_reasoning_streaming_incremental("ink> normal text", &[]);
assert_eq!(r2.reasoning_text, "");
assert_eq!(r2.normal_text, " normal text");
}
#[test] fn test_batched_interleaved_with_mid_string_partial() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 =
parser.parse_reasoning_streaming_incremental("<think>thought1</think>answer1<thi", &[]);
assert_eq!(r1.reasoning_text, "thought1");
assert_eq!(r1.normal_text, "answer1");
let r2 = parser.parse_reasoning_streaming_incremental("nk>thought2</think>answer2", &[]);
assert_eq!(r2.reasoning_text, "thought2");
assert_eq!(r2.normal_text, "answer2");
}
#[test] fn test_partial_tag_false_positive() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 = parser.parse_reasoning_streaming_incremental("value <thesis on", &[]);
let r2 = parser.parse_reasoning_streaming_incremental(" AI> is great", &[]);
let combined_normal = format!("{}{}", r1.normal_text, r2.normal_text);
assert_eq!(combined_normal, "value <thesis on AI> is great");
assert_eq!(r1.reasoning_text, "");
assert_eq!(r2.reasoning_text, "");
}
#[test] fn test_partial_closing_tag_fakeout() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), false, true);
let r1 = parser.parse_reasoning_streaming_incremental("<think>abc</th", &[]);
assert_eq!(r1.reasoning_text, "abc");
assert_eq!(r1.normal_text, "");
let r2 = parser.parse_reasoning_streaming_incremental("ing>def", &[]);
assert_eq!(r2.reasoning_text, "</thing>def");
assert_eq!(r2.normal_text, "");
let r3 = parser.parse_reasoning_streaming_incremental("</think>done", &[]);
assert_eq!(r3.reasoning_text, "");
assert_eq!(r3.normal_text, "done");
}
#[test] fn test_overlap_helper_function() {
assert_eq!(overlap("abc</th", "</think>"), 4);
assert_eq!(overlap("abc</thing>def", "</think>"), 0);
assert_eq!(overlap("<", "<think>"), 1);
assert_eq!(overlap("<th", "<think>"), 3);
assert_eq!(overlap("<think>", "<think>"), 7); assert_eq!(overlap("no match", "<think>"), 0);
assert_eq!(overlap("", "<think>"), 0);
assert_eq!(overlap("Hello world <thi", "<think>"), 4);
assert_eq!(overlap("text◁", "◁think▷"), 3); assert_eq!(overlap("text◁th", "◁think▷"), 5);
assert_eq!(overlap("text◁/thi", "◁/think▷"), 7);
assert_eq!(overlap("no match", "◁think▷"), 0);
}
fn kimi_k2_parser() -> BasicReasoningParser {
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), true, true)
.with_tool_start_token(crate::reasoning::KIMI_K2_TOOL_SECTION_BEGIN)
}
#[rstest] #[case(
"thinking text <|tool_calls_section_begin|><|tool_call_begin|>functions.foo:0<|tool_call_argument_begin|>{}<|tool_call_end|><|tool_calls_section_end|>",
"thinking text ",
"<|tool_calls_section_begin|><|tool_call_begin|>functions.foo:0<|tool_call_argument_begin|>{}<|tool_call_end|><|tool_calls_section_end|>"
)]
#[case("r</think>a", "r", "a")]
#[case(
"reasoning</think>answer <|tool_calls_section_begin|>tc",
"reasoning",
"answer <|tool_calls_section_begin|>tc"
)]
fn test_kimi_k2_one_shot_split(
#[case] input: &str,
#[case] expected_reasoning: &str,
#[case] expected_normal: &str,
) {
let mut parser = kimi_k2_parser();
let r = parser.detect_and_parse_reasoning(input, &[]);
assert_eq!(r.reasoning_text, expected_reasoning);
assert_eq!(r.normal_text, expected_normal);
}
#[test] fn test_force_exit_streaming_single_chunk() {
let mut parser = kimi_k2_parser();
let r = parser.parse_reasoning_streaming_incremental(
"thinking text <|tool_calls_section_begin|><|tool_call_begin|>functions.foo:0<|tool_call_argument_begin|>{}<|tool_call_end|><|tool_calls_section_end|>",
&[],
);
assert_eq!(r.reasoning_text, "thinking text ");
assert_eq!(
r.normal_text,
"<|tool_calls_section_begin|><|tool_call_begin|>functions.foo:0<|tool_call_argument_begin|>{}<|tool_call_end|><|tool_calls_section_end|>"
);
}
#[test] fn test_force_exit_streaming_split_across_chunks() {
let mut parser = kimi_k2_parser();
let r1 = parser.parse_reasoning_streaming_incremental("thinking ", &[]);
assert_eq!(r1.reasoning_text, "thinking ");
assert_eq!(r1.normal_text, "");
let r2 = parser.parse_reasoning_streaming_incremental("text <|tool_cal", &[]);
assert_eq!(r2.reasoning_text, "text ");
assert_eq!(r2.normal_text, "");
let r3 = parser.parse_reasoning_streaming_incremental("ls_section_begin|>rest", &[]);
assert_eq!(r3.reasoning_text, "");
assert_eq!(r3.normal_text, "<|tool_calls_section_begin|>rest");
}
#[test] fn test_force_exit_partial_marker_resolves_as_non_marker() {
let mut parser = kimi_k2_parser();
let r1 = parser.parse_reasoning_streaming_incremental("abc <|tool_ca", &[]);
assert_eq!(r1.reasoning_text, "abc ");
assert_eq!(r1.normal_text, "");
let r2 = parser.parse_reasoning_streaming_incremental("xxx", &[]);
assert_eq!(r2.reasoning_text, "<|tool_caxxx");
assert_eq!(r2.normal_text, "");
}
#[test] fn test_no_tool_start_token_behaves_as_before() {
let mut parser =
BasicReasoningParser::new("<think>".to_string(), "</think>".to_string(), true, true);
let r =
parser.detect_and_parse_reasoning("thinking <|tool_calls_section_begin|>stuff", &[]);
assert_eq!(
r.reasoning_text,
"thinking <|tool_calls_section_begin|>stuff"
);
assert_eq!(r.normal_text, "");
}
}