Additional thinking tags

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto 2026-01-20 12:02:35 +01:00
parent a352125726
commit 61a6e95f7d
2 changed files with 140 additions and 6 deletions

View file

@ -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
"<think>\n",
"<think>",
// Generic thinking tags
"<thinking>\n",
"<thinking>",
"<|inner_prefix|>", // Apertus
"<|START_THINKING|>", // Command R7B
"<seed:think>", // Seed
"[THINK]\n", // Magistral
// Apertus
"<|inner_prefix|>",
// Command R7B
"<|START_THINKING|>",
// Seed
"<seed:think>",
// 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{"</thinking>", "</think>"}
// These match the closing tags used by llama.cpp for various models
closingTags := []string{
"</thinking>",
"</think>",
"<|END_THINKING|>", // Command R7B
"<|inner_suffix|>", // Apertus
"</seed:think>", // 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
}{
{"<thinking>", "</thinking>"},
{"<think>", "</think>"},
{"<|START_THINKING|>", "<|END_THINKING|>"}, // Command R7B
{"<|inner_prefix|>", "<|inner_suffix|>"}, // Apertus
{"<seed:think>", "</seed:think>"}, // Seed
{"[THINK]", "[/THINK]"}, // Magistral
}
// Track the last position we've processed

View file

@ -381,5 +381,119 @@ var _ = Describe("Extract", func() {
Expect(reasoning).To(Equal("Reasoning"))
Expect(cleaned).To(Equal("content</thinking>more"))
})
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 content</seed:think>actual 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 <seed:think>reasoning here</seed:think> 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 <seed:think>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 content</seed:think>actual 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"))
})
})
})