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