fix: strip leaked Thinking/Final tags from OpenAI-compatible responses

Some OpenAI-compatible providers return chain-of-thought content mixed into
the response field using XML-style tags. This internal reasoning text leaks
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

Uses direct regex calls instead of pattern array to avoid linter issues.

Fixes #1235

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hobostay 2026-03-12 17:05:32 +08:00
parent f0caa04f60
commit 2940264045
No known key found for this signature in database

View file

@ -526,7 +526,7 @@ func supportsPromptCacheKey(apiBase string) bool {
// stripThinkingTags removes chain-of-thought tags from content that may be // stripThinkingTags removes chain-of-thought tags from content that may be
// leaked by some OpenAI-compatible providers (e.g., MiniMax-style responses). // leaked by some OpenAI-compatible providers (e.g., MiniMax-style responses).
// Handles both normal tags and escaped Unicode forms. // Handles both normal tags and escaped Unicode forms.
// Tags removed: <think>, <thinking>, <thought>, <reasoning>, <final> // Tags removed: </think>, <thinking>, <thought>, <reasoning>, <final>
func stripThinkingTags(content string) string { func stripThinkingTags(content string) string {
if content == "" { if content == "" {
return content return content
@ -539,26 +539,33 @@ func stripThinkingTags(content string) string {
content = strings.ReplaceAll(content, `\\u003c`, "<") content = strings.ReplaceAll(content, `\\u003c`, "<")
content = strings.ReplaceAll(content, `\\u003e`, ">") content = strings.ReplaceAll(content, `\\u003e`, ">")
// Pattern to match thinking/reasoning tags (with optional inner content) // Remove </think> tags and content (case-insensitive, multiline)
// Matches: <tag>content</tag>, <tag />, <tag></tag> content = regexp.MustCompile(`(?is)<think\b[^>]*>.*?
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, "")
}
} `).ReplaceAllString(content, "")
// Remove <thinking> tags and content
content = regexp.MustCompile(`(?is)<thinking\b[^>]*>.*?</thinking>`).ReplaceAllString(content, "")
// Remove <thought> tags and content
content = regexp.MustCompile(`(?is)<thought\b[^>]*>.*?</thought>`).ReplaceAllString(content, "")
// Remove <reasoning> tags and content
content = regexp.MustCompile(`(?is)<reasoning\b[^>]*>.*?</reasoning>`).ReplaceAllString(content, "")
// For <final> tags, remove the tags but keep the inner content
// Handle both <final/> and <final>content</final>
content = regexp.MustCompile(`(?i)<final\b[^>]*/>`).ReplaceAllString(content, "")
content = regexp.MustCompile(`(?is)<final\b[^>]*>(.*?)</final>`).ReplaceAllString(content, "$1")
// Clean up any whitespace-only lines left after removing blocks // Clean up any whitespace-only lines left after removing blocks
lines := strings.Split(content, "\n") lines := strings.Split(content, "\n")