From 0e1477eb23562008a1a78c434e211a4daa0cc6ef Mon Sep 17 00:00:00 2001 From: Haruko386 Date: Wed, 29 Apr 2026 19:06:40 +0800 Subject: [PATCH] Go: implement provider: MiniMax (#14478) ### What problem does this PR solve? implement MiniMax provider ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality) --- conf/models/minimax.json | 49 ++++- internal/entity/models/minimax.go | 312 ++++++++++++++++++++++++++- internal/entity/models/volcengine.go | 2 +- 3 files changed, 351 insertions(+), 12 deletions(-) diff --git a/conf/models/minimax.json b/conf/models/minimax.json index 9480ac2c06..31760ac259 100644 --- a/conf/models/minimax.json +++ b/conf/models/minimax.json @@ -6,6 +6,7 @@ }, "url_suffix": { "chat": "v1/text/chatcompletion_v2", + "models": "v1/models", "tts": "v1/t2a_v2", "files": "v1/files/list" }, @@ -16,56 +17,88 @@ "max_tokens": 204800, "model_types": [ "chat" - ] + ], + "thinking": { + "default_value": true, + "clear_thinking": true + } }, { "name": "minimax-m2.7-highspeed", "max_tokens": 204800, "model_types": [ "chat" - ] + ], + "thinking": { + "default_value": true, + "clear_thinking": true + } }, { "name": "minimax-m2.5", "max_tokens": 204800, "model_types": [ "chat" - ] + ], + "thinking": { + "default_value": true, + "clear_thinking": true + } }, { "name": "minimax-m2.5-highspeed", "max_tokens": 204800, "model_types": [ "chat" - ] + ], + "thinking": { + "default_value": true, + "clear_thinking": true + } }, { "name": "minimax-m2.1", "max_tokens": 204800, "model_types": [ "chat" - ] + ], + "thinking": { + "default_value": true, + "clear_thinking": true + } }, { "name": "minimax-m2.1-highspeed", "max_tokens": 204800, "model_types": [ "chat" - ] + ], + "thinking": { + "default_value": true, + "clear_thinking": true + } }, { "name": "minimax-m2", "max_tokens": 204800, "model_types": [ "chat" - ] + ], + "thinking": { + "default_value": true, + "clear_thinking": true + } }, { "name": "minimax-m2-her", "max_tokens": 65536, "model_types": [ "chat" - ] + ], + "thinking": { + "default_value": true, + "clear_thinking": true + } } ] } \ No newline at end of file diff --git a/internal/entity/models/minimax.go b/internal/entity/models/minimax.go index 9fe32a289d..90c8492d77 100644 --- a/internal/entity/models/minimax.go +++ b/internal/entity/models/minimax.go @@ -17,9 +17,14 @@ package models import ( + "bufio" + "bytes" + "encoding/json" "fmt" "io" "net/http" + "ragflow/internal/logger" + "strings" "time" ) @@ -57,7 +62,120 @@ func (z *MinimaxModel) Name() string { // Chat sends a message and returns response func (z *MinimaxModel) Chat(modelName, message *string, apiConfig *APIConfig, modelConfig *ChatConfig) (*ChatResponse, error) { - return nil, fmt.Errorf("%s, no such method", z.Name()) + var region = "default" + + if *apiConfig.Region != "" { + region = *apiConfig.Region + } + + url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Chat) + + // Build request Body + reqBody := map[string]interface{}{ + "model": modelName, + "messages": []map[string]interface{}{ + {"role": "user", "content": *message}, + }, + "stream": false, + "temperature": 1, + } + + if modelConfig.Temperature != nil { + reqBody["temperature"] = *modelConfig.Temperature + } + + if modelConfig.MaxTokens != nil { + reqBody["max_tokens"] = *modelConfig.MaxTokens + } + + if modelConfig.Stream != nil { + reqBody["stream"] = *modelConfig.Stream + } + + if modelConfig.TopP != nil { + reqBody["top_p"] = *modelConfig.TopP + } + + if modelConfig.DoSample != nil { + reqBody["do_sample"] = *modelConfig.DoSample + } + + if modelConfig.Thinking != nil { + reqBody["thinking"] = *modelConfig.Thinking + } + + jsonData, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + + resp, err := z.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to send request: %d %s", resp.StatusCode, string(body)) + } + + // Parse response + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + choices, ok := result["choices"].([]interface{}) + if !ok { + return nil, fmt.Errorf("no choices in response") + } + + firstChoice, ok := choices[0].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("no choices in response") + } + + messageMap, ok := firstChoice["message"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("no message in response") + } + + content, ok := messageMap["content"].(string) + if !ok { + return nil, fmt.Errorf("no message in response") + } + + var reasonContent string + if modelConfig.Thinking != nil && *modelConfig.Thinking { + reasonContent, ok = messageMap["reasoning_content"].(string) + if !ok { + return nil, fmt.Errorf("invalid content format") + } + // if first char of reasonContent is \n remove the \n + if reasonContent != "" && reasonContent[0] == '\n' { + reasonContent = reasonContent[1:] + } + } + + chatResponse := &ChatResponse{ + Answer: &content, + ReasonContent: &reasonContent, + } + + return chatResponse, nil } // ChatWithMessages sends multiple messages with roles and returns response @@ -67,7 +185,143 @@ func (z *MinimaxModel) ChatWithMessages(modelName string, apiKey *string, messag // ChatStreamlyWithSender sends a message and streams response via sender function (best performance, no channel) func (z *MinimaxModel) ChatStreamlyWithSender(modelName, message *string, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { - return fmt.Errorf("%s, no such method", z.Name()) + var region = "default" + + if apiConfig.Region != nil { + region = *apiConfig.Region + } + + url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Chat) + + // Build request body with streaming enabled + reqBody := map[string]interface{}{ + "model": modelName, + "messages": []map[string]interface{}{ + {"role": "user", "content": *message}, + }, + "stream": true, + "temperature": 1, + } + + if modelConfig.Stream != nil { + reqBody["stream"] = *modelConfig.Stream + } + + if modelConfig.MaxTokens != nil { + reqBody["max_tokens"] = *modelConfig.MaxTokens + } + + if modelConfig.Temperature != nil { + reqBody["temperature"] = *modelConfig.Temperature + } + + if modelConfig.TopP != nil { + reqBody["top_p"] = *modelConfig.TopP + } + + if modelConfig.DoSample != nil { + reqBody["do_sample"] = *modelConfig.DoSample + } + + if modelConfig.Stop != nil { + reqBody["stop"] = *modelConfig.Stop + } + + if modelConfig.Thinking != nil { + reqBody["thinking"] = *modelConfig.Thinking + } + + jsonData, err := json.Marshal(reqBody) + if err != nil { + return fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + + resp, err := z.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) + } + + // SSE parsing: read line by line + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + line := scanner.Text() + logger.Info(line) + + // SSE data line start with data: + if !strings.HasPrefix(line, "data:") { + continue + } + + // Extract JSON after data: + data := strings.TrimSpace(line[5:]) + + // [DONE] marks the end of stream + if data == "[DONE]" { + break + } + + // Parse the JSON event + var event map[string]interface{} + if err := json.Unmarshal([]byte(data), &event); err != nil { + continue + } + + choices, ok := event["choices"].([]interface{}) + if !ok || len(choices) == 0 { + continue + } + + firstChoice, ok := choices[0].(map[string]interface{}) + if !ok { + continue + } + + delta, ok := firstChoice["delta"].(map[string]interface{}) + if !ok { + continue + } + + content, ok := delta["content"].(string) + if ok && content != "" { + if err := sender(&content, nil); err != nil { + return err + } + } + + reasoningContent, ok := delta["reasoning_content"].(string) + if ok && reasoningContent != "" { + if err := sender(nil, &reasoningContent); err != nil { + return err + } + } + + finishReason, ok := firstChoice["finish_reason"].(string) + if ok && finishReason != "" { + break + } + } + + // Send [DONE] marker for OpenAI compatibility + endOfStream := "[DONE]" + if err = sender(&endOfStream, nil); err != nil { + return err + } + + return scanner.Err() } // Encode encodes a list of texts into embeddings @@ -76,7 +330,59 @@ func (z *MinimaxModel) Encode(modelName *string, texts []string, apiConfig *APIC } func (z *MinimaxModel) ListModels(apiConfig *APIConfig) ([]string, error) { - return nil, fmt.Errorf("%s, no such method", z.Name()) + var region = "default" + if apiConfig.Region != nil { + region = *apiConfig.Region + } + + url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Models) + + // Build request body + reqBody := map[string]interface{}{} + + jsonData, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + + resp, err := z.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API requestssss failed with status %d: %s : %s", resp.StatusCode, string(body), url) + } + + // Parse response + var result map[string]interface{} + if err = json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + // convert result["data"] to []map[string]interface{} + models := make([]string, 0) + for _, model := range result["data"].([]interface{}) { + modelMap := model.(map[string]interface{}) + modelName := modelMap["id"].(string) + models = append(models, modelName) + } + + return models, nil } func (z *MinimaxModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { diff --git a/internal/entity/models/volcengine.go b/internal/entity/models/volcengine.go index a7fc5b6769..49a120962c 100644 --- a/internal/entity/models/volcengine.go +++ b/internal/entity/models/volcengine.go @@ -362,7 +362,7 @@ func (z *VolcEngine) ChatStreamlyWithSender(modelName, message *string, apiConfi } content, ok := delta["content"].(string) - if ok || content != "" { + if ok && content != "" { if err := sender(&content, nil); err != nil { return err }