fix: strip leaked Thinking/Final tags from OpenAI-compatible responses
Some OpenAI-compatible providers (e.g., MiniMax) may return chain-of-thought content mixed into the response using tags like: - </think>...</think> - <thinking>...</thinking> - <thought>...</thought> - <reasoning>...</reasoning> - <final>...</final> These tags can leak internal reasoning text to end users. This commit adds a sanitizer that: 1. Handles escaped Unicode forms (\u003c/\u003e) 2. Removes thinking blocks entirely (tags + content) 3. Removes <final> tag wrappers while preserving the final content 4. Cleans up leftover whitespace-only lines Fixes #1235 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6460a0a7c7
commit
f0caa04f60
1 changed files with 55 additions and 1 deletions
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -346,8 +347,11 @@ func parseResponse(body io.Reader) (*LLMResponse, error) {
|
||||||
toolCalls = append(toolCalls, toolCall)
|
toolCalls = append(toolCalls, toolCall)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Strip any leaked thinking/reasoning tags from content
|
||||||
|
cleanedContent := stripThinkingTags(choice.Message.Content)
|
||||||
|
|
||||||
return &LLMResponse{
|
return &LLMResponse{
|
||||||
Content: choice.Message.Content,
|
Content: cleanedContent,
|
||||||
ReasoningContent: choice.Message.ReasoningContent,
|
ReasoningContent: choice.Message.ReasoningContent,
|
||||||
Reasoning: choice.Message.Reasoning,
|
Reasoning: choice.Message.Reasoning,
|
||||||
ReasoningDetails: choice.Message.ReasoningDetails,
|
ReasoningDetails: choice.Message.ReasoningDetails,
|
||||||
|
|
@ -518,3 +522,53 @@ func supportsPromptCacheKey(apiBase string) bool {
|
||||||
host := u.Hostname()
|
host := u.Hostname()
|
||||||
return host == "api.openai.com" || strings.HasSuffix(host, ".openai.azure.com")
|
return host == "api.openai.com" || strings.HasSuffix(host, ".openai.azure.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stripThinkingTags removes chain-of-thought tags from content that may be
|
||||||
|
// leaked by some OpenAI-compatible providers (e.g., MiniMax-style responses).
|
||||||
|
// Handles both normal tags and escaped Unicode forms.
|
||||||
|
// Tags removed: <think>, <thinking>, <thought>, <reasoning>, <final>
|
||||||
|
func stripThinkingTags(content string) string {
|
||||||
|
if content == "" {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, handle escaped forms like \u003cthink\u003e...\u003c/think\u003e
|
||||||
|
// These appear as literal backslash-u-sequences in the string
|
||||||
|
content = strings.ReplaceAll(content, `\u003c`, "<")
|
||||||
|
content = strings.ReplaceAll(content, `\u003e`, ">")
|
||||||
|
content = strings.ReplaceAll(content, `\\u003c`, "<")
|
||||||
|
content = strings.ReplaceAll(content, `\\u003e`, ">")
|
||||||
|
|
||||||
|
// Pattern to match thinking/reasoning tags (with optional inner content)
|
||||||
|
// Matches: <tag>content</tag>, <tag />, <tag></tag>
|
||||||
|
patterns := []string{
|
||||||
|
`(?i)<think\b[^>]*>.*?</think>`, // <think>...</think>
|
||||||
|
`(?i)<thinking\b[^>]*>.*?</thinking>`, // <thinking>...</thinking>
|
||||||
|
`(?i)<thought\b[^>]*>.*?</thought>`, // <thought>...</thought>
|
||||||
|
`(?i)<reasoning\b[^>]*>.*?</reasoning>`, // <reasoning>...</reasoning>
|
||||||
|
`(?i)<final\b[^>*>(?:/>|>.*?</final>)`, // <final/> or <final>...</final> (remove tag, keep content)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
re := regexp.MustCompile(pattern)
|
||||||
|
if strings.HasPrefix(pattern, `(?i)<final`) {
|
||||||
|
// For <final> tags, remove the tags but keep the inner content
|
||||||
|
content = re.ReplaceAllString(content, "$1")
|
||||||
|
} else {
|
||||||
|
// For thinking tags, remove both tags and content
|
||||||
|
content = re.ReplaceAllString(content, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up any whitespace-only lines left after removing blocks
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
var cleaned []string
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed != "" {
|
||||||
|
cleaned = append(cleaned, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(cleaned, "\n")
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue