model/parsers: close think block if tool block starts in Qwen3.5 (#15022)

This commit is contained in:
Alfredo Matas 2026-03-27 19:28:34 +01:00 committed by GitHub
parent aec2fef95d
commit 1cefa749aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 23 additions and 8 deletions

View file

@ -21,6 +21,7 @@ const (
const (
qwen35ThinkingOpenTag = "<think>"
qwen35ThinkingCloseTag = "</think>"
qwen35ToolCallOpenTag = "<tool_call>"
)
// Qwen35Parser handles qwen3.5 reasoning extraction and delegates post-thinking
@ -202,6 +203,14 @@ func (p *Qwen35Parser) eat() ([]qwen35Event, bool) {
events = append(events, qwen35EventThinkingContent{content: unambiguous})
}
return events, false
} else if strings.Contains(acc, qwen35ToolCallOpenTag) {
// qwen3.5:9b model forgets sometimes to use </think> tag before the <tool_call> block starts
// this condition ends the Think block and continues with the <tool_call> when the tag
// is found
thinking, tooling := p.splitAtTag(qwen35ToolCallOpenTag, true)
p.buffer.Reset()
p.buffer.WriteString(thinking + qwen35ThinkingCloseTag + qwen35ToolCallOpenTag + tooling)
return events, true
}
whitespaceLen := trailingWhitespaceLen(acc)

View file

@ -108,7 +108,7 @@ func TestQwen35ParserAssistantPrefillStartsInContent(t *testing.T) {
}
}
func TestQwen35ParserToolCallEmittedInThinkingIsNotParsed(t *testing.T) {
func TestQwen35ParserToolCallEmittedInThinkingIsParsed(t *testing.T) {
parser := ParserForName("qwen3.5")
if parser == nil {
t.Fatal("expected qwen3.5 parser")
@ -141,14 +141,20 @@ SF
if content != "" {
t.Fatalf("expected empty content, got %q", content)
}
expectedThinking := `Need weather lookup<tool_call><function=get_weather><parameter=location>
SF
</parameter></function></tool_call>`
if thinking != expectedThinking {
t.Fatalf("expected thinking %q, got %q", expectedThinking, thinking)
if thinking != "Need weather lookup" {
t.Fatalf("expected thinking %q, got %q", "Need weather lookup", thinking)
}
if len(calls) != 0 {
t.Fatalf("expected no tool calls before </think>, got %d", len(calls))
if len(calls) != 1 {
t.Fatalf("expected 1 tool call, got %d", len(calls))
}
if calls[0].Function.Name != "get_weather" {
t.Fatalf("expected tool name %q, got %q", "get_weather", calls[0].Function.Name)
}
location, ok := calls[0].Function.Arguments.Get("location")
if !ok || location != "SF" {
t.Fatalf("expected location %q, got %v", "SF", location)
}
}