@@ -2,10 +2,12 @@ package provider
22
33import (
44 "context"
5+ "encoding/json"
56 "errors"
67 "fmt"
78 "io"
89 "log/slog"
10+ "slices"
911 "strings"
1012 "time"
1113
@@ -14,6 +16,7 @@ import (
1416 "github.com/charmbracelet/crush/internal/llm/tools"
1517 "github.com/charmbracelet/crush/internal/log"
1618 "github.com/charmbracelet/crush/internal/message"
19+ "github.com/google/uuid"
1720 "github.com/openai/openai-go"
1821 "github.com/openai/openai-go/option"
1922 "github.com/openai/openai-go/packages/param"
@@ -70,8 +73,9 @@ func (o *openaiClient) convertMessages(messages []message.Message) (openaiMessag
7073 systemMessage = o.providerOptions.systemPromptPrefix + "\n" + systemMessage
7174 }
7275
73− systemTextBlock := openai.ChatCompletionContentPartTextParam{Text: systemMessage}
76+ system := openai.SystemMessage(systemMessage)
7477 if isAnthropicModel && !o.providerOptions.disableCache {
78+ systemTextBlock := openai.ChatCompletionContentPartTextParam{Text: systemMessage}
7579 systemTextBlock.SetExtraFields(
7680 map[string]any{
7781 "cache_control": map[string]string{
@@ -79,10 +83,10 @@ func (o *openaiClient) convertMessages(messages []message.Message) (openaiMessag
7983 },
8084 },
8185 )
86+ var content []openai.ChatCompletionContentPartTextParam
87+ content = append(content, systemTextBlock)
88+ system = openai.SystemMessage(content)
8289 }
83− var content []openai.ChatCompletionContentPartTextParam
84− content = append(content, systemTextBlock)
85− system := openai.SystemMessage(content)
8690 openaiMessages = append(openaiMessages, system)
8791
8892 for i, msg := range messages {
@@ -93,9 +97,12 @@ func (o *openaiClient) convertMessages(messages []message.Message) (openaiMessag
9397 switch msg.Role {
9498 case message.User:
9599 var content []openai.ChatCompletionContentPartUnionParam
100+
96101 textBlock := openai.ChatCompletionContentPartTextParam{Text: msg.Content().String()}
97102 content = append(content, openai.ChatCompletionContentPartUnionParam{OfText: &textBlock})
103+ hasBinaryContent := false
98104 for _, binaryContent := range msg.BinaryContent() {
105+ hasBinaryContent = true
99106 imageURL := openai.ChatCompletionContentPartImageImageURLParam{URL: binaryContent.String(catwalk.InferenceProviderOpenAI)}
100107 imageBlock := openai.ChatCompletionContentPartImageParam{ImageURL: imageURL}
101108
@@ -108,8 +115,11 @@ func (o *openaiClient) convertMessages(messages []message.Message) (openaiMessag
108115 },
109116 })
110117 }
111−
112− openaiMessages = append(openaiMessages, openai.UserMessage(content))
118+ if hasBinaryContent || (isAnthropicModel && !o.providerOptions.disableCache) {
119+ openaiMessages = append(openaiMessages, openai.UserMessage(content))
120+ } else {
121+ openaiMessages = append(openaiMessages, openai.UserMessage(msg.Content().String()))
122+ }
113123
114124 case message.Assistant:
115125 assistantMsg := openai.ChatCompletionAssistantMessageParam{
@@ -134,13 +144,15 @@ func (o *openaiClient) convertMessages(messages []message.Message) (openaiMessag
134144 },
135145 },
136146 }
147+ if !isAnthropicModel {
148+ assistantMsg.Content = openai.ChatCompletionAssistantMessageParamContentUnion{
149+ OfString: param.NewOpt(msg.Content().String()),
150+ }
151+ }
137152 }
138153
139154 if len(msg.ToolCalls()) > 0 {
140155 hasContent = true
141− assistantMsg.Content = openai.ChatCompletionAssistantMessageParamContentUnion{
142− OfString: param.NewOpt(msg.Content().String()),
143− }
144156 assistantMsg.ToolCalls = make([]openai.ChatCompletionMessageToolCallParam, len(msg.ToolCalls()))
145157 for i, call := range msg.ToolCalls() {
146158 assistantMsg.ToolCalls[i] = openai.ChatCompletionMessageToolCallParam{
@@ -329,21 +341,26 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
329341 acc := openai.ChatCompletionAccumulator{}
330342 currentContent := ""
331343 toolCalls := make([]message.ToolCall, 0)
332−
333− var currentToolCallID string
334− var currentToolCall openai.ChatCompletionMessageToolCall
335344 var msgToolCalls []openai.ChatCompletionMessageToolCall
336− currentToolIndex := 0
337345 for openaiStream.Next() {
338346 chunk := openaiStream.Current()
339347 // Kujtim: this is an issue with openrouter qwen, its sending -1 for the tool index
340348 if len(chunk.Choices) > 0 && len(chunk.Choices[0].Delta.ToolCalls) > 0 && chunk.Choices[0].Delta.ToolCalls[0].Index == -1 {
341− chunk.Choices[0].Delta.ToolCalls[0].Index = int64(currentToolIndex)
342− currentToolIndex++
349+ chunk.Choices[0].Delta.ToolCalls[0].Index = 0
343350 }
344351 acc.AddChunk(chunk)
345− // This fixes multiple tool calls for some providers
346− for _, choice := range chunk.Choices {
352+ for i, choice := range chunk.Choices {
353+ reasoning, ok := choice.Delta.JSON.ExtraFields["reasoning"]
354+ if ok && reasoning.Raw() != "" {
355+ reasoningStr := ""
356+ json.Unmarshal([]byte(reasoning.Raw()), &reasoningStr)
357+ if reasoningStr != "" {
358+ eventChan <- ProviderEvent{
359+ Type: EventThinkingDelta,
360+ Thinking: reasoningStr,
361+ }
362+ }
363+ }
347364 if choice.Delta.Content != "" {
348365 eventChan <- ProviderEvent{
349366 Type: EventContentDelta,
@@ -352,63 +369,50 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
352369 currentContent += choice.Delta.Content
353370 } else if len(choice.Delta.ToolCalls) > 0 {
354371 toolCall := choice.Delta.ToolCalls[0]
355− // Detect tool use start
356− if currentToolCallID == "" {
357− if toolCall.ID != "" {
358− currentToolCallID = toolCall.ID
359− eventChan <- ProviderEvent{
360− Type: EventToolUseStart,
361− ToolCall: &message.ToolCall{
362− ID: toolCall.ID,
363− Name: toolCall.Function.Name,
364− Finished: false,
365− },
372+ newToolCall := false
373+ if len(msgToolCalls)-1 >= int(toolCall.Index) { // tool call exists
374+ existingToolCall := msgToolCalls[toolCall.Index]
375+ if toolCall.ID != "" && toolCall.ID != existingToolCall.ID {
376+ found := false
377+ // try to find the tool based on the ID
378+ for i, tool := range msgToolCalls {
379+ if tool.ID == toolCall.ID {
380+ msgToolCalls[i].Function.Arguments += toolCall.Function.Arguments
381+ found = true
382+ }
366383 }
367− currentToolCall = openai.ChatCompletionMessageToolCall{
368− ID: toolCall.ID,
369− Type: "function",
370− Function: openai.ChatCompletionMessageToolCallFunction{
371− Name: toolCall.Function.Name,
372− Arguments: toolCall.Function.Arguments,
373− },
384+ if !found {
385+ newToolCall = true
374386 }
375− }
376− } else {
377− // Delta tool use
378− if toolCall.ID == "" || toolCall.ID == currentToolCallID {
379− currentToolCall.Function.Arguments += toolCall.Function.Arguments
380387 } else {
381− // Detect new tool use
382− if toolCall.ID != currentToolCallID {
383− msgToolCalls = append(msgToolCalls, currentToolCall)
384− currentToolCallID = toolCall.ID
385− eventChan <- ProviderEvent{
386− Type: EventToolUseStart,
387− ToolCall: &message.ToolCall{
388− ID: toolCall.ID,
389− Name: toolCall.Function.Name,
390− Finished: false,
391− },
392− }
393− currentToolCall = openai.ChatCompletionMessageToolCall{
394− ID: toolCall.ID,
395− Type: "function",
396− Function: openai.ChatCompletionMessageToolCallFunction{
397− Name: toolCall.Function.Name,
398− Arguments: toolCall.Function.Arguments,
399− },
400− }
401− }
388+ msgToolCalls[toolCall.Index].Function.Arguments += toolCall.Function.Arguments
402389 }
390+ } else {
391+ newToolCall = true
403392 }
404− }
405− // Kujtim: some models send finish stop even for tool calls
406− if choice.FinishReason == "tool_calls" || (choice.FinishReason == "stop" && currentToolCallID != "") {
407− msgToolCalls = append(msgToolCalls, currentToolCall)
408− if len(acc.Choices) > 0 {
409− acc.Choices[0].Message.ToolCalls = msgToolCalls
393+ if newToolCall { // new tool call
394+ if toolCall.ID == "" {
395+ toolCall.ID = uuid.NewString()
396+ }
397+ eventChan <- ProviderEvent{
398+ Type: EventToolUseStart,
399+ ToolCall: &message.ToolCall{
400+ ID: toolCall.ID,
401+ Name: toolCall.Function.Name,
402+ Finished: false,
403+ },
404+ }
405+ msgToolCalls = append(msgToolCalls, openai.ChatCompletionMessageToolCall{
406+ ID: toolCall.ID,
407+ Type: "function",
408+ Function: openai.ChatCompletionMessageToolCallFunction{
409+ Name: toolCall.Function.Name,
410+ Arguments: toolCall.Function.Arguments,
411+ },
412+ })
410413 }
411414 }
415+ acc.Choices[i].Message.ToolCalls = slices.Clone(msgToolCalls)
412416 }
413417 }
414418