From 06df0795c80b17c1366522c45de24cb01bf9d8a3 Mon Sep 17 00:00:00 2001 From: jaberjaber23 Date: Thu, 5 Mar 2026 15:41:08 +0300 Subject: [PATCH] think stripping --- crates/openfang-api/src/openai_compat.rs | 2 +- crates/openfang-api/src/routes.rs | 7 ++-- crates/openfang-api/src/ws.rs | 43 ++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/crates/openfang-api/src/openai_compat.rs b/crates/openfang-api/src/openai_compat.rs index 884add98..a0e806c6 100644 --- a/crates/openfang-api/src/openai_compat.rs +++ b/crates/openfang-api/src/openai_compat.rs @@ -335,7 +335,7 @@ pub async fn chat_completions( index: 0, message: ChoiceMessage { role: "assistant", - content: Some(result.response), + content: Some(crate::ws::strip_think_tags(&result.response)), tool_calls: None, }, finish_reason: "stop", diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index bcf5c6ca..29198ace 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -321,8 +321,11 @@ pub async fn send_message( .await { Ok(result) => { + // Strip ... blocks from model output + let cleaned = crate::ws::strip_think_tags(&result.response); + // Guard: ensure we never return an empty response to the client - let response = if result.response.trim().is_empty() { + let response = if cleaned.trim().is_empty() { format!( "[The agent completed processing but returned no text response. ({} in / {} out | {} iter)]", result.total_usage.input_tokens, @@ -330,7 +333,7 @@ pub async fn send_message( result.iterations, ) } else { - result.response + cleaned }; ( StatusCode::OK, diff --git a/crates/openfang-api/src/ws.rs b/crates/openfang-api/src/ws.rs index 6b6a526e..19ff864f 100644 --- a/crates/openfang-api/src/ws.rs +++ b/crates/openfang-api/src/ws.rs @@ -621,8 +621,12 @@ async fn handle_text_message( return; } + // Strip ... blocks from model output + // (e.g. MiniMax, DeepSeek reasoning tokens) + let cleaned_response = strip_think_tags(&result.response); + // Guard: ensure we never send an empty response - let content = if result.response.trim().is_empty() { + let content = if cleaned_response.trim().is_empty() { format!( "[The agent completed processing but returned no text response. ({} in / {} out | {} iter)]", result.total_usage.input_tokens, @@ -630,7 +634,7 @@ async fn handle_text_message( result.iterations, ) } else { - result.response + cleaned_response }; // Estimate context pressure from last call @@ -1156,6 +1160,27 @@ fn extract_status_code(s: &str) -> Option { None } +/// Strip `...` blocks from model output. +/// +/// Some models (MiniMax, DeepSeek, etc.) wrap their reasoning in `` tags. +/// These are internal chain-of-thought and shouldn't be shown to the user. +pub fn strip_think_tags(text: &str) -> String { + let mut result = String::with_capacity(text.len()); + let mut remaining = text; + while let Some(start) = remaining.find("") { + result.push_str(&remaining[..start]); + if let Some(end) = remaining[start..].find("") { + remaining = &remaining[(start + end + 8)..]; // 8 = "".len() + } else { + // Unclosed tag — strip to end + remaining = ""; + break; + } + } + result.push_str(remaining); + result +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- @@ -1232,4 +1257,18 @@ mod tests { fn test_sanitize_trims_whitespace() { assert_eq!(sanitize_user_input(" hello "), "hello"); } + + #[test] + fn test_strip_think_tags() { + assert_eq!( + strip_think_tags("reasoning hereThe answer is 42."), + "The answer is 42." + ); + assert_eq!( + strip_think_tags("Hello \nsome thinking\n world"), + "Hello world" + ); + assert_eq!(strip_think_tags("No thinking here"), "No thinking here"); + assert_eq!(strip_think_tags("all thinking"), ""); + } }