@@ -2,10 +2,12 @@ package provider
@@ -14,6 +16,7 @@ import (
14 "github.com/charmbracelet/crush/internal/llm/tools"
16 "github.com/charmbracelet/crush/internal/llm/tools"
15 "github.com/charmbracelet/crush/internal/log"
17 "github.com/charmbracelet/crush/internal/log"
16 "github.com/charmbracelet/crush/internal/message"
18 "github.com/charmbracelet/crush/internal/message"
19+ "github.com/google/uuid"
17 "github.com/openai/openai-go"
20 "github.com/openai/openai-go"
18 "github.com/openai/openai-go/option"
21 "github.com/openai/openai-go/option"
19 "github.com/openai/openai-go/packages/param"
22 "github.com/openai/openai-go/packages/param"
@@ -70,8 +73,9 @@ func (o *openaiClient) convertMessages(messages []message.Message) (openaiMessag
70 systemMessage = o.providerOptions.systemPromptPrefix + "\n" + systemMessage
73 systemMessage = o.providerOptions.systemPromptPrefix + "\n" + systemMessage
73− systemTextBlock := openai.ChatCompletionContentPartTextParam{Text: systemMessage}
76+ system := openai.SystemMessage(systemMessage)
74 if isAnthropicModel && !o.providerOptions.disableCache {
77 if isAnthropicModel && !o.providerOptions.disableCache {
78+ systemTextBlock := openai.ChatCompletionContentPartTextParam{Text: systemMessage}
75 systemTextBlock.SetExtraFields(
79 systemTextBlock.SetExtraFields(
76 map[string]any{
80 map[string]any{
77 "cache_control": map[string]string{
81 "cache_control": map[string]string{
@@ -79,10 +83,10 @@ func (o *openaiClient) convertMessages(messages []message.Message) (openaiMessag
86+ var content []openai.ChatCompletionContentPartTextParam
87+ content = append(content, systemTextBlock)
88+ system = openai.SystemMessage(content)
83− var content []openai.ChatCompletionContentPartTextParam
84− content = append(content, systemTextBlock)
85− system := openai.SystemMessage(content)
86 openaiMessages = append(openaiMessages, system)
90 openaiMessages = append(openaiMessages, system)
88 for i, msg := range messages {
92 for i, msg := range messages {
@@ -93,9 +97,12 @@ func (o *openaiClient) convertMessages(messages []message.Message) (openaiMessag
93 switch msg.Role {
97 switch msg.Role {
94 case message.User:
98 case message.User:
95 var content []openai.ChatCompletionContentPartUnionParam
99 var content []openai.ChatCompletionContentPartUnionParam
96 textBlock := openai.ChatCompletionContentPartTextParam{Text: msg.Content().String()}
101 textBlock := openai.ChatCompletionContentPartTextParam{Text: msg.Content().String()}
97 content = append(content, openai.ChatCompletionContentPartUnionParam{OfText: &textBlock})
102 content = append(content, openai.ChatCompletionContentPartUnionParam{OfText: &textBlock})
103+ hasBinaryContent := false
98 for _, binaryContent := range msg.BinaryContent() {
104 for _, binaryContent := range msg.BinaryContent() {
105+ hasBinaryContent = true
99 imageURL := openai.ChatCompletionContentPartImageImageURLParam{URL: binaryContent.String(catwalk.InferenceProviderOpenAI)}
106 imageURL := openai.ChatCompletionContentPartImageImageURLParam{URL: binaryContent.String(catwalk.InferenceProviderOpenAI)}
100 imageBlock := openai.ChatCompletionContentPartImageParam{ImageURL: imageURL}
107 imageBlock := openai.ChatCompletionContentPartImageParam{ImageURL: imageURL}
@@ -108,8 +115,11 @@ func (o *openaiClient) convertMessages(messages []message.Message) (openaiMessag
111−
118+ if hasBinaryContent || (isAnthropicModel && !o.providerOptions.disableCache) {
112− openaiMessages = append(openaiMessages, openai.UserMessage(content))
119+ openaiMessages = append(openaiMessages, openai.UserMessage(content))
121+ openaiMessages = append(openaiMessages, openai.UserMessage(msg.Content().String()))
114 case message.Assistant:
124 case message.Assistant:
115 assistantMsg := openai.ChatCompletionAssistantMessageParam{
125 assistantMsg := openai.ChatCompletionAssistantMessageParam{
@@ -134,13 +144,15 @@ func (o *openaiClient) convertMessages(messages []message.Message) (openaiMessag
147+ if !isAnthropicModel {
148+ assistantMsg.Content = openai.ChatCompletionAssistantMessageParamContentUnion{
149+ OfString: param.NewOpt(msg.Content().String()),
139 if len(msg.ToolCalls()) > 0 {
154 if len(msg.ToolCalls()) > 0 {
140 hasContent = true
155 hasContent = true
141− assistantMsg.Content = openai.ChatCompletionAssistantMessageParamContentUnion{
142− OfString: param.NewOpt(msg.Content().String()),
144 assistantMsg.ToolCalls = make([]openai.ChatCompletionMessageToolCallParam, len(msg.ToolCalls()))
156 assistantMsg.ToolCalls = make([]openai.ChatCompletionMessageToolCallParam, len(msg.ToolCalls()))
145 for i, call := range msg.ToolCalls() {
157 for i, call := range msg.ToolCalls() {
146 assistantMsg.ToolCalls[i] = openai.ChatCompletionMessageToolCallParam{
158 assistantMsg.ToolCalls[i] = openai.ChatCompletionMessageToolCallParam{
@@ -329,21 +341,26 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
329 acc := openai.ChatCompletionAccumulator{}
341 acc := openai.ChatCompletionAccumulator{}
330 currentContent := ""
342 currentContent := ""
331 toolCalls := make([]message.ToolCall, 0)
343 toolCalls := make([]message.ToolCall, 0)
333− var currentToolCallID string
334− var currentToolCall openai.ChatCompletionMessageToolCall
335 var msgToolCalls []openai.ChatCompletionMessageToolCall
344 var msgToolCalls []openai.ChatCompletionMessageToolCall
336− currentToolIndex := 0
337 for openaiStream.Next() {
345 for openaiStream.Next() {
338 chunk := openaiStream.Current()
346 chunk := openaiStream.Current()
339 // Kujtim: this is an issue with openrouter qwen, its sending -1 for the tool index
347 // Kujtim: this is an issue with openrouter qwen, its sending -1 for the tool index
340 if len(chunk.Choices) > 0 && len(chunk.Choices[0].Delta.ToolCalls) > 0 && chunk.Choices[0].Delta.ToolCalls[0].Index == -1 {
348 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)
349+ chunk.Choices[0].Delta.ToolCalls[0].Index = 0
344 acc.AddChunk(chunk)
351 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() != "" {
356+ json.Unmarshal([]byte(reasoning.Raw()), &reasoningStr)
357+ if reasoningStr != "" {
358+ eventChan <- ProviderEvent{
359+ Type: EventThinkingDelta,
360+ Thinking: reasoningStr,
347 if choice.Delta.Content != "" {
364 if choice.Delta.Content != "" {
348 eventChan <- ProviderEvent{
365 eventChan <- ProviderEvent{
349 Type: EventContentDelta,
366 Type: EventContentDelta,
@@ -352,63 +369,50 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
352 currentContent += choice.Delta.Content
369 currentContent += choice.Delta.Content
353 } else if len(choice.Delta.ToolCalls) > 0 {
370 } else if len(choice.Delta.ToolCalls) > 0 {
354 toolCall := choice.Delta.ToolCalls[0]
371 toolCall := choice.Delta.ToolCalls[0]
355− // Detect tool use start
372+ newToolCall := false
356− if currentToolCallID == "" {
373+ if len(msgToolCalls)-1 >= int(toolCall.Index) { // tool call exists
374+ existingToolCall := msgToolCalls[toolCall.Index]
375+ if toolCall.ID != "" && toolCall.ID != existingToolCall.ID {
377+ // try to find the tool based on the ID
378+ for i, tool := range msgToolCalls {
357− if toolCall.ID != "" {
379+ if tool.ID == toolCall.ID {
358− currentToolCallID = toolCall.ID
380+ msgToolCalls[i].Function.Arguments += toolCall.Function.Arguments
359− eventChan <- ProviderEvent{
381+ found = true
360− Type: EventToolUseStart,
361− ToolCall: &message.ToolCall{
363− Name: toolCall.Function.Name,
367− currentToolCall = openai.ChatCompletionMessageToolCall{
384+ if !found {
368− ID: toolCall.ID,
385+ newToolCall = true
370− Function: openai.ChatCompletionMessageToolCallFunction{
371− Name: toolCall.Function.Name,
372− Arguments: toolCall.Function.Arguments,
378− if toolCall.ID == "" || toolCall.ID == currentToolCallID {
379− currentToolCall.Function.Arguments += toolCall.Function.Arguments
381− // Detect new tool use
388+ msgToolCalls[toolCall.Index].Function.Arguments += toolCall.Function.Arguments
382− if toolCall.ID != currentToolCallID {
383− msgToolCalls = append(msgToolCalls, currentToolCall)
384− currentToolCallID = toolCall.ID
385− eventChan <- ProviderEvent{
386− Type: EventToolUseStart,
387− ToolCall: &message.ToolCall{
389− Name: toolCall.Function.Name,
393− currentToolCall = openai.ChatCompletionMessageToolCall{
396− Function: openai.ChatCompletionMessageToolCallFunction{
397− Name: toolCall.Function.Name,
398− Arguments: toolCall.Function.Arguments,
404− }
393+ if newToolCall { // new tool call
405− // Kujtim: some models send finish stop even for tool calls
394+ if toolCall.ID == "" {
406− if choice.FinishReason == "tool_calls" || (choice.FinishReason == "stop" && currentToolCallID != "") {
395+ toolCall.ID = uuid.NewString()
397+ eventChan <- ProviderEvent{
398+ Type: EventToolUseStart,
399+ ToolCall: &message.ToolCall{
401+ Name: toolCall.Function.Name,
407− msgToolCalls = append(msgToolCalls, currentToolCall)
405+ msgToolCalls = append(msgToolCalls, openai.ChatCompletionMessageToolCall{
408− if len(acc.Choices) > 0 {
406+ ID: toolCall.ID,
409− acc.Choices[0].Message.ToolCalls = msgToolCalls
407+ Type: "function",
408+ Function: openai.ChatCompletionMessageToolCallFunction{
409+ Name: toolCall.Function.Name,
410+ Arguments: toolCall.Function.Arguments,
415+ acc.Choices[i].Message.ToolCalls = slices.Clone(msgToolCalls)