diff --git a/conf/models/minimax.json b/conf/models/minimax.json index c2b06e37be..0c142a96a8 100644 --- a/conf/models/minimax.json +++ b/conf/models/minimax.json @@ -2,8 +2,8 @@ "name": "MiniMax", "rank": 987, "url": { - "default": "https://api.minimaxi.com/", - "global": "https://api.minimax.io/" + "default": "https://api.minimaxi.com", + "global": "https://api.minimax.io" }, "url_suffix": { "chat": "v1/text/chatcompletion_v2", diff --git a/internal/entity/models/baidu.go b/internal/entity/models/baidu.go index 1b46cbb1fa..fef59b6104 100644 --- a/internal/entity/models/baidu.go +++ b/internal/entity/models/baidu.go @@ -25,6 +25,7 @@ import ( "io" "net/http" "ragflow/internal/common" + "sort" "strings" ) @@ -65,12 +66,18 @@ func (b *BaiduModel) ChatWithMessages(modelName string, messages []Message, apiC url := fmt.Sprintf("%s/%s", resolvedBaseURL, b.baseModel.URLSuffix.Chat) // Convert messages to API format - apiMessages := make([]map[string]interface{}, len(messages)) + apiMessages := make([]map[string]any, len(messages)) for i, msg := range messages { - apiMessages[i] = map[string]interface{}{ + apiMessages[i] = map[string]any{ "role": msg.Role, "content": msg.Content, } + if msg.ToolCallID != "" { + apiMessages[i]["tool_call_id"] = msg.ToolCallID + } + if len(msg.ToolCalls) > 0 { + apiMessages[i]["tool_calls"] = msg.ToolCalls + } } // Build request body @@ -142,6 +149,12 @@ func (b *BaiduModel) ChatWithMessages(modelName string, messages []Message, apiC } } } + if chatModelConfig.Tools != nil { + reqBody["tools"] = chatModelConfig.Tools + } + if chatModelConfig.ToolChoice != nil { + reqBody["tool_choice"] = *chatModelConfig.ToolChoice + } } jsonData, err := json.Marshal(reqBody) @@ -201,6 +214,15 @@ func (b *BaiduModel) ChatWithMessages(modelName string, messages []Message, apiC return nil, fmt.Errorf("invalid content format") } + var toolCalls []map[string]any + if tcs, ok := messageMap["tool_calls"].([]any); ok { + for _, tc := range tcs { + if toolCall, ok := tc.(map[string]any); ok { + toolCalls = append(toolCalls, toolCall) + } + } + } + var reasonContent string if chatModelConfig != nil && chatModelConfig.Thinking != nil && *chatModelConfig.Thinking { reasonContent, ok = messageMap["reasoning_content"].(string) @@ -216,6 +238,12 @@ func (b *BaiduModel) ChatWithMessages(modelName string, messages []Message, apiC chatResponse := &ChatResponse{ Answer: &content, ReasonContent: &reasonContent, + ToolCalls: toolCalls, + } + if pt, ct, tt := extractUsageFromMap(result); tt > 0 { + chatResponse.Usage = &ChatUsage{ + PromptTokens: pt, CompletionTokens: ct, TotalTokens: tt, + } } return chatResponse, nil @@ -243,14 +271,19 @@ func (b *BaiduModel) ChatStreamlyWithSender(modelName string, messages []Message "role": msg.Role, "content": msg.Content, } + if msg.ToolCallID != "" { + apiMessages[i]["tool_call_id"] = msg.ToolCallID + } + if len(msg.ToolCalls) > 0 { + apiMessages[i]["tool_calls"] = msg.ToolCalls + } } // Build request body with streaming enabled reqBody := map[string]interface{}{ - "model": modelName, - "messages": apiMessages, - "stream": true, - "temperature": 1, + "model": modelName, + "messages": apiMessages, + "stream": true, } if modelConfig != nil { @@ -318,6 +351,13 @@ func (b *BaiduModel) ChatStreamlyWithSender(modelName string, messages []Message } } } + + if modelConfig.Tools != nil { + reqBody["tools"] = modelConfig.Tools + } + if modelConfig.ToolChoice != nil { + reqBody["tool_choice"] = *modelConfig.ToolChoice + } } jsonData, err := json.Marshal(reqBody) @@ -349,6 +389,7 @@ func (b *BaiduModel) ChatStreamlyWithSender(modelName string, messages []Message // SSE parsing: read line by line sawTerminal := false + accumulatedToolCalls := make(map[int]map[string]interface{}) done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error { common.Info(fmt.Sprintf("%v", event)) @@ -367,6 +408,56 @@ func (b *BaiduModel) ChatStreamlyWithSender(modelName string, messages []Message return nil } + if tcs, ok := delta["tool_calls"].([]interface{}); ok { + for _, tc := range tcs { + tcMap, ok := tc.(map[string]interface{}) + if !ok { + continue + } + idxF, ok := tcMap["index"].(float64) + if !ok { + continue + } + idx := int(idxF) + existing, hasExisting := accumulatedToolCalls[idx] + if !hasExisting { + accumulatedToolCalls[idx] = cloneMap(tcMap) + continue + } + if id, ok := tcMap["id"].(string); ok && id != "" { + if eid, ok := existing["id"].(string); ok { + existing["id"] = eid + id + } else { + existing["id"] = id + } + } + if typ, ok := tcMap["type"].(string); ok && typ != "" { + existing["type"] = typ + } + if fn, ok := tcMap["function"].(map[string]interface{}); ok { + ef, ok := existing["function"].(map[string]interface{}) + if !ok { + ef = make(map[string]interface{}) + existing["function"] = ef + } + if name, ok := fn["name"].(string); ok && name != "" { + if en, ok := ef["name"].(string); ok { + ef["name"] = en + name + } else { + ef["name"] = name + } + } + if args, ok := fn["arguments"].(string); ok && args != "" { + if ea, ok := ef["arguments"].(string); ok { + ef["arguments"] = ea + args + } else { + ef["arguments"] = args + } + } + } + } + } + reasoningContent, ok := delta["reasoning_content"].(string) if ok && reasoningContent != "" { if err = sender(nil, &reasoningContent); err != nil { @@ -395,6 +486,19 @@ func (b *BaiduModel) ChatStreamlyWithSender(modelName string, messages []Message return fmt.Errorf("baidu: stream ended before [DONE] or finish_reason") } + if len(accumulatedToolCalls) > 0 && modelConfig != nil { + indices := make([]int, 0, len(accumulatedToolCalls)) + for idx := range accumulatedToolCalls { + indices = append(indices, idx) + } + sort.Ints(indices) + tcs := make([]map[string]interface{}, 0, len(accumulatedToolCalls)) + for _, idx := range indices { + tcs = append(tcs, accumulatedToolCalls[idx]) + } + modelConfig.ToolCallsResult = &tcs + } + // Send [DONE] marker for OpenAI compatibility endOfStream := "[DONE]" if err = sender(&endOfStream, nil); err != nil { diff --git a/internal/entity/models/base_model.go b/internal/entity/models/base_model.go index b7af60da55..c3ab60c70b 100644 --- a/internal/entity/models/base_model.go +++ b/internal/entity/models/base_model.go @@ -81,6 +81,7 @@ func (b *BaseModel) GetBaseURL(apiConfig *APIConfig) (string, error) { } return "", fmt.Errorf("no base URL configured for region %q", region) } + baseURL = strings.TrimSuffix(baseURL, "/") return baseURL, nil } diff --git a/internal/entity/models/minimax.go b/internal/entity/models/minimax.go index e7c3af9b06..e50227a346 100644 --- a/internal/entity/models/minimax.go +++ b/internal/entity/models/minimax.go @@ -24,6 +24,7 @@ import ( "fmt" "io" "net/http" + "sort" "strings" ) @@ -85,14 +86,19 @@ func (m *MinimaxModel) ChatWithMessages(modelName string, messages []Message, ap "role": msg.Role, "content": msg.Content, } + if msg.ToolCallID != "" { + apiMessages[i]["tool_call_id"] = msg.ToolCallID + } + if len(msg.ToolCalls) > 0 { + apiMessages[i]["tool_calls"] = msg.ToolCalls + } } // Build request body reqBody := map[string]interface{}{ - "model": modelName, - "messages": apiMessages, - "stream": false, - "temperature": 1, + "model": modelName, + "messages": apiMessages, + "stream": false, } if chatModelConfig != nil { @@ -112,7 +118,7 @@ func (m *MinimaxModel) ChatWithMessages(modelName string, messages []Message, ap reqBody["do_sample"] = *chatModelConfig.DoSample } - if chatModelConfig != nil && chatModelConfig.Thinking != nil { + if chatModelConfig.Thinking != nil { if *chatModelConfig.Thinking { reqBody["thinking"] = map[string]interface{}{ "type": "adaptive", @@ -124,6 +130,10 @@ func (m *MinimaxModel) ChatWithMessages(modelName string, messages []Message, ap } } } + + if chatModelConfig.Tools != nil { + reqBody["tools"] = chatModelConfig.Tools + } } jsonData, err := json.Marshal(reqBody) @@ -178,25 +188,36 @@ func (m *MinimaxModel) ChatWithMessages(modelName string, messages []Message, ap return nil, fmt.Errorf("no message in response") } - content, ok := messageMap["content"].(string) - if !ok { - return nil, fmt.Errorf("no message in response") - } + content, _ := messageMap["content"].(string) var reasonContent string if chatModelConfig != nil && chatModelConfig.Thinking != nil && *chatModelConfig.Thinking { - reasonContent, ok = messageMap["reasoning_content"].(string) - if !ok { - return nil, fmt.Errorf("invalid content format") + if rc, ok := messageMap["reasoning_content"].(string); ok { + reasonContent = rc + if reasonContent != "" && reasonContent[0] == '\n' { + reasonContent = reasonContent[1:] + } } - if reasonContent != "" && reasonContent[0] == '\n' { - reasonContent = reasonContent[1:] + } + + var toolCalls []map[string]any + if tcs, ok := messageMap["tool_calls"].([]any); ok { + for _, tc := range tcs { + if tcMap, ok := tc.(map[string]any); ok { + toolCalls = append(toolCalls, tcMap) + } } } chatResponse := &ChatResponse{ Answer: &content, ReasonContent: &reasonContent, + ToolCalls: toolCalls, + } + if pt, ct, tt := extractUsageFromMap(result); tt > 0 { + chatResponse.Usage = &ChatUsage{ + PromptTokens: pt, CompletionTokens: ct, TotalTokens: tt, + } } return chatResponse, nil @@ -232,14 +253,19 @@ func (m *MinimaxModel) ChatStreamlyWithSender(modelName string, messages []Messa "role": msg.Role, "content": msg.Content, } + if msg.ToolCallID != "" { + apiMessages[i]["tool_call_id"] = msg.ToolCallID + } + if len(msg.ToolCalls) > 0 { + apiMessages[i]["tool_calls"] = msg.ToolCalls + } } // Build request body with streaming enabled reqBody := map[string]interface{}{ - "model": modelName, - "messages": apiMessages, - "stream": true, - "temperature": 1, + "model": modelName, + "messages": apiMessages, + "stream": true, } if modelConfig != nil { @@ -275,6 +301,10 @@ func (m *MinimaxModel) ChatStreamlyWithSender(modelName string, messages []Messa } } } + + if modelConfig.Tools != nil { + reqBody["tools"] = modelConfig.Tools + } } jsonData, err := json.Marshal(reqBody) @@ -307,6 +337,7 @@ func (m *MinimaxModel) ChatStreamlyWithSender(modelName string, messages []Messa // SSE parsing: read line by line sawTerminal := false + accumulatedToolCalls := make(map[int]map[string]any) done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error { choices, ok := event["choices"].([]interface{}) if !ok || len(choices) == 0 { @@ -323,6 +354,36 @@ func (m *MinimaxModel) ChatStreamlyWithSender(modelName string, messages []Messa return nil } + if tcs, ok := delta["tool_calls"].([]interface{}); ok { + for _, tc := range tcs { + tcMap, ok := tc.(map[string]interface{}) + if !ok { + continue + } + idxF, ok := tcMap["index"].(float64) + if !ok { + continue + } + idx := int(idxF) + existing, hasExisting := accumulatedToolCalls[idx] + if hasExisting { + if fn, ok := tcMap["function"].(map[string]interface{}); ok { + if args, ok := fn["arguments"].(string); ok { + if ef, ok := existing["function"].(map[string]interface{}); ok { + if ea, ok := ef["arguments"].(string); ok { + ef["arguments"] = ea + args + } else { + ef["arguments"] = args + } + } + } + } + } else { + accumulatedToolCalls[idx] = cloneMap(tcMap) + } + } + } + content, ok := delta["content"].(string) if ok && content != "" { if err := sender(&content, nil); err != nil { @@ -351,6 +412,19 @@ func (m *MinimaxModel) ChatStreamlyWithSender(modelName string, messages []Messa return fmt.Errorf("minimax: stream ended before [DONE] or finish_reason") } + if len(accumulatedToolCalls) > 0 && modelConfig != nil { + indices := make([]int, 0, len(accumulatedToolCalls)) + for idx := range accumulatedToolCalls { + indices = append(indices, idx) + } + sort.Ints(indices) + tcs := make([]map[string]interface{}, 0, len(accumulatedToolCalls)) + for _, idx := range indices { + tcs = append(tcs, accumulatedToolCalls[idx]) + } + modelConfig.ToolCallsResult = &tcs + } + // Send [DONE] marker for OpenAI compatibility endOfStream := "[DONE]" return sender(&endOfStream, nil) diff --git a/internal/entity/models/moonshot.go b/internal/entity/models/moonshot.go index bcec5a18ed..df848aed9e 100644 --- a/internal/entity/models/moonshot.go +++ b/internal/entity/models/moonshot.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "sort" "strings" ) @@ -77,20 +78,25 @@ func (m *MoonshotModel) ChatWithMessages(modelName string, messages []Message, a url := fmt.Sprintf("%s/%s", resolvedBaseURL, m.baseModel.URLSuffix.Chat) // Convert messages to the format expected by API - apiMessages := make([]map[string]interface{}, len(messages)) + apiMessages := make([]map[string]any, len(messages)) for i, msg := range messages { - apiMessages[i] = map[string]interface{}{ + apiMessages[i] = map[string]any{ "role": msg.Role, "content": msg.Content, } + if msg.ToolCallID != "" { + apiMessages[i]["tool_call_id"] = msg.ToolCallID + } + if len(msg.ToolCalls) > 0 { + apiMessages[i]["tool_calls"] = msg.ToolCalls + } } // Build request body reqBody := map[string]interface{}{ - "model": modelName, - "messages": apiMessages, - "stream": false, - "temperature": 0.6, + "model": modelName, + "messages": apiMessages, + "stream": false, } if chatModelConfig != nil { @@ -121,6 +127,15 @@ func (m *MoonshotModel) ChatWithMessages(modelName string, messages []Message, a } } } + + if chatModelConfig.Tools != nil { + reqBody["tools"] = chatModelConfig.Tools + toolChoice := "auto" + if chatModelConfig.ToolChoice != nil { + toolChoice = *chatModelConfig.ToolChoice + } + reqBody["tool_choice"] = toolChoice + } } jsonData, err := json.Marshal(reqBody) @@ -175,9 +190,9 @@ func (m *MoonshotModel) ChatWithMessages(modelName string, messages []Message, a return nil, fmt.Errorf("invalid message format") } - content, ok := messageMap["content"].(string) - if !ok { - return nil, fmt.Errorf("invalid content format") + var content string + if c, ok := messageMap["content"].(string); ok { + content = c } var reasonContent string @@ -189,9 +204,24 @@ func (m *MoonshotModel) ChatWithMessages(modelName string, messages []Message, a } } + var toolCalls []map[string]any + if tcs, ok := messageMap["tool_calls"].([]interface{}); ok { + for _, tc := range tcs { + if tcMap, ok := tc.(map[string]any); ok { + toolCalls = append(toolCalls, tcMap) + } + } + } + chatResponse := &ChatResponse{ Answer: &content, ReasonContent: &reasonContent, + ToolCalls: toolCalls, + } + if pt, ct, tt := extractUsageFromMap(result); tt > 0 { + chatResponse.Usage = &ChatUsage{ + PromptTokens: pt, CompletionTokens: ct, TotalTokens: tt, + } } return chatResponse, nil @@ -227,6 +257,12 @@ func (m *MoonshotModel) ChatStreamlyWithSender(modelName string, messages []Mess "role": msg.Role, "content": msg.Content, } + if msg.ToolCallID != "" { + apiMessages[i]["tool_call_id"] = msg.ToolCallID + } + if len(msg.ToolCalls) > 0 { + apiMessages[i]["tool_calls"] = msg.ToolCalls + } } // Build request body with streaming enabled @@ -268,6 +304,15 @@ func (m *MoonshotModel) ChatStreamlyWithSender(modelName string, messages []Mess } } } + + if chatModelConfig.Tools != nil { + reqBody["tools"] = chatModelConfig.Tools + toolChoice := "auto" + if chatModelConfig.ToolChoice != nil { + toolChoice = *chatModelConfig.ToolChoice + } + reqBody["tool_choice"] = toolChoice + } } jsonData, err := json.Marshal(reqBody) @@ -300,6 +345,10 @@ func (m *MoonshotModel) ChatStreamlyWithSender(modelName string, messages []Mess // SSE parsing: read line by line sawTerminal := false + if chatModelConfig != nil { + chatModelConfig.ToolCallsResult = nil + } + accumulatedToolCalls := make(map[int]map[string]any) done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error { choices, ok := event["choices"].([]interface{}) if !ok || len(choices) == 0 { @@ -316,6 +365,36 @@ func (m *MoonshotModel) ChatStreamlyWithSender(modelName string, messages []Mess return nil } + if tcs, ok := delta["tool_calls"].([]interface{}); ok { + for _, tc := range tcs { + tcMap, ok := tc.(map[string]interface{}) + if !ok { + continue + } + idxF, ok := tcMap["index"].(float64) + if !ok { + continue + } + idx := int(idxF) + existing, hasExisting := accumulatedToolCalls[idx] + if hasExisting { + if fn, ok := tcMap["function"].(map[string]interface{}); ok { + if args, ok := fn["arguments"].(string); ok { + if ef, ok := existing["function"].(map[string]interface{}); ok { + if ea, ok := ef["arguments"].(string); ok { + ef["arguments"] = ea + args + } else { + ef["arguments"] = args + } + } + } + } + } else { + accumulatedToolCalls[idx] = cloneMap(tcMap) + } + } + } + reasoningContent, ok := delta["reasoning_content"].(string) if ok && reasoningContent != "" { if err := sender(nil, &reasoningContent); err != nil { @@ -344,6 +423,19 @@ func (m *MoonshotModel) ChatStreamlyWithSender(modelName string, messages []Mess return fmt.Errorf("moonshot: stream ended before [DONE] or finish_reason") } + if len(accumulatedToolCalls) > 0 && chatModelConfig != nil { + indices := make([]int, 0, len(accumulatedToolCalls)) + for idx := range accumulatedToolCalls { + indices = append(indices, idx) + } + sort.Ints(indices) + tcs := make([]map[string]interface{}, 0, len(accumulatedToolCalls)) + for _, idx := range indices { + tcs = append(tcs, accumulatedToolCalls[idx]) + } + chatModelConfig.ToolCallsResult = &tcs + } + // Send [DONE] marker for OpenAI compatibility endOfStream := "[DONE]" if err = sender(&endOfStream, nil); err != nil { diff --git a/internal/entity/models/moonshot_test.go b/internal/entity/models/moonshot_test.go index d8eb5f7a72..07e8f2f06a 100644 --- a/internal/entity/models/moonshot_test.go +++ b/internal/entity/models/moonshot_test.go @@ -129,6 +129,57 @@ func TestMoonshotChatForcesNonStreaming(t *testing.T) { } } +func TestMoonshotChatSupportsTools(t *testing.T) { + srv := newMoonshotServer(t, func(t *testing.T, _ *http.Request, body map[string]interface{}, w http.ResponseWriter) { + tools, ok := body["tools"].([]interface{}) + if !ok || len(tools) != 1 { + t.Errorf("tools=%#v, want one tool", body["tools"]) + } + if body["tool_choice"] != "auto" { + t.Errorf("tool_choice=%v, want auto", body["tool_choice"]) + } + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "choices": []map[string]interface{}{{ + "message": map[string]interface{}{ + "content": nil, + "tool_calls": []map[string]interface{}{{ + "id": "call_weather", + "type": "function", + "function": map[string]string{ + "name": "get_weather", + "arguments": `{"city":"北京"}`, + }, + }}, + }, + }}, + }) + }) + defer srv.Close() + + apiKey := "test-key" + tools := []map[string]interface{}{{ + "type": "function", + "function": map[string]interface{}{ + "name": "get_weather", + }, + }} + resp, err := newMoonshotForTest(srv.URL).ChatWithMessages( + "kimi-k2.6", + []Message{{Role: "user", Content: "北京今天天气怎么样?"}}, + &APIConfig{ApiKey: &apiKey}, + &ChatConfig{Tools: tools}, + ) + if err != nil { + t.Fatalf("ChatWithMessages: %v", err) + } + if resp.Answer == nil || *resp.Answer != "" { + t.Errorf("Answer=%v, want empty", resp.Answer) + } + if len(resp.ToolCalls) != 1 || resp.ToolCalls[0]["id"] != "call_weather" { + t.Errorf("ToolCalls=%#v, want call_weather", resp.ToolCalls) + } +} + func TestMoonshotStreamForcesStreaming(t *testing.T) { srv := newMoonshotServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.Method != http.MethodPost {