mirror of
https://github.com/ollama/ollama.git
synced 2026-04-17 15:53:27 +02:00
model/parsers: fix missing parallel tool call indices (#15467)
We were missing setting the function index for several models that can make parallel tool calls. In the future we may want to consider putting some sort of post-parse hook and relieve the parsers of this duty. Fixes: #15457
This commit is contained in:
@@ -33,8 +33,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CogitoParser struct {
|
type CogitoParser struct {
|
||||||
state CogitoParserState
|
state CogitoParserState
|
||||||
buffer strings.Builder
|
buffer strings.Builder
|
||||||
|
callIndex int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *CogitoParser) HasToolSupport() bool {
|
func (p *CogitoParser) HasToolSupport() bool {
|
||||||
@@ -72,6 +73,7 @@ func (p *CogitoParser) setInitialState(lastMessage *api.Message, tools []api.Too
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *CogitoParser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
func (p *CogitoParser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||||
|
p.callIndex = 0
|
||||||
p.setInitialState(lastMessage, tools, thinkValue)
|
p.setInitialState(lastMessage, tools, thinkValue)
|
||||||
return tools
|
return tools
|
||||||
}
|
}
|
||||||
@@ -114,6 +116,11 @@ func (p *CogitoParser) Add(s string, done bool) (content string, thinking string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range toolCalls {
|
||||||
|
toolCalls[i].Function.Index = p.callIndex
|
||||||
|
p.callIndex++
|
||||||
|
}
|
||||||
|
|
||||||
return contentSb.String(), thinkingSb.String(), toolCalls, nil
|
return contentSb.String(), thinkingSb.String(), toolCalls, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ func TestCogitoParser(t *testing.T) {
|
|||||||
expectedToolCalls: []api.ToolCall{
|
expectedToolCalls: []api.ToolCall{
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
Name: "get_weather",
|
Index: 0,
|
||||||
|
Name: "get_weather",
|
||||||
Arguments: testArgs(map[string]any{
|
Arguments: testArgs(map[string]any{
|
||||||
"location": "Paris",
|
"location": "Paris",
|
||||||
}),
|
}),
|
||||||
@@ -110,7 +111,8 @@ func TestCogitoParser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
Name: "get_weather",
|
Index: 1,
|
||||||
|
Name: "get_weather",
|
||||||
Arguments: testArgs(map[string]any{
|
Arguments: testArgs(map[string]any{
|
||||||
"location": "London",
|
"location": "London",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const (
|
|||||||
type DeepSeek3Parser struct {
|
type DeepSeek3Parser struct {
|
||||||
state DeepSeek3ParserState
|
state DeepSeek3ParserState
|
||||||
buffer strings.Builder
|
buffer strings.Builder
|
||||||
|
callIndex int
|
||||||
hasThinkingSupport bool
|
hasThinkingSupport bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ func (p *DeepSeek3Parser) setInitialState(lastMessage *api.Message, tools []api.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *DeepSeek3Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
func (p *DeepSeek3Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||||
|
p.callIndex = 0
|
||||||
p.setInitialState(lastMessage, tools, thinkValue)
|
p.setInitialState(lastMessage, tools, thinkValue)
|
||||||
return tools
|
return tools
|
||||||
}
|
}
|
||||||
@@ -106,6 +108,11 @@ func (p *DeepSeek3Parser) Add(s string, done bool) (content string, thinking str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range toolCalls {
|
||||||
|
toolCalls[i].Function.Index = p.callIndex
|
||||||
|
p.callIndex++
|
||||||
|
}
|
||||||
|
|
||||||
return contentSb.String(), thinkingSb.String(), toolCalls, nil
|
return contentSb.String(), thinkingSb.String(), toolCalls, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ func TestDeepSeekParser(t *testing.T) {
|
|||||||
expectedCalls: []api.ToolCall{
|
expectedCalls: []api.ToolCall{
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
Name: "get_weather",
|
Index: 0,
|
||||||
|
Name: "get_weather",
|
||||||
Arguments: testArgs(map[string]any{
|
Arguments: testArgs(map[string]any{
|
||||||
"location": "Paris",
|
"location": "Paris",
|
||||||
}),
|
}),
|
||||||
@@ -74,7 +75,8 @@ func TestDeepSeekParser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
Name: "get_weather",
|
Index: 1,
|
||||||
|
Name: "get_weather",
|
||||||
Arguments: testArgs(map[string]any{
|
Arguments: testArgs(map[string]any{
|
||||||
"location": "London",
|
"location": "London",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ const (
|
|||||||
|
|
||||||
// This format uses <start_function_call>call:name{args}<end_function_call> for tool calls.
|
// This format uses <start_function_call>call:name{args}<end_function_call> for tool calls.
|
||||||
type FunctionGemmaParser struct {
|
type FunctionGemmaParser struct {
|
||||||
state FunctionGemmaParserState
|
state FunctionGemmaParserState
|
||||||
buffer strings.Builder
|
buffer strings.Builder
|
||||||
tools []api.Tool
|
tools []api.Tool
|
||||||
|
callIndex int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *FunctionGemmaParser) HasToolSupport() bool { return true }
|
func (p *FunctionGemmaParser) HasToolSupport() bool { return true }
|
||||||
@@ -33,6 +34,7 @@ func (p *FunctionGemmaParser) HasThinkingSupport() bool { return false }
|
|||||||
func (p *FunctionGemmaParser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
func (p *FunctionGemmaParser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||||
p.tools = tools
|
p.tools = tools
|
||||||
p.state = FunctionGemmaCollectingContent
|
p.state = FunctionGemmaCollectingContent
|
||||||
|
p.callIndex = 0
|
||||||
return tools
|
return tools
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +68,11 @@ func (p *FunctionGemmaParser) Add(s string, done bool) (content string, thinking
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range toolCalls {
|
||||||
|
toolCalls[i].Function.Index = p.callIndex
|
||||||
|
p.callIndex++
|
||||||
|
}
|
||||||
|
|
||||||
return contentSb.String(), "", toolCalls, nil
|
return contentSb.String(), "", toolCalls, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,12 +124,14 @@ func TestFunctionGemmaParser(t *testing.T) {
|
|||||||
expectedCalls: []api.ToolCall{
|
expectedCalls: []api.ToolCall{
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 0,
|
||||||
Name: "get_weather",
|
Name: "get_weather",
|
||||||
Arguments: testArgs(map[string]any{"city": "Paris"}),
|
Arguments: testArgs(map[string]any{"city": "Paris"}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 1,
|
||||||
Name: "get_weather",
|
Name: "get_weather",
|
||||||
Arguments: testArgs(map[string]any{"city": "London"}),
|
Arguments: testArgs(map[string]any{"city": "London"}),
|
||||||
},
|
},
|
||||||
@@ -345,12 +347,14 @@ func TestFunctionGemmaParser(t *testing.T) {
|
|||||||
expectedCalls: []api.ToolCall{
|
expectedCalls: []api.ToolCall{
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 0,
|
||||||
Name: "get_weather",
|
Name: "get_weather",
|
||||||
Arguments: testArgs(map[string]any{"city": "Paris"}),
|
Arguments: testArgs(map[string]any{"city": "Paris"}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 1,
|
||||||
Name: "get_time",
|
Name: "get_time",
|
||||||
Arguments: testArgs(map[string]any{"timezone": "UTC"}),
|
Arguments: testArgs(map[string]any{"timezone": "UTC"}),
|
||||||
},
|
},
|
||||||
@@ -372,12 +376,14 @@ func TestFunctionGemmaParser(t *testing.T) {
|
|||||||
expectedCalls: []api.ToolCall{
|
expectedCalls: []api.ToolCall{
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 0,
|
||||||
Name: "first",
|
Name: "first",
|
||||||
Arguments: api.NewToolCallFunctionArguments(),
|
Arguments: api.NewToolCallFunctionArguments(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 1,
|
||||||
Name: "second",
|
Name: "second",
|
||||||
Arguments: api.NewToolCallFunctionArguments(),
|
Arguments: api.NewToolCallFunctionArguments(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type Gemma4Parser struct {
|
|||||||
state Gemma4ParserState
|
state Gemma4ParserState
|
||||||
buffer strings.Builder
|
buffer strings.Builder
|
||||||
tools []api.Tool
|
tools []api.Tool
|
||||||
|
callIndex int
|
||||||
hasThinkingSupport bool
|
hasThinkingSupport bool
|
||||||
thinkingEnabled bool // true when both model supports and user requested thinking
|
thinkingEnabled bool // true when both model supports and user requested thinking
|
||||||
needsChannelNameStrip bool // true when we just entered thinking and need to strip "thought\n"
|
needsChannelNameStrip bool // true when we just entered thinking and need to strip "thought\n"
|
||||||
@@ -53,6 +54,7 @@ func (p *Gemma4Parser) HasThinkingSupport() bool {
|
|||||||
|
|
||||||
func (p *Gemma4Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
func (p *Gemma4Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||||
p.tools = tools
|
p.tools = tools
|
||||||
|
p.callIndex = 0
|
||||||
|
|
||||||
prefill := lastMessage != nil && lastMessage.Role == "assistant"
|
prefill := lastMessage != nil && lastMessage.Role == "assistant"
|
||||||
|
|
||||||
@@ -116,6 +118,11 @@ func (p *Gemma4Parser) Add(s string, done bool) (content string, thinking string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range toolCalls {
|
||||||
|
toolCalls[i].Function.Index = p.callIndex
|
||||||
|
p.callIndex++
|
||||||
|
}
|
||||||
|
|
||||||
return contentSb.String(), thinkingSb.String(), toolCalls, nil
|
return contentSb.String(), thinkingSb.String(), toolCalls, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -290,7 +290,8 @@ func TestGemma4Parser(t *testing.T) {
|
|||||||
expectedToolCalls: []api.ToolCall{
|
expectedToolCalls: []api.ToolCall{
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
Name: "get_weather",
|
Index: 0,
|
||||||
|
Name: "get_weather",
|
||||||
Arguments: testArgs(map[string]any{
|
Arguments: testArgs(map[string]any{
|
||||||
"location": "Paris",
|
"location": "Paris",
|
||||||
}),
|
}),
|
||||||
@@ -298,7 +299,8 @@ func TestGemma4Parser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
Name: "get_weather",
|
Index: 1,
|
||||||
|
Name: "get_weather",
|
||||||
Arguments: testArgs(map[string]any{
|
Arguments: testArgs(map[string]any{
|
||||||
"location": "London",
|
"location": "London",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const (
|
|||||||
type LFM2Parser struct {
|
type LFM2Parser struct {
|
||||||
state LFM2ParserState
|
state LFM2ParserState
|
||||||
buffer strings.Builder
|
buffer strings.Builder
|
||||||
|
callIndex int
|
||||||
hasThinkingSupport bool
|
hasThinkingSupport bool
|
||||||
needsThinkingLeadingTrim bool // trim leading whitespace after <think> tag
|
needsThinkingLeadingTrim bool // trim leading whitespace after <think> tag
|
||||||
needsContentLeadingTrim bool // trim leading whitespace after </think> tag
|
needsContentLeadingTrim bool // trim leading whitespace after </think> tag
|
||||||
@@ -66,6 +67,7 @@ func (p *LFM2Parser) setInitialState(lastMessage *api.Message, thinkValue *api.T
|
|||||||
|
|
||||||
func (p *LFM2Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
func (p *LFM2Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||||
p.toolNames = make(map[string]struct{}, len(tools))
|
p.toolNames = make(map[string]struct{}, len(tools))
|
||||||
|
p.callIndex = 0
|
||||||
p.hasTools = len(tools) > 0
|
p.hasTools = len(tools) > 0
|
||||||
for _, tool := range tools {
|
for _, tool := range tools {
|
||||||
if tool.Function.Name != "" {
|
if tool.Function.Name != "" {
|
||||||
@@ -123,6 +125,11 @@ func (p *LFM2Parser) Add(s string, done bool) (content string, thinking string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range toolCalls {
|
||||||
|
toolCalls[i].Function.Index = p.callIndex
|
||||||
|
p.callIndex++
|
||||||
|
}
|
||||||
|
|
||||||
return contentSb.String(), thinkingSb.String(), toolCalls, nil
|
return contentSb.String(), thinkingSb.String(), toolCalls, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ func TestLFM2Parser(t *testing.T) {
|
|||||||
expectedCalls: []api.ToolCall{
|
expectedCalls: []api.ToolCall{
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
Name: "get_weather",
|
Index: 0,
|
||||||
|
Name: "get_weather",
|
||||||
Arguments: testArgs(map[string]any{
|
Arguments: testArgs(map[string]any{
|
||||||
"location": "Paris",
|
"location": "Paris",
|
||||||
}),
|
}),
|
||||||
@@ -68,7 +69,8 @@ func TestLFM2Parser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
Name: "get_weather",
|
Index: 1,
|
||||||
|
Name: "get_weather",
|
||||||
Arguments: testArgs(map[string]any{
|
Arguments: testArgs(map[string]any{
|
||||||
"location": "London",
|
"location": "London",
|
||||||
}),
|
}),
|
||||||
@@ -205,7 +207,8 @@ func TestLFM2Parser(t *testing.T) {
|
|||||||
expectedCalls: []api.ToolCall{
|
expectedCalls: []api.ToolCall{
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
Name: "bash",
|
Index: 0,
|
||||||
|
Name: "bash",
|
||||||
Arguments: testArgs(map[string]any{
|
Arguments: testArgs(map[string]any{
|
||||||
"command": "ls",
|
"command": "ls",
|
||||||
}),
|
}),
|
||||||
@@ -213,7 +216,8 @@ func TestLFM2Parser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
Name: "bash",
|
Index: 1,
|
||||||
|
Name: "bash",
|
||||||
Arguments: testArgs(map[string]any{
|
Arguments: testArgs(map[string]any{
|
||||||
"command": "pwd",
|
"command": "pwd",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ type MinistralParser struct {
|
|||||||
state ministralParserState
|
state ministralParserState
|
||||||
buffer strings.Builder
|
buffer strings.Builder
|
||||||
tools []api.Tool
|
tools []api.Tool
|
||||||
|
callIndex int
|
||||||
hasThinkingSupport bool
|
hasThinkingSupport bool
|
||||||
pendingToolName string // stores tool name while collecting args
|
pendingToolName string // stores tool name while collecting args
|
||||||
}
|
}
|
||||||
@@ -73,6 +74,7 @@ func (p *MinistralParser) setInitialState(lastMessage *api.Message) {
|
|||||||
|
|
||||||
func (p *MinistralParser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
func (p *MinistralParser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||||
p.tools = tools
|
p.tools = tools
|
||||||
|
p.callIndex = 0
|
||||||
p.setInitialState(lastMessage)
|
p.setInitialState(lastMessage)
|
||||||
return tools
|
return tools
|
||||||
}
|
}
|
||||||
@@ -288,6 +290,11 @@ func (p *MinistralParser) Add(s string, done bool) (content string, thinking str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range toolCalls {
|
||||||
|
toolCalls[i].Function.Index = p.callIndex
|
||||||
|
p.callIndex++
|
||||||
|
}
|
||||||
|
|
||||||
return contentBuilder.String(), thinkingBuilder.String(), toolCalls, nil
|
return contentBuilder.String(), thinkingBuilder.String(), toolCalls, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/ollama/ollama/api"
|
"github.com/ollama/ollama/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -395,6 +396,54 @@ func TestMinistralParserStreaming(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMinistralParserAssignsSequentialToolCallIndices(t *testing.T) {
|
||||||
|
parser := &MinistralParser{}
|
||||||
|
parser.Init([]api.Tool{
|
||||||
|
{Function: api.ToolFunction{Name: "get_weather"}},
|
||||||
|
{Function: api.ToolFunction{Name: "get_time"}},
|
||||||
|
}, nil, nil)
|
||||||
|
|
||||||
|
content, thinking, calls, err := parser.Add(
|
||||||
|
`[TOOL_CALLS]get_weather[ARGS]{"location":"NYC"}[TOOL_CALLS]get_time[ARGS]{"timezone":"EST"}`,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Add() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if content != "" {
|
||||||
|
t.Fatalf("expected no content, got %q", content)
|
||||||
|
}
|
||||||
|
if thinking != "" {
|
||||||
|
t.Fatalf("expected no thinking, got %q", thinking)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 0,
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: testArgs(map[string]any{
|
||||||
|
"location": "NYC",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 1,
|
||||||
|
Name: "get_time",
|
||||||
|
Arguments: testArgs(map[string]any{
|
||||||
|
"timezone": "EST",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(expected, calls, argsComparer); diff != "" {
|
||||||
|
t.Fatalf("tool calls mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMinistralParser_Errors(t *testing.T) {
|
func TestMinistralParser_Errors(t *testing.T) {
|
||||||
t.Run("unknown tool returns error", func(t *testing.T) {
|
t.Run("unknown tool returns error", func(t *testing.T) {
|
||||||
p := &MinistralParser{}
|
p := &MinistralParser{}
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Olmo3Parser struct {
|
type Olmo3Parser struct {
|
||||||
state olmo3ParserState
|
state olmo3ParserState
|
||||||
buffer strings.Builder
|
buffer strings.Builder
|
||||||
|
callIndex int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Olmo3Parser) HasToolSupport() bool {
|
func (p *Olmo3Parser) HasToolSupport() bool {
|
||||||
@@ -40,6 +41,7 @@ func (p *Olmo3Parser) HasThinkingSupport() bool {
|
|||||||
|
|
||||||
func (p *Olmo3Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
func (p *Olmo3Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||||
p.state = olmo3StateContent
|
p.state = olmo3StateContent
|
||||||
|
p.callIndex = 0
|
||||||
return tools
|
return tools
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +86,11 @@ func (p *Olmo3Parser) Add(s string, done bool) (content string, thinking string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range allCalls {
|
||||||
|
allCalls[i].Function.Index = p.callIndex
|
||||||
|
p.callIndex++
|
||||||
|
}
|
||||||
|
|
||||||
return contentSb.String(), "", allCalls, nil
|
return contentSb.String(), "", allCalls, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,12 +69,14 @@ get_weather(location="New York")</function_calls>`,
|
|||||||
expectedCalls: []api.ToolCall{
|
expectedCalls: []api.ToolCall{
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 0,
|
||||||
Name: "get_weather",
|
Name: "get_weather",
|
||||||
Arguments: testArgs(map[string]any{"location": "San Francisco"}),
|
Arguments: testArgs(map[string]any{"location": "San Francisco"}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Function: api.ToolCallFunction{
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 1,
|
||||||
Name: "get_weather",
|
Name: "get_weather",
|
||||||
Arguments: testArgs(map[string]any{"location": "New York"}),
|
Arguments: testArgs(map[string]any{"location": "New York"}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type Qwen3VLParser struct {
|
|||||||
state qwenParserState
|
state qwenParserState
|
||||||
buffer strings.Builder
|
buffer strings.Builder
|
||||||
tools []api.Tool
|
tools []api.Tool
|
||||||
|
callIndex int
|
||||||
hasThinkingSupport bool
|
hasThinkingSupport bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +57,7 @@ func (p *Qwen3VLParser) setInitialState(lastMessage *api.Message) {
|
|||||||
|
|
||||||
func (p *Qwen3VLParser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
func (p *Qwen3VLParser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||||
p.tools = tools
|
p.tools = tools
|
||||||
|
p.callIndex = 0
|
||||||
p.setInitialState(lastMessage)
|
p.setInitialState(lastMessage)
|
||||||
return tools
|
return tools
|
||||||
}
|
}
|
||||||
@@ -90,6 +92,11 @@ func (p *Qwen3VLParser) Add(s string, done bool) (content string, thinking strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range calls {
|
||||||
|
calls[i].Function.Index = p.callIndex
|
||||||
|
p.callIndex++
|
||||||
|
}
|
||||||
|
|
||||||
return contentSb.String(), thinkingSb.String(), calls, nil
|
return contentSb.String(), thinkingSb.String(), calls, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/ollama/ollama/api"
|
"github.com/ollama/ollama/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -217,6 +218,51 @@ func TestQwen3VLNonThinkingParserStreaming(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQwen3VLNonThinkingAssignsSequentialToolCallIndices(t *testing.T) {
|
||||||
|
parser := Qwen3VLParser{hasThinkingSupport: false}
|
||||||
|
parser.Init([]api.Tool{}, nil, nil)
|
||||||
|
|
||||||
|
content, thinking, calls, err := parser.Add(
|
||||||
|
`<tool_call>{"name":"first","arguments":{"a":"1"}}</tool_call><tool_call>{"name":"second","arguments":{"b":"2"}}</tool_call>`,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Add() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if content != "" {
|
||||||
|
t.Fatalf("expected no content, got %q", content)
|
||||||
|
}
|
||||||
|
if thinking != "" {
|
||||||
|
t.Fatalf("expected no thinking, got %q", thinking)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 0,
|
||||||
|
Name: "first",
|
||||||
|
Arguments: testArgs(map[string]any{
|
||||||
|
"a": "1",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 1,
|
||||||
|
Name: "second",
|
||||||
|
Arguments: testArgs(map[string]any{
|
||||||
|
"b": "2",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(expected, calls, argsComparer); diff != "" {
|
||||||
|
t.Fatalf("tool calls mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestQwenOldParserStreaming(t *testing.T) {
|
func TestQwenOldParserStreaming(t *testing.T) {
|
||||||
type step struct {
|
type step struct {
|
||||||
input string
|
input string
|
||||||
|
|||||||
Reference in New Issue
Block a user