diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go
index 5c868626a0..55bc414163 100644
--- a/pkg/providers/openai_compat/provider.go
+++ b/pkg/providers/openai_compat/provider.go
@@ -10,6 +10,7 @@ import (
"log"
"net/http"
"net/url"
+ "regexp"
"strings"
"time"
@@ -40,6 +41,13 @@ type Option func(*Provider)
const defaultRequestTimeout = 120 * time.Second
+var (
+ // Providers sometimes leak chain-of-thought in these tags in `content`.
+ quickReasoningTagRE = regexp.MustCompile(`(?i)<\s*/?\s*(?:think(?:ing)?|thought|reasoning|final)\b`)
+ finalTagRE = regexp.MustCompile(`(?i)<\s*/?\s*final\b[^>]*>`)
+ thinkingTagRE = regexp.MustCompile(`(?i)<\s*(/?)\s*(?:think(?:ing)?|thought|reasoning)\b[^>]*>`)
+)
+
func WithMaxTokensField(maxTokensField string) Option {
return func(p *Provider) {
p.maxTokensField = maxTokensField
@@ -271,6 +279,52 @@ func responsePreview(body []byte, maxLen int) string {
return string(trimmed[:maxLen]) + "..."
}
+// stripThinkingAndFinalTags removes leaked reasoning blocks while preserving
+// user-facing answer text (e.g. answer -> answer).
+func stripThinkingAndFinalTags(content string) string {
+ if content == "" {
+ return content
+ }
+ // Some APIs double-escape angle brackets in JSON payloads.
+ content = strings.ReplaceAll(content, `\u003c`, "<")
+ content = strings.ReplaceAll(content, `\u003e`, ">")
+ content = strings.ReplaceAll(content, `\u003C`, "<")
+ content = strings.ReplaceAll(content, `\u003E`, ">")
+
+ if !quickReasoningTagRE.MatchString(content) {
+ return strings.TrimSpace(content)
+ }
+
+ cleaned := finalTagRE.ReplaceAllString(content, "")
+ indexes := thinkingTagRE.FindAllStringSubmatchIndex(cleaned, -1)
+ if len(indexes) == 0 {
+ return strings.TrimSpace(cleaned)
+ }
+
+ var b strings.Builder
+ lastIndex := 0
+ inThinking := false
+ for _, idx := range indexes {
+ matchStart, matchEnd := idx[0], idx[1]
+ isClose := idx[2] >= 0 && cleaned[idx[2]:idx[3]] == "/"
+
+ if !inThinking {
+ b.WriteString(cleaned[lastIndex:matchStart])
+ if !isClose {
+ inThinking = true
+ }
+ } else if isClose {
+ inThinking = false
+ }
+ lastIndex = matchEnd
+ }
+
+ if !inThinking {
+ b.WriteString(cleaned[lastIndex:])
+ }
+ return strings.TrimSpace(b.String())
+}
+
func parseResponse(body io.Reader) (*LLMResponse, error) {
var apiResponse struct {
Choices []struct {
@@ -351,7 +405,7 @@ func parseResponse(body io.Reader) (*LLMResponse, error) {
}
return &LLMResponse{
- Content: choice.Message.Content,
+ Content: stripThinkingAndFinalTags(choice.Message.Content),
ReasoningContent: choice.Message.ReasoningContent,
Reasoning: choice.Message.Reasoning,
ReasoningDetails: choice.Message.ReasoningDetails,
diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go
index 9a3a7acc5c..53f21e9042 100644
--- a/pkg/providers/openai_compat/provider_test.go
+++ b/pkg/providers/openai_compat/provider_test.go
@@ -152,6 +152,63 @@ func TestProviderChat_ParsesReasoningContent(t *testing.T) {
}
}
+func TestParseResponse_StripsThinkingAndFinalTags(t *testing.T) {
+ body := strings.NewReader(`{
+ "choices": [{
+ "message": {
+ "content": "internal reasoningThe answer is 2"
+ },
+ "finish_reason": "stop"
+ }]
+ }`)
+
+ out, err := parseResponse(body)
+ if err != nil {
+ t.Fatalf("parseResponse() error = %v", err)
+ }
+ if out.Content != "The answer is 2" {
+ t.Fatalf("Content = %q, want %q", out.Content, "The answer is 2")
+ }
+}
+
+func TestParseResponse_StripsEscapedThinkingTags(t *testing.T) {
+ body := strings.NewReader(`{
+ "choices": [{
+ "message": {
+ "content": "\\u003cthink\\u003einternal\\u003c/think\\u003e\\u003cfinal\\u003eOK\\u003c/final\\u003e"
+ },
+ "finish_reason": "stop"
+ }]
+ }`)
+
+ out, err := parseResponse(body)
+ if err != nil {
+ t.Fatalf("parseResponse() error = %v", err)
+ }
+ if out.Content != "OK" {
+ t.Fatalf("Content = %q, want %q", out.Content, "OK")
+ }
+}
+
+func TestParseResponse_DropsUnclosedThinkingBlock(t *testing.T) {
+ body := strings.NewReader(`{
+ "choices": [{
+ "message": {
+ "content": "Visiblehidden reasoning"
+ },
+ "finish_reason": "stop"
+ }]
+ }`)
+
+ out, err := parseResponse(body)
+ if err != nil {
+ t.Fatalf("parseResponse() error = %v", err)
+ }
+ if out.Content != "Visible" {
+ t.Fatalf("Content = %q, want %q", out.Content, "Visible")
+ }
+}
+
func TestProviderChat_PreservesReasoningContentInHistory(t *testing.T) {
var requestBody map[string]any