//! Post-processing for LLM responses. //! Strips chain-of-thought reasoning tags that models like qwen3 emit. //! Applied to every provider response so callers never see internal reasoning. /// Strip `...` blocks from LLM responses. /// Models like qwen3 wrap internal chain-of-thought reasoning in these tags. /// Handles multiline content, multiple blocks, and partial/malformed tags. pub fn strip_thinking_tags(text: &str) -> String { let mut result = String::with_capacity(text.len()); let mut remaining = text; while let Some(start) = remaining.find("") { // Keep everything before the opening tag result.push_str(&remaining[..start]); // Find the matching closing tag let after_open = &remaining[start + 7..]; // len("") == 7 if let Some(end) = after_open.find("") { remaining = &after_open[end + 8..]; // len("") == 8 } else { // Unclosed — discard everything after it (the model is still "thinking") remaining = ""; } } result.push_str(remaining); // Clean up: leftover or /think fragments from partial responses let result = result.replace("", ""); let result = result.replace("/think", ""); // Collapse leading whitespace left behind after stripping let trimmed = result.trim(); if trimmed.is_empty() { String::new() } else { trimmed.to_string() } } #[cfg(test)] mod tests { use super::*; #[test] fn strips_simple_thinking_block() { let input = "reasoning hereactual response"; assert_eq!(strip_thinking_tags(input), "actual response"); } #[test] fn strips_multiline_thinking() { let input = "\nlong\nthinking\nprocess\n\nclean output"; assert_eq!(strip_thinking_tags(input), "clean output"); } #[test] fn passthrough_no_tags() { let input = "no thinking tags here"; assert_eq!(strip_thinking_tags(input), "no thinking tags here"); } #[test] fn strips_partial_think_at_end() { let input = "some text /think"; assert_eq!(strip_thinking_tags(input), "some text"); } #[test] fn strips_orphan_closing_tag() { let input = "some text more text"; assert_eq!(strip_thinking_tags(input), "some text more text"); } #[test] fn strips_multiple_thinking_blocks() { let input = "firsthello secondworld"; assert_eq!(strip_thinking_tags(input), "hello world"); } #[test] fn handles_unclosed_think_tag() { // Model started thinking and never closed — discard everything after let input = "good contentstill reasoning..."; assert_eq!(strip_thinking_tags(input), "good content"); } #[test] fn handles_empty_thinking_block() { let input = "content"; assert_eq!(strip_thinking_tags(input), "content"); } #[test] fn handles_only_thinking() { let input = "just thinking, no output"; assert_eq!(strip_thinking_tags(input), ""); } #[test] fn preserves_json_content() { let input = "let me analyze...{\"key\": \"value\", \"count\": 42}"; assert_eq!( strip_thinking_tags(input), "{\"key\": \"value\", \"count\": 42}" ); } #[test] fn real_world_extract_leak() { // Actual bug: qwen3 leaked "/think" into JSON values let input = "analyzing the page{\"learn_more\": \"Learn more\"}"; assert_eq!( strip_thinking_tags(input), "{\"learn_more\": \"Learn more\"}" ); } #[test] fn thinking_with_newlines_before_json() { let input = "\nstep 1\nstep 2\n\n\n{\"result\": true}"; assert_eq!(strip_thinking_tags(input), "{\"result\": true}"); } }