use regex::Regex;
pub fn strip_thinking(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut current = text;
let mut has_thinking = false;
while !current.is_empty() {
let opening_pos = find_thinking_opening(current);
if let Some(start) = opening_pos {
result.push_str(¤t[..start]);
let (after_content, found) = find_and_consume_thinking_block(¤t[start..]);
if found {
has_thinking = true;
current = after_content;
} else {
break;
}
} else {
result.push_str(current);
break;
}
}
if has_thinking {
cleanup_thinking_artifacts(&result)
} else {
result
}
}
fn find_thinking_opening(text: &str) -> Option<usize> {
let lower = text.to_ascii_lowercase();
let patterns = [
"<thinking>",
"<think>",
"[thinking]",
"[[thinking]]",
"```thinking",
"<!--thinking",
];
let mut min_pos = None;
for pattern in &patterns {
if let Some(pos) = lower.find(pattern) {
if let Some(current_min) = min_pos {
if pos < current_min {
min_pos = Some(pos);
}
} else {
min_pos = Some(pos);
}
}
}
min_pos
}
fn find_and_consume_thinking_block(text: &str) -> (&str, bool) {
let lower = text.to_ascii_lowercase();
let tag_pairs = [
("<thinking>", "</thinking>"),
("<think>", "</think>"),
("[thinking]", "[/thinking]"),
("[[thinking]]", "[[/thinking]]"),
("```thinking", "```"),
("<!--thinking", "-->"),
];
let mut earliest_closing: Option<usize> = None;
let mut earliest_after_closing: Option<&str> = None;
for (opening, closing) in &tag_pairs {
if let Some(opening_pos) = lower.find(opening) {
let content_after_opening = &text[opening_pos + opening.len()..];
if let Some(closing_pos) = content_after_opening.to_ascii_lowercase().find(closing) {
let absolute_closing = opening_pos + opening.len() + closing_pos;
if let Some(current) = earliest_closing {
if absolute_closing < current {
earliest_closing = Some(absolute_closing);
earliest_after_closing = Some(&text[absolute_closing + closing.len()..]);
}
} else {
earliest_closing = Some(absolute_closing);
earliest_after_closing = Some(&text[absolute_closing + closing.len()..]);
}
}
}
}
if let Some(after) = earliest_after_closing {
(after, true)
} else {
(text, false)
}
}
fn cleanup_thinking_artifacts(text: &str) -> String {
let re = Regex::new(r"\n\s*\n\s*\n+").unwrap();
let result = re.replace_all(text, "\n\n");
result.trim().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_thinking_basic() {
let input = "feat: add login\n<thinking>I should write a clear message</thinking>";
let output = strip_thinking(input);
assert_eq!(output.trim(), "feat: add login");
}
#[test]
fn test_strip_thinking_anthropic_format() {
let input = "feat: add login\n<think> I should write a clear message</think>";
let output = strip_thinking(input);
assert_eq!(output.trim(), "feat: add login");
}
#[test]
fn test_strip_thinking_multiple_blocks() {
let input =
"First part\n<thinking>block 1</thinking>\nMiddle\n<thinking>block 2</thinking>\nLast";
let output = strip_thinking(input);
assert!(output.contains("First part"));
assert!(output.contains("Middle"));
assert!(output.contains("Last"));
assert!(!output.contains("<thinking>"));
}
#[test]
fn test_strip_thinking_no_thinking() {
let input = "feat: add login feature";
let output = strip_thinking(input);
assert_eq!(output, "feat: add login feature");
}
#[test]
fn test_strip_thinking_unclosed_tag() {
let input = "feat: add login\n<thinking unclosed";
let output = strip_thinking(input);
assert!(output.contains("<thinking"));
}
}