fix(openai_compat): strip <think> tags from content before sending to user
Some models (e.g. MiniMax M2.5, DeepSeek) embed chain-of-thought in the content field using <think> tags rather than a dedicated reasoning_content field. Move these blocks into ReasoningContent so channels don't render raw thinking output to users. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
81dfdf5f45
commit
1239cd24cb
1 changed files with 26 additions and 2 deletions
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -271,6 +272,9 @@ func responsePreview(body []byte, maxLen int) string {
|
||||||
return string(trimmed[:maxLen]) + "..."
|
return string(trimmed[:maxLen]) + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// thinkTagRe matches <think>…</think> blocks (including multi-line).
|
||||||
|
var thinkTagRe = regexp.MustCompile(`(?s)<think>(.*?)</think>`)
|
||||||
|
|
||||||
func parseResponse(body io.Reader) (*LLMResponse, error) {
|
func parseResponse(body io.Reader) (*LLMResponse, error) {
|
||||||
var apiResponse struct {
|
var apiResponse struct {
|
||||||
Choices []struct {
|
Choices []struct {
|
||||||
|
|
@ -310,6 +314,26 @@ func parseResponse(body io.Reader) (*LLMResponse, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
choice := apiResponse.Choices[0]
|
choice := apiResponse.Choices[0]
|
||||||
|
|
||||||
|
// Strip <think>...</think> blocks from content.
|
||||||
|
// Some models (e.g. MiniMax M2.5, DeepSeek) embed chain-of-thought in the
|
||||||
|
// content field using <think> tags rather than a dedicated reasoning_content
|
||||||
|
// field. We move them into ReasoningContent so channels don't render them.
|
||||||
|
msgContent := choice.Message.Content
|
||||||
|
msgReasoning := choice.Message.ReasoningContent
|
||||||
|
if msgReasoning == "" && strings.Contains(msgContent, "<think>") {
|
||||||
|
var blocks []string
|
||||||
|
msgContent = thinkTagRe.ReplaceAllStringFunc(msgContent, func(m string) string {
|
||||||
|
sub := thinkTagRe.FindStringSubmatch(m)
|
||||||
|
if len(sub) > 1 {
|
||||||
|
blocks = append(blocks, strings.TrimSpace(sub[1]))
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
msgContent = strings.TrimSpace(msgContent)
|
||||||
|
msgReasoning = strings.Join(blocks, "\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
toolCalls := make([]ToolCall, 0, len(choice.Message.ToolCalls))
|
toolCalls := make([]ToolCall, 0, len(choice.Message.ToolCalls))
|
||||||
for _, tc := range choice.Message.ToolCalls {
|
for _, tc := range choice.Message.ToolCalls {
|
||||||
arguments := make(map[string]any)
|
arguments := make(map[string]any)
|
||||||
|
|
@ -351,8 +375,8 @@ func parseResponse(body io.Reader) (*LLMResponse, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &LLMResponse{
|
return &LLMResponse{
|
||||||
Content: choice.Message.Content,
|
Content: msgContent,
|
||||||
ReasoningContent: choice.Message.ReasoningContent,
|
ReasoningContent: msgReasoning,
|
||||||
Reasoning: choice.Message.Reasoning,
|
Reasoning: choice.Message.Reasoning,
|
||||||
ReasoningDetails: choice.Message.ReasoningDetails,
|
ReasoningDetails: choice.Message.ReasoningDetails,
|
||||||
ToolCalls: toolCalls,
|
ToolCalls: toolCalls,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue