parsers: qwen3.5 streaming tool-call parsing and add regression test (#15098)

This commit is contained in:
Jeffrey Morgan
2026-03-27 14:04:14 -07:00
committed by GitHub
parent 1cefa749aa
commit 69ed0c2729
2 changed files with 128 additions and 1 deletions

View File

@@ -190,7 +190,7 @@ func (p *Qwen35Parser) eat() ([]qwen35Event, bool) {
p.state = qwen35ParserStateCollectingContent
}
return events, true
} else if overlapLen := overlap(acc, qwen35ThinkingCloseTag); overlapLen > 0 {
} else if overlapLen := max(overlap(acc, qwen35ThinkingCloseTag), overlap(acc, qwen35ToolCallOpenTag)); overlapLen > 0 {
beforePartialTag := acc[:len(acc)-overlapLen]
trailingWsLen := trailingWhitespaceLen(beforePartialTag)
ambiguousStart := len(beforePartialTag) - trailingWsLen

View File

@@ -158,6 +158,133 @@ SF
}
}
func TestQwen35ParserToolCallEmittedInThinkingIsParsedWhenToolCallTagIsSplitAcrossChunks(t *testing.T) {
parser := ParserForName("qwen3.5")
if parser == nil {
t.Fatal("expected qwen3.5 parser")
}
tools := []api.Tool{
{
Function: api.ToolFunction{
Name: "get_weather",
Parameters: api.ToolFunctionParameters{
Properties: func() *api.ToolPropertiesMap {
props := api.NewToolPropertiesMap()
props.Set("location", api.ToolProperty{Type: api.PropertyType{"string"}})
return props
}(),
},
},
},
}
parser.Init(tools, nil, &api.ThinkValue{Value: true})
content, thinking, calls, err := parser.Add("Need weather lookup<tool_c", false)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
if content != "" {
t.Fatalf("expected empty content, got %q", content)
}
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 in first chunk, got %d", len(calls))
}
content, thinking, calls, err = parser.Add(`all><function=get_weather><parameter=location>
SF
</parameter></function></tool_call>`, true)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
if content != "" {
t.Fatalf("expected empty content, got %q", content)
}
if thinking != "" {
t.Fatalf("expected no additional thinking, got %q", thinking)
}
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)
}
}
func TestQwen35ParserFakeoutPartialToolCallThenThinkCloseAcrossChunks(t *testing.T) {
parser := ParserForName("qwen3.5")
if parser == nil {
t.Fatal("expected qwen3.5 parser")
}
tools := []api.Tool{
{
Function: api.ToolFunction{
Name: "get_weather",
Parameters: api.ToolFunctionParameters{
Properties: func() *api.ToolPropertiesMap {
props := api.NewToolPropertiesMap()
props.Set("location", api.ToolProperty{Type: api.PropertyType{"string"}})
return props
}(),
},
},
},
}
parser.Init(tools, nil, &api.ThinkValue{Value: true})
content, thinking, calls, err := parser.Add("Need weather lookup<tool_c", false)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
if content != "" {
t.Fatalf("expected empty content, got %q", content)
}
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 in first chunk, got %d", len(calls))
}
content, thinking, calls, err = parser.Add("</thi", false)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
if content != "" {
t.Fatalf("expected empty content, got %q", content)
}
if thinking != "<tool_c" {
t.Fatalf("expected thinking %q, got %q", "<tool_c", thinking)
}
if len(calls) != 0 {
t.Fatalf("expected no tool calls in second chunk, got %d", len(calls))
}
content, thinking, calls, err = parser.Add("nk>", true)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
if content != "" {
t.Fatalf("expected empty content, got %q", content)
}
if thinking != "" {
t.Fatalf("expected no additional thinking in third chunk, got %q", thinking)
}
if len(calls) != 0 {
t.Fatalf("expected no tool calls in third chunk, got %d", len(calls))
}
}
func TestQwen35ParserToolCallAfterThinkingCloseIsParsed(t *testing.T) {
parser := ParserForName("qwen3.5")
if parser == nil {