diff --git a/pkg/reasoning/reasoning.go b/pkg/reasoning/reasoning.go
index 6d85566cc..e07b5954d 100644
--- a/pkg/reasoning/reasoning.go
+++ b/pkg/reasoning/reasoning.go
@@ -4,16 +4,23 @@ import (
"strings"
)
-// Common thinking/reasoning opening tags used by various models
+// Common thinking/reasoning opening tags used by various models.
+// These match the tags detected by llama.cpp in common/chat.cpp
var thinkingOpenTags = []string{
+ // DeepSeek R1, V3.1, Nemotron V2, MiniMax M2, Hermes 2 Pro, Granite, Exaone MOE
"\n",
"",
+ // Generic thinking tags
"\n",
"",
- "<|inner_prefix|>", // Apertus
- "<|START_THINKING|>", // Command R7B
- "", // Seed
- "[THINK]\n", // Magistral
+ // Apertus
+ "<|inner_prefix|>",
+ // Command R7B
+ "<|START_THINKING|>",
+ // Seed
+ "",
+ // Magistral (not in llama.cpp but common)
+ "[THINK]\n",
"[THINK]",
}
@@ -60,7 +67,15 @@ func Extract(content string, opts ...Option) (reasoning string, cleanedContent s
// All content from the start is treated as reasoning until a closing tag is found.
func extractForcedOpen(content string) (reasoning string, cleanedContent string) {
// Look for the earliest closing tag
- closingTags := []string{"", ""}
+ // These match the closing tags used by llama.cpp for various models
+ closingTags := []string{
+ "",
+ "",
+ "<|END_THINKING|>", // Command R7B
+ "<|inner_suffix|>", // Apertus
+ "", // Seed
+ "[/THINK]", // Magistral
+ }
earliestCloseIdx := -1
var matchedCloseTag string
@@ -110,12 +125,17 @@ func extractFromTags(content string) (reasoning string, cleanedContent string) {
remaining := content
// Define tag pairs to look for
+ // These match the tags used by llama.cpp for various models
tagPairs := []struct {
start string
end string
}{
{"", ""},
{"", ""},
+ {"<|START_THINKING|>", "<|END_THINKING|>"}, // Command R7B
+ {"<|inner_prefix|>", "<|inner_suffix|>"}, // Apertus
+ {"", ""}, // Seed
+ {"[THINK]", "[/THINK]"}, // Magistral
}
// Track the last position we've processed
diff --git a/pkg/reasoning/reasoning_test.go b/pkg/reasoning/reasoning_test.go
index a22cb9e22..796f106d9 100644
--- a/pkg/reasoning/reasoning_test.go
+++ b/pkg/reasoning/reasoning_test.go
@@ -381,5 +381,119 @@ var _ = Describe("Extract", func() {
Expect(reasoning).To(Equal("Reasoning"))
Expect(cleaned).To(Equal("contentmore"))
})
+
+ It("should handle Command R7B closing tag", func() {
+ content := "Reasoning content<|END_THINKING|>actual response"
+ reasoning, cleaned := Extract(content, WithThinkingForcedOpen())
+ Expect(reasoning).To(Equal("Reasoning content"))
+ Expect(cleaned).To(Equal("actual response"))
+ })
+
+ It("should handle Apertus closing tag", func() {
+ content := "Reasoning content<|inner_suffix|>actual response"
+ reasoning, cleaned := Extract(content, WithThinkingForcedOpen())
+ Expect(reasoning).To(Equal("Reasoning content"))
+ Expect(cleaned).To(Equal("actual response"))
+ })
+
+ It("should handle Seed closing tag", func() {
+ content := "Reasoning contentactual response"
+ reasoning, cleaned := Extract(content, WithThinkingForcedOpen())
+ Expect(reasoning).To(Equal("Reasoning content"))
+ Expect(cleaned).To(Equal("actual response"))
+ })
+
+ It("should handle Magistral closing tag", func() {
+ content := "Reasoning content[/THINK]actual response"
+ reasoning, cleaned := Extract(content, WithThinkingForcedOpen())
+ Expect(reasoning).To(Equal("Reasoning content"))
+ Expect(cleaned).To(Equal("actual response"))
+ })
+ })
+
+ Context("with model-specific tag pairs", func() {
+ It("should extract Command R7B reasoning tags", func() {
+ content := "Before <|START_THINKING|>reasoning here<|END_THINKING|> After"
+ reasoning, cleaned := Extract(content)
+ Expect(reasoning).To(Equal("reasoning here"))
+ Expect(cleaned).To(Equal("Before After"))
+ })
+
+ It("should extract Apertus reasoning tags", func() {
+ content := "Before <|inner_prefix|>reasoning here<|inner_suffix|> After"
+ reasoning, cleaned := Extract(content)
+ Expect(reasoning).To(Equal("reasoning here"))
+ Expect(cleaned).To(Equal("Before After"))
+ })
+
+ It("should extract Seed reasoning tags", func() {
+ content := "Before reasoning here After"
+ reasoning, cleaned := Extract(content)
+ Expect(reasoning).To(Equal("reasoning here"))
+ Expect(cleaned).To(Equal("Before After"))
+ })
+
+ It("should extract Magistral reasoning tags", func() {
+ content := "Before [THINK]reasoning here[/THINK] After"
+ reasoning, cleaned := Extract(content)
+ Expect(reasoning).To(Equal("reasoning here"))
+ Expect(cleaned).To(Equal("Before After"))
+ })
+
+ It("should handle unclosed Command R7B tag", func() {
+ content := "Before <|START_THINKING|>reasoning still streaming"
+ reasoning, cleaned := Extract(content)
+ Expect(reasoning).To(Equal("reasoning still streaming"))
+ Expect(cleaned).To(Equal("Before "))
+ })
+
+ It("should handle unclosed Apertus tag", func() {
+ content := "Before <|inner_prefix|>reasoning still streaming"
+ reasoning, cleaned := Extract(content)
+ Expect(reasoning).To(Equal("reasoning still streaming"))
+ Expect(cleaned).To(Equal("Before "))
+ })
+
+ It("should handle unclosed Seed tag", func() {
+ content := "Before reasoning still streaming"
+ reasoning, cleaned := Extract(content)
+ Expect(reasoning).To(Equal("reasoning still streaming"))
+ Expect(cleaned).To(Equal("Before "))
+ })
+
+ It("should handle unclosed Magistral tag", func() {
+ content := "Before [THINK]reasoning still streaming"
+ reasoning, cleaned := Extract(content)
+ Expect(reasoning).To(Equal("reasoning still streaming"))
+ Expect(cleaned).To(Equal("Before "))
+ })
+
+ It("should handle closing-only Command R7B tag", func() {
+ content := "Reasoning content<|END_THINKING|>actual response"
+ reasoning, cleaned := Extract(content)
+ Expect(reasoning).To(Equal("Reasoning content"))
+ Expect(cleaned).To(Equal("actual response"))
+ })
+
+ It("should handle closing-only Apertus tag", func() {
+ content := "Reasoning content<|inner_suffix|>actual response"
+ reasoning, cleaned := Extract(content)
+ Expect(reasoning).To(Equal("Reasoning content"))
+ Expect(cleaned).To(Equal("actual response"))
+ })
+
+ It("should handle closing-only Seed tag", func() {
+ content := "Reasoning contentactual response"
+ reasoning, cleaned := Extract(content)
+ Expect(reasoning).To(Equal("Reasoning content"))
+ Expect(cleaned).To(Equal("actual response"))
+ })
+
+ It("should handle closing-only Magistral tag", func() {
+ content := "Reasoning content[/THINK]actual response"
+ reasoning, cleaned := Extract(content)
+ Expect(reasoning).To(Equal("Reasoning content"))
+ Expect(cleaned).To(Equal("actual response"))
+ })
})
})