Go: add tools for moonshot, baidu and minimax (#17000)

### Summary

As title
related to #16990
This commit is contained in:
Haruko386
2026-07-16 19:03:59 +08:00
committed by GitHub
parent c7a623ff81
commit 5307ecd520
6 changed files with 357 additions and 35 deletions

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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 {