diff --git a/model/parsers/glm46.go b/model/parsers/glm46.go
index 05826d9ed..7befc711f 100644
--- a/model/parsers/glm46.go
+++ b/model/parsers/glm46.go
@@ -32,9 +32,10 @@ const (
)
type GLM46Parser struct {
- state glm46ParserState
- buffer strings.Builder
- tools []api.Tool
+ state glm46ParserState
+ buffer strings.Builder
+ tools []api.Tool
+ callIndex int
}
func (p *GLM46Parser) HasToolSupport() bool {
@@ -48,6 +49,7 @@ func (p *GLM46Parser) HasThinkingSupport() bool {
// func (p *GLM46Parser) Init(tools []api.Tool, lastMessage *api.Message) []api.Tool {
func (p *GLM46Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
p.tools = tools
+ p.callIndex = 0
return tools
}
@@ -89,6 +91,8 @@ func (p *GLM46Parser) Add(s string, done bool) (content string, thinking string,
slog.Warn("glm-4.6 tool call parsing failed", "error", err)
return "", "", nil, err
}
+ toolCall.Function.Index = p.callIndex
+ p.callIndex++
toolCalls = append(toolCalls, toolCall)
case glm46EventThinkingContent:
thinkingSb.WriteString(event.content)
diff --git a/model/parsers/glm47.go b/model/parsers/glm47.go
index 4b49934e8..b7e6624a0 100644
--- a/model/parsers/glm47.go
+++ b/model/parsers/glm47.go
@@ -11,6 +11,7 @@ type GLM47Parser struct {
func (p *GLM47Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
p.tools = tools
+ p.callIndex = 0
// When thinking is enabled (nil or true), the prompt ends with ,
// so model output starts directly with thinking content (no opening tag).
if thinkValue == nil || thinkValue.Bool() {
diff --git a/model/parsers/glm47_test.go b/model/parsers/glm47_test.go
index 26c5d7113..0e51bbe42 100644
--- a/model/parsers/glm47_test.go
+++ b/model/parsers/glm47_test.go
@@ -97,3 +97,91 @@ func TestGLM47ParserToolCallEscaping(t *testing.T) {
t.Fatalf("expected %#v, got %#v", expected, toolCall)
}
}
+
+func TestGLM47ParserToolCallIndexing(t *testing.T) {
+ parser := GLM47Parser{}
+ parser.Init(nil, nil, nil)
+
+ input := `plan
+firsta1
+secondb2
+thirdc3`
+
+ _, _, calls, err := parser.Add(input, true)
+ if err != nil {
+ t.Fatalf("parse failed: %v", err)
+ }
+
+ want := []api.ToolCall{
+ {Function: api.ToolCallFunction{Name: "first", Arguments: args(`{"a":"1"}`), Index: 0}},
+ {Function: api.ToolCallFunction{Name: "second", Arguments: args(`{"b":"2"}`), Index: 1}},
+ {Function: api.ToolCallFunction{Name: "third", Arguments: args(`{"c":"3"}`), Index: 2}},
+ }
+ if len(calls) != len(want) {
+ t.Fatalf("expected %d calls, got %d", len(want), len(calls))
+ }
+ for i := range want {
+ if !toolCallEqual(calls[i], want[i]) {
+ t.Fatalf("call %d mismatch: got %#v, want %#v", i, calls[i], want[i])
+ }
+ }
+}
+
+func TestGLM47ParserToolCallIndexingStreaming(t *testing.T) {
+ parser := GLM47Parser{}
+ parser.Init(nil, nil, nil)
+
+ var all []api.ToolCall
+
+ _, _, calls, err := parser.Add("planfirsta1secondb", false)
+ if err != nil {
+ t.Fatalf("step 1 parse failed: %v", err)
+ }
+ all = append(all, calls...)
+
+ _, _, calls, err = parser.Add("2thirdc3", true)
+ if err != nil {
+ t.Fatalf("step 2 parse failed: %v", err)
+ }
+ all = append(all, calls...)
+
+ want := []api.ToolCall{
+ {Function: api.ToolCallFunction{Name: "first", Arguments: args(`{"a":"1"}`), Index: 0}},
+ {Function: api.ToolCallFunction{Name: "second", Arguments: args(`{"b":"2"}`), Index: 1}},
+ {Function: api.ToolCallFunction{Name: "third", Arguments: args(`{"c":"3"}`), Index: 2}},
+ }
+ if len(all) != len(want) {
+ t.Fatalf("expected %d calls, got %d", len(want), len(all))
+ }
+ for i := range want {
+ if !toolCallEqual(all[i], want[i]) {
+ t.Fatalf("call %d mismatch: got %#v, want %#v", i, all[i], want[i])
+ }
+ }
+}
+
+func TestGLM47ParserToolCallIndexResetOnInit(t *testing.T) {
+ parser := GLM47Parser{}
+ parser.Init(nil, nil, nil)
+
+ _, _, _, err := parser.Add("planfirsta1", true)
+ if err != nil {
+ t.Fatalf("first parse failed: %v", err)
+ }
+
+ parser.Init(nil, nil, nil)
+ _, _, calls, err := parser.Add("plansecondb2", true)
+ if err != nil {
+ t.Fatalf("second parse failed: %v", err)
+ }
+
+ want := api.ToolCall{
+ Function: api.ToolCallFunction{Name: "second", Arguments: args(`{"b":"2"}`), Index: 0},
+ }
+ if len(calls) != 1 {
+ t.Fatalf("expected 1 call, got %d", len(calls))
+ }
+ if !toolCallEqual(calls[0], want) {
+ t.Fatalf("got %#v, want %#v", calls[0], want)
+ }
+}
diff --git a/model/parsers/qwen3.go b/model/parsers/qwen3.go
index a2c541059..7e503b232 100644
--- a/model/parsers/qwen3.go
+++ b/model/parsers/qwen3.go
@@ -38,6 +38,7 @@ type Qwen3Parser struct {
state qwen3ParserState
buffer strings.Builder
tools []api.Tool
+ callIndex int
hasThinkingSupport bool
defaultThinking bool
maybeThinkingOpenAtBOL bool
@@ -54,6 +55,7 @@ func (p *Qwen3Parser) HasThinkingSupport() bool {
func (p *Qwen3Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
p.tools = tools
p.buffer.Reset()
+ p.callIndex = 0
thinkingEnabled := thinkValue != nil && thinkValue.Bool()
if thinkValue == nil {
@@ -106,6 +108,8 @@ func (p *Qwen3Parser) Add(s string, done bool) (content string, thinking string,
slog.Warn("qwen3 tool call parsing failed", "error", err)
return "", "", nil, err
}
+ toolCall.Function.Index = p.callIndex
+ p.callIndex++
calls = append(calls, toolCall)
case qwen3EventThinkingContent:
thinkingSb.WriteString(event.content)
diff --git a/model/parsers/qwen3_test.go b/model/parsers/qwen3_test.go
index a1a2a8875..544616201 100644
--- a/model/parsers/qwen3_test.go
+++ b/model/parsers/qwen3_test.go
@@ -230,3 +230,89 @@ func TestQwen35ParserRespectsNoThink(t *testing.T) {
t.Fatalf("expected no tool calls, got %d", len(calls))
}
}
+
+func TestQwen3ParserToolCallIndexing(t *testing.T) {
+ parser := &Qwen3Parser{hasThinkingSupport: false, defaultThinking: false}
+ parser.Init(nil, nil, &api.ThinkValue{Value: false})
+
+ input := `{"name":"first","arguments":{"a":"1"}}
+{"name":"second","arguments":{"b":"2"}}
+{"name":"third","arguments":{"c":"3"}}`
+ _, _, calls, err := parser.Add(input, true)
+ if err != nil {
+ t.Fatalf("parse failed: %v", err)
+ }
+
+ want := []api.ToolCall{
+ {Function: api.ToolCallFunction{Name: "first", Arguments: args(`{"a":"1"}`), Index: 0}},
+ {Function: api.ToolCallFunction{Name: "second", Arguments: args(`{"b":"2"}`), Index: 1}},
+ {Function: api.ToolCallFunction{Name: "third", Arguments: args(`{"c":"3"}`), Index: 2}},
+ }
+ if len(calls) != len(want) {
+ t.Fatalf("expected %d calls, got %d", len(want), len(calls))
+ }
+ for i := range want {
+ if !toolCallEqual(calls[i], want[i]) {
+ t.Fatalf("call %d mismatch: got %#v, want %#v", i, calls[i], want[i])
+ }
+ }
+}
+
+func TestQwen3ParserToolCallIndexingStreaming(t *testing.T) {
+ parser := &Qwen3Parser{hasThinkingSupport: false, defaultThinking: false}
+ parser.Init(nil, nil, &api.ThinkValue{Value: false})
+
+ var all []api.ToolCall
+
+ _, _, calls, err := parser.Add(`{"name":"first","arguments":{"a":"1"}}{"name":"second","arguments":{"b":"2"}`, false)
+ if err != nil {
+ t.Fatalf("step 1 parse failed: %v", err)
+ }
+ all = append(all, calls...)
+
+ _, _, calls, err = parser.Add(`}{"name":"third","arguments":{"c":"3"}}`, true)
+ if err != nil {
+ t.Fatalf("step 2 parse failed: %v", err)
+ }
+ all = append(all, calls...)
+
+ want := []api.ToolCall{
+ {Function: api.ToolCallFunction{Name: "first", Arguments: args(`{"a":"1"}`), Index: 0}},
+ {Function: api.ToolCallFunction{Name: "second", Arguments: args(`{"b":"2"}`), Index: 1}},
+ {Function: api.ToolCallFunction{Name: "third", Arguments: args(`{"c":"3"}`), Index: 2}},
+ }
+ if len(all) != len(want) {
+ t.Fatalf("expected %d calls, got %d", len(want), len(all))
+ }
+ for i := range want {
+ if !toolCallEqual(all[i], want[i]) {
+ t.Fatalf("call %d mismatch: got %#v, want %#v", i, all[i], want[i])
+ }
+ }
+}
+
+func TestQwen3ParserToolCallIndexResetOnInit(t *testing.T) {
+ parser := &Qwen3Parser{hasThinkingSupport: false, defaultThinking: false}
+ parser.Init(nil, nil, &api.ThinkValue{Value: false})
+
+ _, _, _, err := parser.Add(`{"name":"first","arguments":{"a":"1"}}`, true)
+ if err != nil {
+ t.Fatalf("first parse failed: %v", err)
+ }
+
+ parser.Init(nil, nil, &api.ThinkValue{Value: false})
+ _, _, calls, err := parser.Add(`{"name":"second","arguments":{"b":"2"}}`, true)
+ if err != nil {
+ t.Fatalf("second parse failed: %v", err)
+ }
+
+ want := api.ToolCall{
+ Function: api.ToolCallFunction{Name: "second", Arguments: args(`{"b":"2"}`), Index: 0},
+ }
+ if len(calls) != 1 {
+ t.Fatalf("expected 1 call, got %d", len(calls))
+ }
+ if !toolCallEqual(calls[0], want) {
+ t.Fatalf("got %#v, want %#v", calls[0], want)
+ }
+}
diff --git a/model/parsers/qwen3coder.go b/model/parsers/qwen3coder.go
index 5604988ec..dfa604acc 100644
--- a/model/parsers/qwen3coder.go
+++ b/model/parsers/qwen3coder.go
@@ -29,9 +29,10 @@ const (
)
type Qwen3CoderParser struct {
- state qwenParserState
- acc strings.Builder
- tools []api.Tool
+ state qwenParserState
+ acc strings.Builder
+ tools []api.Tool
+ callIndex int
}
func (p *Qwen3CoderParser) HasToolSupport() bool {
@@ -44,6 +45,7 @@ func (p *Qwen3CoderParser) HasThinkingSupport() bool {
func (p *Qwen3CoderParser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
p.tools = tools
+ p.callIndex = 0
return tools // Qwen doesn't modify tools
}
@@ -62,6 +64,8 @@ func (p *Qwen3CoderParser) Add(s string, done bool) (content string, thinking st
slog.Warn("qwen tool call parsing failed", "error", err)
return "", "", nil, err
}
+ toolCall.Function.Index = p.callIndex
+ p.callIndex++
toolCalls = append(toolCalls, toolCall)
case qwenEventContent:
// TODO(drifkin): if the same turn contains multiple interleaved content
diff --git a/model/parsers/qwen3coder_test.go b/model/parsers/qwen3coder_test.go
index 24ccc5e9e..7142567ed 100644
--- a/model/parsers/qwen3coder_test.go
+++ b/model/parsers/qwen3coder_test.go
@@ -1035,6 +1035,92 @@ func TestQwenToolCallValueParsing(t *testing.T) {
}
}
+func TestQwen3CoderParserToolCallIndexing(t *testing.T) {
+ parser := Qwen3CoderParser{}
+ parser.Init(nil, nil, nil)
+
+ input := `1
+2
+3`
+ _, _, calls, err := parser.Add(input, true)
+ if err != nil {
+ t.Fatalf("parse failed: %v", err)
+ }
+
+ want := []api.ToolCall{
+ {Function: api.ToolCallFunction{Name: "first", Arguments: testArgs(map[string]any{"a": "1"}), Index: 0}},
+ {Function: api.ToolCallFunction{Name: "second", Arguments: testArgs(map[string]any{"b": "2"}), Index: 1}},
+ {Function: api.ToolCallFunction{Name: "third", Arguments: testArgs(map[string]any{"c": "3"}), Index: 2}},
+ }
+ if len(calls) != len(want) {
+ t.Fatalf("expected %d calls, got %d", len(want), len(calls))
+ }
+ for i := range want {
+ if !toolCallEqual(calls[i], want[i]) {
+ t.Fatalf("call %d mismatch: got %#v, want %#v", i, calls[i], want[i])
+ }
+ }
+}
+
+func TestQwen3CoderParserToolCallIndexingStreaming(t *testing.T) {
+ parser := Qwen3CoderParser{}
+ parser.Init(nil, nil, nil)
+
+ var all []api.ToolCall
+
+ _, _, calls, err := parser.Add("1", false)
+ if err != nil {
+ t.Fatalf("step 1 parse failed: %v", err)
+ }
+ all = append(all, calls...)
+
+ _, _, calls, err = parser.Add("23", true)
+ if err != nil {
+ t.Fatalf("step 2 parse failed: %v", err)
+ }
+ all = append(all, calls...)
+
+ want := []api.ToolCall{
+ {Function: api.ToolCallFunction{Name: "first", Arguments: testArgs(map[string]any{"a": "1"}), Index: 0}},
+ {Function: api.ToolCallFunction{Name: "second", Arguments: testArgs(map[string]any{"b": "2"}), Index: 1}},
+ {Function: api.ToolCallFunction{Name: "third", Arguments: testArgs(map[string]any{"c": "3"}), Index: 2}},
+ }
+ if len(all) != len(want) {
+ t.Fatalf("expected %d calls, got %d", len(want), len(all))
+ }
+ for i := range want {
+ if !toolCallEqual(all[i], want[i]) {
+ t.Fatalf("call %d mismatch: got %#v, want %#v", i, all[i], want[i])
+ }
+ }
+}
+
+func TestQwen3CoderParserToolCallIndexResetOnInit(t *testing.T) {
+ parser := Qwen3CoderParser{}
+ parser.Init(nil, nil, nil)
+
+ _, _, _, err := parser.Add("1", true)
+ if err != nil {
+ t.Fatalf("first parse failed: %v", err)
+ }
+
+ parser.Init(nil, nil, nil)
+ _, _, calls, err := parser.Add("2", true)
+ if err != nil {
+ t.Fatalf("second parse failed: %v", err)
+ }
+
+ want := api.ToolCall{
+ Function: api.ToolCallFunction{Name: "second", Arguments: testArgs(map[string]any{"b": "2"}), Index: 0},
+ }
+ if len(calls) != 1 {
+ t.Fatalf("expected 1 call, got %d", len(calls))
+ }
+ if !toolCallEqual(calls[0], want) {
+ t.Fatalf("got %#v, want %#v", calls[0], want)
+ }
+}
+
func TestQwenXMLTransform(t *testing.T) {
cases := []struct {
desc string