Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions core/http/react-ui/src/hooks/useChat.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { useState, useCallback, useRef, useEffect } from 'react'
import { API_CONFIG } from '../utils/config'
import { apiUrl } from '../utils/basePath'

const thinkingTagRegex = /<thinking>([\s\S]*?)<\/thinking>|<think>([\s\S]*?)<\/think>/g
const openThinkTagRegex = /<thinking>|<think>/
const closeThinkTagRegex = /<\/thinking>|<\/think>/
const thinkingTagRegex = /<thinking>([\s\S]*?)<\/thinking>|<think>([\s\S]*?)<\/think>|<\|channel>thought([\s\S]*?)<channel\|>/g
const openThinkTagRegex = /<thinking>|<think>|<\|channel>thought/
const closeThinkTagRegex = /<\/thinking>|<\/think>|<channel\|>/

async function extractHttpError(response) {
let errorMsg = `HTTP ${response.status}`
Expand All @@ -23,7 +23,7 @@ function extractThinking(text) {
thinkingTagRegex.lastIndex = 0
while ((match = thinkingTagRegex.exec(text)) !== null) {
regularContent += text.slice(lastIdx, match.index)
thinkingContent += match[1] || match[2] || ''
thinkingContent += match[1] || match[2] || match[3] || ''
lastIdx = match.index + match[0].length
}
regularContent += text.slice(lastIdx)
Expand Down Expand Up @@ -578,9 +578,9 @@ export function useChat(initialModel = '') {
}

if (insideThinkTag) {
const lastOpen = Math.max(rawContent.lastIndexOf('<thinking>'), rawContent.lastIndexOf('<think>'))
const lastOpen = Math.max(rawContent.lastIndexOf('<thinking>'), rawContent.lastIndexOf('<think>'), rawContent.lastIndexOf('<|channel>thought'))
if (lastOpen >= 0) {
const partial = rawContent.slice(lastOpen).replace(/<thinking>|<think>/, '')
const partial = rawContent.slice(lastOpen).replace(/<thinking>|<think>|<\|channel>thought/, '')
setStreamingReasoning(partial)
const beforeThink = rawContent.slice(0, lastOpen)
const { regularContent: contentBeforeThink } = extractThinking(beforeThink)
Expand Down
3 changes: 3 additions & 0 deletions pkg/reasoning/reasoning.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
// - <|inner_prefix|> (Apertus models)
// - <seed:think> (Seed models)
// - <think> (DeepSeek, Granite, ExaOne models)
// - <|channel>thought (Gemma 4 models)
// - <|think|> (Solar Open models)
// - <thinking> (General thinking tag)
// - [THINK] (Magistral models)
Expand All @@ -28,6 +29,7 @@ func DetectThinkingStartToken(prompt string, config *Config) string {
"<seed:think>", // Seed models
"<think>", // DeepSeek, Granite, ExaOne models
"<|think|>", // Solar Open models
"<|channel>thought", // Gemma 4 models
"<thinking>", // General thinking tag
"[THINK]", // Magistral models
}
Expand Down Expand Up @@ -146,6 +148,7 @@ func ExtractReasoning(content string, config *Config) (reasoning string, cleaned
{"<seed:think>", "</seed:think>"}, // Seed models
{"<think>", "</think>"}, // DeepSeek, Granite, ExaOne models
{"<|think|>", "<|end|><|begin|>assistant<|content|>"}, // Solar Open models (complex end)
{"<|channel>thought", "<channel|>"}, // Gemma 4 models
{"<thinking>", "</thinking>"}, // General thinking tag
{"[THINK]", "[/THINK]"}, // Magistral models
}
Expand Down
37 changes: 37 additions & 0 deletions pkg/reasoning/reasoning_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,29 @@ var _ = Describe("ExtractReasoning", func() {
Expect(cleaned).To(Equal("Before "))
})
})

Context("when content has <|channel>thought tags (Gemma 4)", func() {
It("should extract reasoning from channel thought block", func() {
content := "<|channel>thought\nThis is my reasoning\n<channel|>Hello! How can I help?"
reasoning, cleaned := ExtractReasoning(content, nil)
Expect(reasoning).To(Equal("This is my reasoning"))
Expect(cleaned).To(Equal("Hello! How can I help?"))
})

It("should handle unclosed channel thought block", func() {
content := "<|channel>thought\nIncomplete reasoning"
reasoning, cleaned := ExtractReasoning(content, nil)
Expect(reasoning).To(Equal("Incomplete reasoning"))
Expect(cleaned).To(Equal(""))
})

It("should handle content before and after channel thought block", func() {
content := "Before <|channel>thought\nGemma 4 reasoning\n<channel|> After"
reasoning, cleaned := ExtractReasoning(content, nil)
Expect(reasoning).To(Equal("Gemma 4 reasoning"))
Expect(cleaned).To(Equal("Before After"))
})
})
})

var _ = Describe("DetectThinkingStartToken", func() {
Expand All @@ -339,6 +362,12 @@ var _ = Describe("DetectThinkingStartToken", func() {
Expect(token).To(Equal("<thinking>"))
})

It("should detect <|channel>thought at the end (Gemma 4)", func() {
prompt := "Prompt text <|channel>thought"
token := DetectThinkingStartToken(prompt, nil)
Expect(token).To(Equal("<|channel>thought"))
})

It("should detect <|inner_prefix|> at the end", func() {
prompt := "Prompt <|inner_prefix|>"
token := DetectThinkingStartToken(prompt, nil)
Expand Down Expand Up @@ -817,6 +846,14 @@ var _ = Describe("ExtractReasoningWithConfig", func() {
Expect(cleaned).To(Equal("Text More"))
})

It("should strip reasoning from Gemma 4 channel tags when StripReasoningOnly is true", func() {
content := "<|channel>thought\nGemma 4 reasoning\n<channel|>Response text"
config := Config{StripReasoningOnly: boolPtr(true)}
reasoning, cleaned := ExtractReasoningWithConfig(content, "<|channel>thought", config)
Expect(reasoning).To(BeEmpty())
Expect(cleaned).To(Equal("Response text"))
})

It("should strip reasoning with multiline content when StripReasoningOnly is true", func() {
content := "Start <thinking>Line 1\nLine 2\nLine 3</thinking> End"
config := Config{StripReasoningOnly: boolPtr(true)}
Expand Down
Loading