mirror of
https://github.com/ollama/ollama.git
synced 2026-04-17 15:53:27 +02:00
parsers: qwen3.5 streaming tool-call parsing and add regression test (#15098)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user