mirror of
https://github.com/ollama/ollama.git
synced 2026-10-23 12:03:40 +00:00
openai: keep tool message content parts in one message
FromChatRequest splits array content into one api.Message per part. That is intended for user content, but a tool message is a single result: the split also dropped its tool_call_id and tool name, since those were only copied when the message had tool_calls. Renderers that resolve tool results by ID then see extra, unnamed results; for example, Kimi K3 fails with "tool message 2 has no resolvable tool name" on a tool result with two text parts, which the OpenAI spec allows. Merge the parts of a tool message into one message with the text joined, the images kept, and ToolCallID and ToolName set, matching how function_call_output items are converted for /v1/responses.
This commit is contained in:
parent
1abe35e6e6
commit
de119ea47f
2 changed files with 65 additions and 0 deletions
|
|
@ -634,6 +634,7 @@ func FromChatRequest(r ChatCompletionRequest, thinking ...*model.Thinking) (*api
|
|||
}
|
||||
messages = append(messages, api.Message{Role: msg.Role, Content: content, Thinking: msg.Reasoning, ToolCalls: toolCalls, ToolName: toolName, ToolCallID: msg.ToolCallID})
|
||||
case []any:
|
||||
start := len(messages)
|
||||
for _, c := range content {
|
||||
data, ok := c.(map[string]any)
|
||||
if !ok {
|
||||
|
|
@ -682,6 +683,16 @@ func FromChatRequest(r ChatCompletionRequest, thinking ...*model.Thinking) (*api
|
|||
return nil, errors.New("invalid message format")
|
||||
}
|
||||
}
|
||||
// a tool message is a single result, so keep its parts together and
|
||||
// preserve the tool call it answers
|
||||
if strings.ToLower(msg.Role) == "tool" && len(messages) > start {
|
||||
tool := api.Message{Role: msg.Role, ToolName: toolName, ToolCallID: msg.ToolCallID}
|
||||
for _, part := range messages[start:] {
|
||||
tool.Content += part.Content
|
||||
tool.Images = append(tool.Images, part.Images...)
|
||||
}
|
||||
messages = append(messages[:start], tool)
|
||||
}
|
||||
// since we might have added multiple messages above, if we have tools
|
||||
// calls we'll add them to the last message
|
||||
if len(messages) > 0 && len(msg.ToolCalls) > 0 {
|
||||
|
|
|
|||
|
|
@ -157,6 +157,60 @@ func TestFromChatRequest_WithImage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFromChatRequest_ToolMessageArrayContent(t *testing.T) {
|
||||
imgData, _ := base64.StdEncoding.DecodeString(image)
|
||||
|
||||
req := ChatCompletionRequest{
|
||||
Model: "test-model",
|
||||
Messages: []Message{
|
||||
{Role: "user", Content: "What's in the screenshot?"},
|
||||
{
|
||||
Role: "assistant",
|
||||
ToolCalls: []ToolCall{{
|
||||
ID: "call_1",
|
||||
Type: "function",
|
||||
Function: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
}{Name: "screenshot", Arguments: "{}"},
|
||||
}},
|
||||
},
|
||||
{
|
||||
Role: "tool",
|
||||
ToolCallID: "call_1",
|
||||
Content: []any{
|
||||
map[string]any{"type": "text", "text": "Captured "},
|
||||
map[string]any{
|
||||
"type": "image_url",
|
||||
"image_url": map[string]any{"url": prefix + image},
|
||||
},
|
||||
map[string]any{"type": "text", "text": "screen.png"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := FromChatRequest(req)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(result.Messages) != 3 {
|
||||
t.Fatalf("expected 3 messages, got %d", len(result.Messages))
|
||||
}
|
||||
|
||||
tool := result.Messages[2]
|
||||
if tool.Role != "tool" || tool.ToolCallID != "call_1" || tool.ToolName != "screenshot" {
|
||||
t.Errorf("expected tool message for call_1/screenshot, got role=%q id=%q name=%q", tool.Role, tool.ToolCallID, tool.ToolName)
|
||||
}
|
||||
if tool.Content != "Captured screen.png" {
|
||||
t.Errorf("expected content 'Captured screen.png', got %q", tool.Content)
|
||||
}
|
||||
if len(tool.Images) != 1 || string(tool.Images[0]) != string(imgData) {
|
||||
t.Errorf("expected the tool image to be kept, got %d images", len(tool.Images))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromCompleteRequest_Basic(t *testing.T) {
|
||||
temp := float32(0.8)
|
||||
req := CompletionRequest{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue