@@ -6,6 +6,7 @@ import (
@@ -14,6 +15,7 @@ import (
14 "github.com/charmbracelet/crush/internal/llm/tools"
15 "github.com/charmbracelet/crush/internal/llm/tools"
15 "github.com/charmbracelet/crush/internal/log"
16 "github.com/charmbracelet/crush/internal/log"
16 "github.com/charmbracelet/crush/internal/message"
17 "github.com/charmbracelet/crush/internal/message"
18+ "github.com/google/uuid"
17 "github.com/openai/openai-go"
19 "github.com/openai/openai-go"
18 "github.com/openai/openai-go/option"
20 "github.com/openai/openai-go/option"
19 "github.com/openai/openai-go/packages/param"
21 "github.com/openai/openai-go/packages/param"
@@ -338,21 +340,16 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
338 acc := openai.ChatCompletionAccumulator{}
340 acc := openai.ChatCompletionAccumulator{}
339 currentContent := ""
341 currentContent := ""
340 toolCalls := make([]message.ToolCall, 0)
342 toolCalls := make([]message.ToolCall, 0)
342− var currentToolCallID string
343− var currentToolCall openai.ChatCompletionMessageToolCall
344 var msgToolCalls []openai.ChatCompletionMessageToolCall
343 var msgToolCalls []openai.ChatCompletionMessageToolCall
345− currentToolIndex := 0
346 for openaiStream.Next() {
344 for openaiStream.Next() {
347 chunk := openaiStream.Current()
345 chunk := openaiStream.Current()
348 // Kujtim: this is an issue with openrouter qwen, its sending -1 for the tool index
346 // Kujtim: this is an issue with openrouter qwen, its sending -1 for the tool index
349 if len(chunk.Choices) > 0 && len(chunk.Choices[0].Delta.ToolCalls) > 0 && chunk.Choices[0].Delta.ToolCalls[0].Index == -1 {
347 if len(chunk.Choices) > 0 && len(chunk.Choices[0].Delta.ToolCalls) > 0 && chunk.Choices[0].Delta.ToolCalls[0].Index == -1 {
350− chunk.Choices[0].Delta.ToolCalls[0].Index = int64(currentToolIndex)
348+ chunk.Choices[0].Delta.ToolCalls[0].Index = 0
353 acc.AddChunk(chunk)
350 acc.AddChunk(chunk)
354 // This fixes multiple tool calls for some providers
351 // This fixes multiple tool calls for some providers
355− for _, choice := range chunk.Choices {
352+ for i, choice := range chunk.Choices {
356 if choice.Delta.Content != "" {
353 if choice.Delta.Content != "" {
357 eventChan <- ProviderEvent{
354 eventChan <- ProviderEvent{
358 Type: EventContentDelta,
355 Type: EventContentDelta,
@@ -361,63 +358,50 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
361 currentContent += choice.Delta.Content
358 currentContent += choice.Delta.Content
362 } else if len(choice.Delta.ToolCalls) > 0 {
359 } else if len(choice.Delta.ToolCalls) > 0 {
363 toolCall := choice.Delta.ToolCalls[0]
360 toolCall := choice.Delta.ToolCalls[0]
364− // Detect tool use start
361+ newToolCall := false
365− if currentToolCallID == "" {
362+ if len(msgToolCalls)-1 >= int(toolCall.Index) { // tool call exists
363+ existingToolCall := msgToolCalls[toolCall.Index]
364+ if toolCall.ID != "" && toolCall.ID != existingToolCall.ID {
366+ // try to find the tool based on the ID
367+ for i, tool := range msgToolCalls {
366− if toolCall.ID != "" {
368+ if tool.ID == toolCall.ID {
367− currentToolCallID = toolCall.ID
369+ msgToolCalls[i].Function.Arguments += toolCall.Function.Arguments
368− eventChan <- ProviderEvent{
370+ found = true
369− Type: EventToolUseStart,
370− ToolCall: &message.ToolCall{
372− Name: toolCall.Function.Name,
376− currentToolCall = openai.ChatCompletionMessageToolCall{
373+ if !found {
377− ID: toolCall.ID,
374+ newToolCall = true
379− Function: openai.ChatCompletionMessageToolCallFunction{
380− Name: toolCall.Function.Name,
381− Arguments: toolCall.Function.Arguments,
387− if toolCall.ID == "" || toolCall.ID == currentToolCallID {
388− currentToolCall.Function.Arguments += toolCall.Function.Arguments
390− // Detect new tool use
377+ msgToolCalls[toolCall.Index].Function.Arguments += toolCall.Function.Arguments
391− if toolCall.ID != currentToolCallID {
392− msgToolCalls = append(msgToolCalls, currentToolCall)
393− currentToolCallID = toolCall.ID
394− eventChan <- ProviderEvent{
395− Type: EventToolUseStart,
396− ToolCall: &message.ToolCall{
398− Name: toolCall.Function.Name,
402− currentToolCall = openai.ChatCompletionMessageToolCall{
405− Function: openai.ChatCompletionMessageToolCallFunction{
406− Name: toolCall.Function.Name,
407− Arguments: toolCall.Function.Arguments,
413− }
382+ if newToolCall { // new tool call
414− // Kujtim: some models send finish stop even for tool calls
383+ if toolCall.ID == "" {
415− if choice.FinishReason == "tool_calls" || (choice.FinishReason == "stop" && currentToolCallID != "") {
384+ toolCall.ID = uuid.NewString()
386+ eventChan <- ProviderEvent{
387+ Type: EventToolUseStart,
388+ ToolCall: &message.ToolCall{
390+ Name: toolCall.Function.Name,
416− msgToolCalls = append(msgToolCalls, currentToolCall)
394+ msgToolCalls = append(msgToolCalls, openai.ChatCompletionMessageToolCall{
417− if len(acc.Choices) > 0 {
395+ ID: toolCall.ID,
418− acc.Choices[0].Message.ToolCalls = msgToolCalls
396+ Type: "function",
397+ Function: openai.ChatCompletionMessageToolCallFunction{
398+ Name: toolCall.Function.Name,
399+ Arguments: toolCall.Function.Arguments,
404+ acc.Choices[i].Message.ToolCalls = slices.Clone(msgToolCalls)