diff --git a/model/parsers/cogito.go b/model/parsers/cogito.go index 2415dd31b..a335c42de 100644 --- a/model/parsers/cogito.go +++ b/model/parsers/cogito.go @@ -33,8 +33,9 @@ const ( ) type CogitoParser struct { - state CogitoParserState - buffer strings.Builder + state CogitoParserState + buffer strings.Builder + callIndex int } 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 { + p.callIndex = 0 p.setInitialState(lastMessage, tools, thinkValue) 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 } diff --git a/model/parsers/cogito_test.go b/model/parsers/cogito_test.go index 932e1b9a6..33166961e 100644 --- a/model/parsers/cogito_test.go +++ b/model/parsers/cogito_test.go @@ -39,7 +39,8 @@ func TestCogitoParser(t *testing.T) { expectedToolCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ - Name: "get_weather", + Index: 0, + Name: "get_weather", Arguments: testArgs(map[string]any{ "location": "Paris", }), @@ -110,7 +111,8 @@ func TestCogitoParser(t *testing.T) { }, { Function: api.ToolCallFunction{ - Name: "get_weather", + Index: 1, + Name: "get_weather", Arguments: testArgs(map[string]any{ "location": "London", }), diff --git a/model/parsers/deepseek3.go b/model/parsers/deepseek3.go index bafc85ba2..67cec42f1 100644 --- a/model/parsers/deepseek3.go +++ b/model/parsers/deepseek3.go @@ -33,6 +33,7 @@ const ( type DeepSeek3Parser struct { state DeepSeek3ParserState buffer strings.Builder + callIndex int 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 { + p.callIndex = 0 p.setInitialState(lastMessage, tools, thinkValue) 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 } diff --git a/model/parsers/deepseek3_test.go b/model/parsers/deepseek3_test.go index d648300d7..7bf2eb9d6 100644 --- a/model/parsers/deepseek3_test.go +++ b/model/parsers/deepseek3_test.go @@ -50,7 +50,8 @@ func TestDeepSeekParser(t *testing.T) { expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ - Name: "get_weather", + Index: 0, + Name: "get_weather", Arguments: testArgs(map[string]any{ "location": "Paris", }), @@ -74,7 +75,8 @@ func TestDeepSeekParser(t *testing.T) { }, { Function: api.ToolCallFunction{ - Name: "get_weather", + Index: 1, + Name: "get_weather", Arguments: testArgs(map[string]any{ "location": "London", }), diff --git a/model/parsers/functiongemma.go b/model/parsers/functiongemma.go index 9d3df9edb..92eea58ab 100644 --- a/model/parsers/functiongemma.go +++ b/model/parsers/functiongemma.go @@ -22,9 +22,10 @@ const ( // This format uses call:name{args} for tool calls. type FunctionGemmaParser struct { - state FunctionGemmaParserState - buffer strings.Builder - tools []api.Tool + state FunctionGemmaParserState + buffer strings.Builder + tools []api.Tool + callIndex int } 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 { p.tools = tools p.state = FunctionGemmaCollectingContent + p.callIndex = 0 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 } diff --git a/model/parsers/functiongemma_test.go b/model/parsers/functiongemma_test.go index 092763019..023044b72 100644 --- a/model/parsers/functiongemma_test.go +++ b/model/parsers/functiongemma_test.go @@ -124,12 +124,14 @@ func TestFunctionGemmaParser(t *testing.T) { expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ + Index: 0, Name: "get_weather", Arguments: testArgs(map[string]any{"city": "Paris"}), }, }, { Function: api.ToolCallFunction{ + Index: 1, Name: "get_weather", Arguments: testArgs(map[string]any{"city": "London"}), }, @@ -345,12 +347,14 @@ func TestFunctionGemmaParser(t *testing.T) { expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ + Index: 0, Name: "get_weather", Arguments: testArgs(map[string]any{"city": "Paris"}), }, }, { Function: api.ToolCallFunction{ + Index: 1, Name: "get_time", Arguments: testArgs(map[string]any{"timezone": "UTC"}), }, @@ -372,12 +376,14 @@ func TestFunctionGemmaParser(t *testing.T) { expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ + Index: 0, Name: "first", Arguments: api.NewToolCallFunctionArguments(), }, }, { Function: api.ToolCallFunction{ + Index: 1, Name: "second", Arguments: api.NewToolCallFunctionArguments(), }, diff --git a/model/parsers/gemma4.go b/model/parsers/gemma4.go index 156605824..1cd151917 100644 --- a/model/parsers/gemma4.go +++ b/model/parsers/gemma4.go @@ -38,6 +38,7 @@ type Gemma4Parser struct { state Gemma4ParserState buffer strings.Builder tools []api.Tool + callIndex int hasThinkingSupport bool 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" @@ -53,6 +54,7 @@ func (p *Gemma4Parser) HasThinkingSupport() bool { func (p *Gemma4Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool { p.tools = tools + p.callIndex = 0 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 } diff --git a/model/parsers/gemma4_test.go b/model/parsers/gemma4_test.go index 07ff67bf1..93221c401 100644 --- a/model/parsers/gemma4_test.go +++ b/model/parsers/gemma4_test.go @@ -290,7 +290,8 @@ func TestGemma4Parser(t *testing.T) { expectedToolCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ - Name: "get_weather", + Index: 0, + Name: "get_weather", Arguments: testArgs(map[string]any{ "location": "Paris", }), @@ -298,7 +299,8 @@ func TestGemma4Parser(t *testing.T) { }, { Function: api.ToolCallFunction{ - Name: "get_weather", + Index: 1, + Name: "get_weather", Arguments: testArgs(map[string]any{ "location": "London", }), diff --git a/model/parsers/lfm2.go b/model/parsers/lfm2.go index 43f926d8a..efac6572a 100644 --- a/model/parsers/lfm2.go +++ b/model/parsers/lfm2.go @@ -29,6 +29,7 @@ const ( type LFM2Parser struct { state LFM2ParserState buffer strings.Builder + callIndex int hasThinkingSupport bool needsThinkingLeadingTrim bool // trim leading whitespace after tag needsContentLeadingTrim bool // trim leading whitespace after 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 { p.toolNames = make(map[string]struct{}, len(tools)) + p.callIndex = 0 p.hasTools = len(tools) > 0 for _, tool := range tools { 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 } diff --git a/model/parsers/lfm2_test.go b/model/parsers/lfm2_test.go index c353424b4..416fcb8ce 100644 --- a/model/parsers/lfm2_test.go +++ b/model/parsers/lfm2_test.go @@ -60,7 +60,8 @@ func TestLFM2Parser(t *testing.T) { expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ - Name: "get_weather", + Index: 0, + Name: "get_weather", Arguments: testArgs(map[string]any{ "location": "Paris", }), @@ -68,7 +69,8 @@ func TestLFM2Parser(t *testing.T) { }, { Function: api.ToolCallFunction{ - Name: "get_weather", + Index: 1, + Name: "get_weather", Arguments: testArgs(map[string]any{ "location": "London", }), @@ -205,7 +207,8 @@ func TestLFM2Parser(t *testing.T) { expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ - Name: "bash", + Index: 0, + Name: "bash", Arguments: testArgs(map[string]any{ "command": "ls", }), @@ -213,7 +216,8 @@ func TestLFM2Parser(t *testing.T) { }, { Function: api.ToolCallFunction{ - Name: "bash", + Index: 1, + Name: "bash", Arguments: testArgs(map[string]any{ "command": "pwd", }), diff --git a/model/parsers/ministral.go b/model/parsers/ministral.go index 5df9ff329..aa588e73a 100644 --- a/model/parsers/ministral.go +++ b/model/parsers/ministral.go @@ -44,6 +44,7 @@ type MinistralParser struct { state ministralParserState buffer strings.Builder tools []api.Tool + callIndex int hasThinkingSupport bool 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 { p.tools = tools + p.callIndex = 0 p.setInitialState(lastMessage) 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 } diff --git a/model/parsers/ministral_test.go b/model/parsers/ministral_test.go index a04590b07..ab3e3eb88 100644 --- a/model/parsers/ministral_test.go +++ b/model/parsers/ministral_test.go @@ -4,6 +4,7 @@ import ( "reflect" "testing" + "github.com/google/go-cmp/cmp" "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) { t.Run("unknown tool returns error", func(t *testing.T) { p := &MinistralParser{} diff --git a/model/parsers/olmo3.go b/model/parsers/olmo3.go index 285a31f62..0fd8d6d21 100644 --- a/model/parsers/olmo3.go +++ b/model/parsers/olmo3.go @@ -26,8 +26,9 @@ const ( ) type Olmo3Parser struct { - state olmo3ParserState - buffer strings.Builder + state olmo3ParserState + buffer strings.Builder + callIndex int } 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 { p.state = olmo3StateContent + p.callIndex = 0 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 } diff --git a/model/parsers/olmo3_test.go b/model/parsers/olmo3_test.go index 1710e3bf3..22c6865de 100644 --- a/model/parsers/olmo3_test.go +++ b/model/parsers/olmo3_test.go @@ -69,12 +69,14 @@ get_weather(location="New York")`, expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ + Index: 0, Name: "get_weather", Arguments: testArgs(map[string]any{"location": "San Francisco"}), }, }, { Function: api.ToolCallFunction{ + Index: 1, Name: "get_weather", Arguments: testArgs(map[string]any{"location": "New York"}), }, diff --git a/model/parsers/qwen3vl.go b/model/parsers/qwen3vl.go index 40bc86106..565e57265 100644 --- a/model/parsers/qwen3vl.go +++ b/model/parsers/qwen3vl.go @@ -28,6 +28,7 @@ type Qwen3VLParser struct { state qwenParserState buffer strings.Builder tools []api.Tool + callIndex int 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 { p.tools = tools + p.callIndex = 0 p.setInitialState(lastMessage) 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 } diff --git a/model/parsers/qwen3vl_nonthinking_test.go b/model/parsers/qwen3vl_nonthinking_test.go index 9b1129d98..cdeeb95b1 100644 --- a/model/parsers/qwen3vl_nonthinking_test.go +++ b/model/parsers/qwen3vl_nonthinking_test.go @@ -4,6 +4,7 @@ import ( "reflect" "testing" + "github.com/google/go-cmp/cmp" "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( + `{"name":"first","arguments":{"a":"1"}}{"name":"second","arguments":{"b":"2"}}`, + 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) { type step struct { input string