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"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -346,8 +347,11 @@ func parseResponse(body io.Reader) (*LLMResponse, error) {
|
|||
toolCalls = append(toolCalls, toolCall)
|
||||
}
|
||||
|
||||
// Strip any leaked thinking/reasoning tags from content
|
||||
cleanedContent := stripThinkingTags(choice.Message.Content)
|
||||
|
||||
return &LLMResponse{
|
||||
Content: choice.Message.Content,
|
||||
Content: cleanedContent,
|
||||
ReasoningContent: choice.Message.ReasoningContent,
|
||||
Reasoning: choice.Message.Reasoning,
|
||||
ReasoningDetails: choice.Message.ReasoningDetails,
|
||||
|
|
@ -518,3 +522,53 @@ func supportsPromptCacheKey(apiBase string) bool {
|
|||
host := u.Hostname()
|
||||
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