diff --git a/internal/llm/provider_openai.go b/internal/llm/provider_openai.go index 65125f47cffcb4940dd6e1daa5f037f325c2b27d..9b4685a182e5716148963adda6ac77ab0db45cd1 100644 --- a/internal/llm/provider_openai.go +++ b/internal/llm/provider_openai.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "net/http" + "sort" "strings" "time" @@ -1122,9 +1123,15 @@ func (c *OpenAIClient) processStreamResponse(body io.Reader, eventChan chan<- St ) } - // Add completed tool calls - for i := 0; i < len(toolCallsMap); i++ { - if tc, exists := toolCallsMap[i]; exists { + // Add completed tool calls in provider index order. Indexes are not + // guaranteed to be contiguous across OpenAI-compatible providers. + toolCallIndexes := make([]int, 0, len(toolCallsMap)) + for index := range toolCallsMap { + toolCallIndexes = append(toolCallIndexes, index) + } + sort.Ints(toolCallIndexes) + for _, i := range toolCallIndexes { + if tc := toolCallsMap[i]; tc != nil { // Parse accumulated arguments if argsBuilder, ok := toolCallArgsMap[i]; ok { argsStr := argsBuilder.String() diff --git a/internal/llm/provider_openai_stream_sparse_index_test.go b/internal/llm/provider_openai_stream_sparse_index_test.go new file mode 100644 index 0000000000000000000000000000000000000000..aefb31d5b80ddcbb2744f6e1acf6c7528fb656d2 --- /dev/null +++ b/internal/llm/provider_openai_stream_sparse_index_test.go @@ -0,0 +1,31 @@ +package llm + +import ( + "strings" + "testing" +) + +func TestProcessStreamPreservesSparseToolCallIndexes(t *testing.T) { + chunk := `{"choices":[{"delta":{"tool_calls":[{"index":2,"id":"call_2","function":{"name":"second","arguments":"{}"}},{"index":0,"id":"call_0","function":{"name":"first","arguments":"{}"}}]},"finish_reason":"tool_calls"}]}` + stream := "data: " + chunk + "\n\ndata: [DONE]\n\n" + events := make(chan StreamEvent, 16) + + response, err := (&OpenAIClient{}).processStreamResponse(strings.NewReader(stream), events) + close(events) + + if err != nil { + t.Fatalf("processStreamResponse returned error: %v", err) + } + var calls []*FunctionCall + for _, part := range response.Candidates[0].Content.Parts { + if part.FunctionCall != nil { + calls = append(calls, part.FunctionCall) + } + } + if len(calls) != 2 { + t.Fatalf("got %d tool calls, want 2", len(calls)) + } + if calls[0].Name != "first" || calls[1].Name != "second" { + t.Fatalf("tool call order = [%q, %q], want [first, second]", calls[0].Name, calls[1].Name) + } +}