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"), "");
+ }
}