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")) + }) }) })