diff --git a/openai/openai.go b/openai/openai.go index 95486ef996..17ef6e82db 100644 --- a/openai/openai.go +++ b/openai/openai.go @@ -34,10 +34,12 @@ type ErrorResponse struct { } type Message struct { - Role string `json:"role"` - Content any `json:"content"` - Reasoning string `json:"reasoning,omitempty"` - ToolCalls []ToolCall `json:"tool_calls,omitempty"` + Role string `json:"role"` + Content any `json:"content"` + Reasoning string `json:"reasoning,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + Name string `json:"name,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` } type Choice struct { @@ -401,13 +403,20 @@ func toModel(r api.ShowResponse, m string) Model { func fromChatRequest(r ChatCompletionRequest) (*api.ChatRequest, error) { var messages []api.Message for _, msg := range r.Messages { + toolName := "" + if strings.ToLower(msg.Role) == "tool" { + toolName = msg.Name + if toolName == "" && msg.ToolCallID != "" { + toolName = nameFromToolCallID(r.Messages, msg.ToolCallID) + } + } switch content := msg.Content.(type) { case string: toolCalls, err := fromCompletionToolCall(msg.ToolCalls) if err != nil { return nil, err } - messages = append(messages, api.Message{Role: msg.Role, Content: content, Thinking: msg.Reasoning, ToolCalls: toolCalls}) + messages = append(messages, api.Message{Role: msg.Role, Content: content, Thinking: msg.Reasoning, ToolCalls: toolCalls, ToolName: toolName}) case []any: for _, c := range content { data, ok := c.(map[string]any) @@ -466,6 +475,9 @@ func fromChatRequest(r ChatCompletionRequest) (*api.ChatRequest, error) { return nil, err } messages[len(messages)-1].ToolCalls = toolCalls + if toolName != "" { + messages[len(messages)-1].ToolName = toolName + } } default: // content is only optional if tool calls are present @@ -563,6 +575,20 @@ func fromChatRequest(r ChatCompletionRequest) (*api.ChatRequest, error) { }, nil } +func nameFromToolCallID(messages []Message, toolCallID string) string { + // iterate backwards to be more resilient to duplicate tool call IDs (this + // follows "last one wins") + for i := len(messages) - 1; i >= 0; i-- { + msg := messages[i] + for _, tc := range msg.ToolCalls { + if tc.ID == toolCallID { + return tc.Function.Name + } + } + } + return "" +} + func fromCompletionToolCall(toolCalls []ToolCall) ([]api.ToolCall, error) { apiToolCalls := make([]api.ToolCall, len(toolCalls)) for i, tc := range toolCalls { diff --git a/openai/openai_test.go b/openai/openai_test.go index 96a94f527e..8305713516 100644 --- a/openai/openai_test.go +++ b/openai/openai_test.go @@ -274,6 +274,94 @@ func TestChatMiddleware(t *testing.T) { Stream: &False, }, }, + { + name: "tool response with call ID", + body: `{ + "model": "test-model", + "messages": [ + {"role": "user", "content": "What's the weather like in Paris Today?"}, + {"role": "assistant", "tool_calls": [{"id": "id_abc", "type": "function", "function": {"name": "get_current_weather", "arguments": "{\"location\": \"Paris, France\", \"format\": \"celsius\"}"}}]}, + {"role": "tool", "tool_call_id": "id_abc", "content": "The weather in Paris is 20 degrees Celsius"} + ] + }`, + req: api.ChatRequest{ + Model: "test-model", + Messages: []api.Message{ + { + Role: "user", + Content: "What's the weather like in Paris Today?", + }, + { + Role: "assistant", + ToolCalls: []api.ToolCall{ + { + Function: api.ToolCallFunction{ + Name: "get_current_weather", + Arguments: map[string]any{ + "location": "Paris, France", + "format": "celsius", + }, + }, + }, + }, + }, + { + Role: "tool", + Content: "The weather in Paris is 20 degrees Celsius", + ToolName: "get_current_weather", + }, + }, + Options: map[string]any{ + "temperature": 1.0, + "top_p": 1.0, + }, + Stream: &False, + }, + }, + { + name: "tool response with name", + body: `{ + "model": "test-model", + "messages": [ + {"role": "user", "content": "What's the weather like in Paris Today?"}, + {"role": "assistant", "tool_calls": [{"id": "id", "type": "function", "function": {"name": "get_current_weather", "arguments": "{\"location\": \"Paris, France\", \"format\": \"celsius\"}"}}]}, + {"role": "tool", "name": "get_current_weather", "content": "The weather in Paris is 20 degrees Celsius"} + ] + }`, + req: api.ChatRequest{ + Model: "test-model", + Messages: []api.Message{ + { + Role: "user", + Content: "What's the weather like in Paris Today?", + }, + { + Role: "assistant", + ToolCalls: []api.ToolCall{ + { + Function: api.ToolCallFunction{ + Name: "get_current_weather", + Arguments: map[string]any{ + "location": "Paris, France", + "format": "celsius", + }, + }, + }, + }, + }, + { + Role: "tool", + Content: "The weather in Paris is 20 degrees Celsius", + ToolName: "get_current_weather", + }, + }, + Options: map[string]any{ + "temperature": 1.0, + "top_p": 1.0, + }, + Stream: &False, + }, + }, { name: "chat handler with streaming tools", body: `{