mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 16:56:56 +08:00
Merge branch 'dev' into openclaw
This commit is contained in:
@@ -832,6 +832,8 @@ func (s *Server) hydrateBacktestAIConfig(cfg *backtest.BacktestConfig) error {
|
|||||||
provider = "google"
|
provider = "google"
|
||||||
} else if strings.Contains(modelNameLower, "deepseek") {
|
} else if strings.Contains(modelNameLower, "deepseek") {
|
||||||
provider = "deepseek"
|
provider = "deepseek"
|
||||||
|
} else if strings.Contains(modelNameLower, "minimax") {
|
||||||
|
provider = "minimax"
|
||||||
} else if model.CustomAPIURL != "" {
|
} else if model.CustomAPIURL != "" {
|
||||||
provider = "custom"
|
provider = "custom"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"nofx/crypto"
|
"nofx/crypto"
|
||||||
"nofx/logger"
|
"nofx/logger"
|
||||||
"nofx/manager"
|
"nofx/manager"
|
||||||
|
"nofx/security"
|
||||||
"nofx/market"
|
"nofx/market"
|
||||||
"nofx/provider/alpaca"
|
"nofx/provider/alpaca"
|
||||||
"nofx/provider/coinank/coinank_api"
|
"nofx/provider/coinank/coinank_api"
|
||||||
@@ -1851,6 +1852,7 @@ func (s *Server) handleGetModelConfigs(c *gin.Context) {
|
|||||||
{ID: "gemini", Name: "Gemini AI", Provider: "gemini", Enabled: false},
|
{ID: "gemini", Name: "Gemini AI", Provider: "gemini", Enabled: false},
|
||||||
{ID: "grok", Name: "Grok AI", Provider: "grok", Enabled: false},
|
{ID: "grok", Name: "Grok AI", Provider: "grok", Enabled: false},
|
||||||
{ID: "kimi", Name: "Kimi AI", Provider: "kimi", Enabled: false},
|
{ID: "kimi", Name: "Kimi AI", Provider: "kimi", Enabled: false},
|
||||||
|
{ID: "minimax", Name: "MiniMax AI", Provider: "minimax", Enabled: false},
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, defaultModels)
|
c.JSON(http.StatusOK, defaultModels)
|
||||||
return
|
return
|
||||||
@@ -1937,6 +1939,15 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) {
|
|||||||
// Update each model's configuration and track traders that need reload
|
// Update each model's configuration and track traders that need reload
|
||||||
tradersToReload := make(map[string]bool)
|
tradersToReload := make(map[string]bool)
|
||||||
for modelID, modelData := range req.Models {
|
for modelID, modelData := range req.Models {
|
||||||
|
// SSRF protection: validate custom_api_url before storing
|
||||||
|
if modelData.CustomAPIURL != "" {
|
||||||
|
cleanURL := strings.TrimSuffix(modelData.CustomAPIURL, "#")
|
||||||
|
if err := security.ValidateURL(cleanURL); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid custom_api_url for model %s: %s", modelID, err.Error())})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Find traders using this AI model BEFORE updating
|
// Find traders using this AI model BEFORE updating
|
||||||
traders, _ := s.store.Trader().ListByAIModelID(userID, modelID)
|
traders, _ := s.store.Trader().ListByAIModelID(userID, modelID)
|
||||||
for _, t := range traders {
|
for _, t := range traders {
|
||||||
@@ -3435,6 +3446,7 @@ func (s *Server) handleGetSupportedModels(c *gin.Context) {
|
|||||||
{"id": "gemini", "name": "Google Gemini", "provider": "gemini", "defaultModel": "gemini-3-pro-preview"},
|
{"id": "gemini", "name": "Google Gemini", "provider": "gemini", "defaultModel": "gemini-3-pro-preview"},
|
||||||
{"id": "grok", "name": "Grok (xAI)", "provider": "grok", "defaultModel": "grok-3-latest"},
|
{"id": "grok", "name": "Grok (xAI)", "provider": "grok", "defaultModel": "grok-3-latest"},
|
||||||
{"id": "kimi", "name": "Kimi (Moonshot)", "provider": "kimi", "defaultModel": "moonshot-v1-auto"},
|
{"id": "kimi", "name": "Kimi (Moonshot)", "provider": "kimi", "defaultModel": "moonshot-v1-auto"},
|
||||||
|
{"id": "minimax", "name": "MiniMax", "provider": "minimax", "defaultModel": "MiniMax-M2.5"},
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, supportedModels)
|
c.JSON(http.StatusOK, supportedModels)
|
||||||
|
|||||||
@@ -665,6 +665,9 @@ func (s *Server) runRealAITest(userID, modelID, systemPrompt, userPrompt string)
|
|||||||
case "openai":
|
case "openai":
|
||||||
aiClient = mcp.NewOpenAIClient()
|
aiClient = mcp.NewOpenAIClient()
|
||||||
aiClient.SetAPIKey(apiKey, model.CustomAPIURL, model.CustomModelName)
|
aiClient.SetAPIKey(apiKey, model.CustomAPIURL, model.CustomModelName)
|
||||||
|
case "minimax":
|
||||||
|
aiClient = mcp.NewMiniMaxClient()
|
||||||
|
aiClient.SetAPIKey(apiKey, model.CustomAPIURL, model.CustomModelName)
|
||||||
default:
|
default:
|
||||||
// Use generic client
|
// Use generic client
|
||||||
aiClient = mcp.NewClient()
|
aiClient = mcp.NewClient()
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ func configureMCPClient(cfg BacktestConfig, base mcp.AIClient) (mcp.AIClient, er
|
|||||||
oaiC := mcp.NewOpenAIClientWithOptions()
|
oaiC := mcp.NewOpenAIClientWithOptions()
|
||||||
oaiC.(*mcp.OpenAIClient).SetAPIKey(cfg.AICfg.APIKey, cfg.AICfg.BaseURL, cfg.AICfg.Model)
|
oaiC.(*mcp.OpenAIClient).SetAPIKey(cfg.AICfg.APIKey, cfg.AICfg.BaseURL, cfg.AICfg.Model)
|
||||||
return oaiC, nil
|
return oaiC, nil
|
||||||
|
case "minimax":
|
||||||
|
if cfg.AICfg.APIKey == "" {
|
||||||
|
return nil, fmt.Errorf("minimax provider requires api key")
|
||||||
|
}
|
||||||
|
mmC := mcp.NewMiniMaxClientWithOptions()
|
||||||
|
mmC.(*mcp.MiniMaxClient).SetAPIKey(cfg.AICfg.APIKey, cfg.AICfg.BaseURL, cfg.AICfg.Model)
|
||||||
|
return mmC, nil
|
||||||
case "custom":
|
case "custom":
|
||||||
if cfg.AICfg.BaseURL == "" || cfg.AICfg.APIKey == "" || cfg.AICfg.Model == "" {
|
if cfg.AICfg.BaseURL == "" || cfg.AICfg.APIKey == "" || cfg.AICfg.Model == "" {
|
||||||
return nil, fmt.Errorf("custom provider requires base_url, api key and model")
|
return nil, fmt.Errorf("custom provider requires base_url, api key and model")
|
||||||
@@ -125,6 +132,11 @@ func cloneBaseClient(base mcp.AIClient) *mcp.Client {
|
|||||||
cp := *c.Client
|
cp := *c.Client
|
||||||
return &cp
|
return &cp
|
||||||
}
|
}
|
||||||
|
case *mcp.MiniMaxClient:
|
||||||
|
if c != nil && c.Client != nil {
|
||||||
|
cp := *c.Client
|
||||||
|
return &cp
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Fall back to a new default client
|
// Fall back to a new default client
|
||||||
return mcp.NewClient().(*mcp.Client)
|
return mcp.NewClient().(*mcp.Client)
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ func (e *DebateEngine) InitializeClients(participants []*store.DebateParticipant
|
|||||||
client = mcp.NewGrokClient()
|
client = mcp.NewGrokClient()
|
||||||
case "kimi":
|
case "kimi":
|
||||||
client = mcp.NewKimiClient()
|
client = mcp.NewKimiClient()
|
||||||
|
case "minimax":
|
||||||
|
client = mcp.NewMiniMaxClient()
|
||||||
default:
|
default:
|
||||||
client = mcp.New()
|
client = mcp.New()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"nofx/logger"
|
"nofx/logger"
|
||||||
|
"nofx/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config client configuration (centralized management of all configurations)
|
// Config client configuration (centralized management of all configurations)
|
||||||
@@ -48,7 +49,7 @@ func DefaultConfig() *Config {
|
|||||||
|
|
||||||
// Default dependencies (use global logger)
|
// Default dependencies (use global logger)
|
||||||
Logger: logger.NewMCPLogger(),
|
Logger: logger.NewMCPLogger(),
|
||||||
HTTPClient: &http.Client{Timeout: DefaultTimeout},
|
HTTPClient: security.SafeHTTPClient(DefaultTimeout),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
83
mcp/minimax_client.go
Normal file
83
mcp/minimax_client.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package mcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProviderMiniMax = "minimax"
|
||||||
|
DefaultMiniMaxBaseURL = "https://api.minimax.io/v1"
|
||||||
|
DefaultMiniMaxModel = "MiniMax-M2.5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MiniMaxClient struct {
|
||||||
|
*Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMiniMaxClient creates MiniMax client (backward compatible)
|
||||||
|
func NewMiniMaxClient() AIClient {
|
||||||
|
return NewMiniMaxClientWithOptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMiniMaxClientWithOptions creates MiniMax client (supports options pattern)
|
||||||
|
//
|
||||||
|
// Usage examples:
|
||||||
|
//
|
||||||
|
// // Basic usage
|
||||||
|
// client := mcp.NewMiniMaxClientWithOptions()
|
||||||
|
//
|
||||||
|
// // Custom configuration
|
||||||
|
// client := mcp.NewMiniMaxClientWithOptions(
|
||||||
|
// mcp.WithAPIKey("sk-xxx"),
|
||||||
|
// mcp.WithLogger(customLogger),
|
||||||
|
// mcp.WithTimeout(60*time.Second),
|
||||||
|
// )
|
||||||
|
func NewMiniMaxClientWithOptions(opts ...ClientOption) AIClient {
|
||||||
|
// 1. Create MiniMax preset options
|
||||||
|
minimaxOpts := []ClientOption{
|
||||||
|
WithProvider(ProviderMiniMax),
|
||||||
|
WithModel(DefaultMiniMaxModel),
|
||||||
|
WithBaseURL(DefaultMiniMaxBaseURL),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Merge user options (user options have higher priority)
|
||||||
|
allOpts := append(minimaxOpts, opts...)
|
||||||
|
|
||||||
|
// 3. Create base client
|
||||||
|
baseClient := NewClient(allOpts...).(*Client)
|
||||||
|
|
||||||
|
// 4. Create MiniMax client
|
||||||
|
minimaxClient := &MiniMaxClient{
|
||||||
|
Client: baseClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Set hooks to point to MiniMaxClient (implement dynamic dispatch)
|
||||||
|
baseClient.hooks = minimaxClient
|
||||||
|
|
||||||
|
return minimaxClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MiniMaxClient) SetAPIKey(apiKey string, customURL string, customModel string) {
|
||||||
|
c.APIKey = apiKey
|
||||||
|
|
||||||
|
if len(apiKey) > 8 {
|
||||||
|
c.logger.Infof("🔧 [MCP] MiniMax API Key: %s...%s", apiKey[:4], apiKey[len(apiKey)-4:])
|
||||||
|
}
|
||||||
|
if customURL != "" {
|
||||||
|
c.BaseURL = customURL
|
||||||
|
c.logger.Infof("🔧 [MCP] MiniMax using custom BaseURL: %s", customURL)
|
||||||
|
} else {
|
||||||
|
c.logger.Infof("🔧 [MCP] MiniMax using default BaseURL: %s", c.BaseURL)
|
||||||
|
}
|
||||||
|
if customModel != "" {
|
||||||
|
c.Model = customModel
|
||||||
|
c.logger.Infof("🔧 [MCP] MiniMax using custom Model: %s", customModel)
|
||||||
|
} else {
|
||||||
|
c.logger.Infof("🔧 [MCP] MiniMax using default Model: %s", c.Model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiniMax uses standard OpenAI-compatible API with Bearer auth
|
||||||
|
func (c *MiniMaxClient) setAuthHeader(reqHeaders http.Header) {
|
||||||
|
c.Client.setAuthHeader(reqHeaders)
|
||||||
|
}
|
||||||
272
mcp/minimax_client_test.go
Normal file
272
mcp/minimax_client_test.go
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
package mcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Test MiniMaxClient Creation and Configuration
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
func TestNewMiniMaxClient_Default(t *testing.T) {
|
||||||
|
client := NewMiniMaxClient()
|
||||||
|
|
||||||
|
if client == nil {
|
||||||
|
t.Fatal("client should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type assertion check
|
||||||
|
mmClient, ok := client.(*MiniMaxClient)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("client should be *MiniMaxClient")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify default values
|
||||||
|
if mmClient.Provider != ProviderMiniMax {
|
||||||
|
t.Errorf("Provider should be '%s', got '%s'", ProviderMiniMax, mmClient.Provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mmClient.BaseURL != DefaultMiniMaxBaseURL {
|
||||||
|
t.Errorf("BaseURL should be '%s', got '%s'", DefaultMiniMaxBaseURL, mmClient.BaseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mmClient.Model != DefaultMiniMaxModel {
|
||||||
|
t.Errorf("Model should be '%s', got '%s'", DefaultMiniMaxModel, mmClient.Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mmClient.logger == nil {
|
||||||
|
t.Error("logger should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mmClient.httpClient == nil {
|
||||||
|
t.Error("httpClient should not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewMiniMaxClientWithOptions(t *testing.T) {
|
||||||
|
mockLogger := NewMockLogger()
|
||||||
|
customModel := "MiniMax-M2.5-highspeed"
|
||||||
|
customAPIKey := "sk-custom-key"
|
||||||
|
|
||||||
|
client := NewMiniMaxClientWithOptions(
|
||||||
|
WithLogger(mockLogger),
|
||||||
|
WithModel(customModel),
|
||||||
|
WithAPIKey(customAPIKey),
|
||||||
|
WithMaxTokens(4000),
|
||||||
|
)
|
||||||
|
|
||||||
|
mmClient := client.(*MiniMaxClient)
|
||||||
|
|
||||||
|
// Verify custom options are applied
|
||||||
|
if mmClient.logger != mockLogger {
|
||||||
|
t.Error("logger should be set from option")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mmClient.Model != customModel {
|
||||||
|
t.Error("Model should be set from option")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mmClient.APIKey != customAPIKey {
|
||||||
|
t.Error("APIKey should be set from option")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mmClient.MaxTokens != 4000 {
|
||||||
|
t.Error("MaxTokens should be 4000")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify MiniMax default values are retained
|
||||||
|
if mmClient.Provider != ProviderMiniMax {
|
||||||
|
t.Errorf("Provider should still be '%s'", ProviderMiniMax)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mmClient.BaseURL != DefaultMiniMaxBaseURL {
|
||||||
|
t.Errorf("BaseURL should still be '%s'", DefaultMiniMaxBaseURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Test SetAPIKey
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
func TestMiniMaxClient_SetAPIKey(t *testing.T) {
|
||||||
|
mockLogger := NewMockLogger()
|
||||||
|
client := NewMiniMaxClientWithOptions(
|
||||||
|
WithLogger(mockLogger),
|
||||||
|
)
|
||||||
|
|
||||||
|
mmClient := client.(*MiniMaxClient)
|
||||||
|
|
||||||
|
// Test setting API Key (default URL and Model)
|
||||||
|
mmClient.SetAPIKey("sk-test-key-12345678", "", "")
|
||||||
|
|
||||||
|
if mmClient.APIKey != "sk-test-key-12345678" {
|
||||||
|
t.Errorf("APIKey should be 'sk-test-key-12345678', got '%s'", mmClient.APIKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify logging
|
||||||
|
logs := mockLogger.GetLogsByLevel("INFO")
|
||||||
|
if len(logs) == 0 {
|
||||||
|
t.Error("should have logged API key setting")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify BaseURL and Model remain default
|
||||||
|
if mmClient.BaseURL != DefaultMiniMaxBaseURL {
|
||||||
|
t.Error("BaseURL should remain default")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mmClient.Model != DefaultMiniMaxModel {
|
||||||
|
t.Error("Model should remain default")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxClient_SetAPIKey_WithCustomURL(t *testing.T) {
|
||||||
|
mockLogger := NewMockLogger()
|
||||||
|
client := NewMiniMaxClientWithOptions(
|
||||||
|
WithLogger(mockLogger),
|
||||||
|
)
|
||||||
|
|
||||||
|
mmClient := client.(*MiniMaxClient)
|
||||||
|
|
||||||
|
customURL := "https://api.minimaxi.com/v1"
|
||||||
|
mmClient.SetAPIKey("sk-test-key-12345678", customURL, "")
|
||||||
|
|
||||||
|
if mmClient.BaseURL != customURL {
|
||||||
|
t.Errorf("BaseURL should be '%s', got '%s'", customURL, mmClient.BaseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify logging
|
||||||
|
logs := mockLogger.GetLogsByLevel("INFO")
|
||||||
|
hasCustomURLLog := false
|
||||||
|
for _, log := range logs {
|
||||||
|
if log.Format == "🔧 [MCP] MiniMax using custom BaseURL: %s" {
|
||||||
|
hasCustomURLLog = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasCustomURLLog {
|
||||||
|
t.Error("should have logged custom BaseURL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxClient_SetAPIKey_WithCustomModel(t *testing.T) {
|
||||||
|
mockLogger := NewMockLogger()
|
||||||
|
client := NewMiniMaxClientWithOptions(
|
||||||
|
WithLogger(mockLogger),
|
||||||
|
)
|
||||||
|
|
||||||
|
mmClient := client.(*MiniMaxClient)
|
||||||
|
|
||||||
|
customModel := "MiniMax-M2.5-highspeed"
|
||||||
|
mmClient.SetAPIKey("sk-test-key-12345678", "", customModel)
|
||||||
|
|
||||||
|
if mmClient.Model != customModel {
|
||||||
|
t.Errorf("Model should be '%s', got '%s'", customModel, mmClient.Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify logging
|
||||||
|
logs := mockLogger.GetLogsByLevel("INFO")
|
||||||
|
hasCustomModelLog := false
|
||||||
|
for _, log := range logs {
|
||||||
|
if log.Format == "🔧 [MCP] MiniMax using custom Model: %s" {
|
||||||
|
hasCustomModelLog = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasCustomModelLog {
|
||||||
|
t.Error("should have logged custom Model")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Test Integration Features
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
func TestMiniMaxClient_CallWithMessages_Success(t *testing.T) {
|
||||||
|
mockHTTP := NewMockHTTPClient()
|
||||||
|
mockHTTP.SetSuccessResponse("MiniMax AI response")
|
||||||
|
mockLogger := NewMockLogger()
|
||||||
|
|
||||||
|
client := NewMiniMaxClientWithOptions(
|
||||||
|
WithHTTPClient(mockHTTP.ToHTTPClient()),
|
||||||
|
WithLogger(mockLogger),
|
||||||
|
WithAPIKey("sk-test-key"),
|
||||||
|
)
|
||||||
|
|
||||||
|
result, err := client.CallWithMessages("system prompt", "user prompt")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != "MiniMax AI response" {
|
||||||
|
t.Errorf("expected 'MiniMax AI response', got '%s'", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify request
|
||||||
|
requests := mockHTTP.GetRequests()
|
||||||
|
if len(requests) != 1 {
|
||||||
|
t.Fatalf("expected 1 request, got %d", len(requests))
|
||||||
|
}
|
||||||
|
|
||||||
|
req := requests[0]
|
||||||
|
|
||||||
|
// Verify URL
|
||||||
|
expectedURL := DefaultMiniMaxBaseURL + "/chat/completions"
|
||||||
|
if req.URL.String() != expectedURL {
|
||||||
|
t.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Authorization header
|
||||||
|
authHeader := req.Header.Get("Authorization")
|
||||||
|
if authHeader != "Bearer sk-test-key" {
|
||||||
|
t.Errorf("expected 'Bearer sk-test-key', got '%s'", authHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Content-Type
|
||||||
|
if req.Header.Get("Content-Type") != "application/json" {
|
||||||
|
t.Error("Content-Type should be application/json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxClient_Timeout(t *testing.T) {
|
||||||
|
client := NewMiniMaxClientWithOptions(
|
||||||
|
WithTimeout(30 * time.Second),
|
||||||
|
)
|
||||||
|
|
||||||
|
mmClient := client.(*MiniMaxClient)
|
||||||
|
|
||||||
|
if mmClient.httpClient.Timeout != 30*time.Second {
|
||||||
|
t.Errorf("expected timeout 30s, got %v", mmClient.httpClient.Timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SetTimeout
|
||||||
|
client.SetTimeout(60 * time.Second)
|
||||||
|
|
||||||
|
if mmClient.httpClient.Timeout != 60*time.Second {
|
||||||
|
t.Errorf("expected timeout 60s after SetTimeout, got %v", mmClient.httpClient.Timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Test hooks Mechanism
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
func TestMiniMaxClient_HooksIntegration(t *testing.T) {
|
||||||
|
client := NewMiniMaxClientWithOptions()
|
||||||
|
mmClient := client.(*MiniMaxClient)
|
||||||
|
|
||||||
|
// Verify hooks point to mmClient itself (implements polymorphism)
|
||||||
|
if mmClient.hooks != mmClient {
|
||||||
|
t.Error("hooks should point to mmClient for polymorphism")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify buildUrl uses MiniMax configuration
|
||||||
|
url := mmClient.buildUrl()
|
||||||
|
expectedURL := DefaultMiniMaxBaseURL + "/chat/completions"
|
||||||
|
if url != expectedURL {
|
||||||
|
t.Errorf("expected URL '%s', got '%s'", expectedURL, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,11 @@ func WithLogger(logger Logger) ClientOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithHTTPClient sets custom HTTP client
|
// WithHTTPClient sets custom HTTP client.
|
||||||
|
//
|
||||||
|
// WARNING: The default client uses security.SafeHTTPClient() with SSRF protection
|
||||||
|
// (blocks private IPs, cloud metadata, validates redirects). Overriding it bypasses
|
||||||
|
// these protections. Only use in tests or with a client providing equivalent safeguards.
|
||||||
//
|
//
|
||||||
// Usage example:
|
// Usage example:
|
||||||
// httpClient := &http.Client{Timeout: 60 * time.Second}
|
// httpClient := &http.Client{Timeout: 60 * time.Second}
|
||||||
@@ -160,3 +164,17 @@ func WithQwenConfig(apiKey string) ClientOption {
|
|||||||
c.Model = DefaultQwenModel
|
c.Model = DefaultQwenModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithMiniMaxConfig sets MiniMax configuration
|
||||||
|
//
|
||||||
|
// Usage example:
|
||||||
|
//
|
||||||
|
// client := mcp.NewClient(mcp.WithMiniMaxConfig("sk-xxx"))
|
||||||
|
func WithMiniMaxConfig(apiKey string) ClientOption {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.Provider = ProviderMiniMax
|
||||||
|
c.APIKey = apiKey
|
||||||
|
c.BaseURL = DefaultMiniMaxBaseURL
|
||||||
|
c.Model = DefaultMiniMaxModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -201,6 +201,11 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au
|
|||||||
mcpClient.SetAPIKey(config.CustomAPIKey, config.CustomAPIURL, config.CustomModelName)
|
mcpClient.SetAPIKey(config.CustomAPIKey, config.CustomAPIURL, config.CustomModelName)
|
||||||
logger.Infof("🤖 [%s] Using OpenAI", config.Name)
|
logger.Infof("🤖 [%s] Using OpenAI", config.Name)
|
||||||
|
|
||||||
|
case "minimax":
|
||||||
|
mcpClient = mcp.NewMiniMaxClient()
|
||||||
|
mcpClient.SetAPIKey(config.CustomAPIKey, config.CustomAPIURL, config.CustomModelName)
|
||||||
|
logger.Infof("🤖 [%s] Using MiniMax AI", config.Name)
|
||||||
|
|
||||||
case "qwen":
|
case "qwen":
|
||||||
mcpClient = mcp.NewQwenClient()
|
mcpClient = mcp.NewQwenClient()
|
||||||
apiKey := config.QwenKey
|
apiKey := config.QwenKey
|
||||||
|
|||||||
4
web/public/icons/minimax.svg
Normal file
4
web/public/icons/minimax.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect width="24" height="24" rx="4" fill="#E45735"/>
|
||||||
|
<text x="12" y="16" text-anchor="middle" font-family="Arial, sans-serif" font-weight="bold" font-size="10" fill="white">M</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 266 B |
@@ -98,6 +98,11 @@ const AI_PROVIDER_CONFIG: Record<string, {
|
|||||||
apiUrl: 'https://platform.moonshot.ai/console/api-keys',
|
apiUrl: 'https://platform.moonshot.ai/console/api-keys',
|
||||||
apiName: 'Moonshot',
|
apiName: 'Moonshot',
|
||||||
},
|
},
|
||||||
|
minimax: {
|
||||||
|
defaultModel: 'MiniMax-M2.5',
|
||||||
|
apiUrl: 'https://platform.minimax.io',
|
||||||
|
apiName: 'MiniMax',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AITradersPageProps {
|
interface AITradersPageProps {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const MODEL_COLORS: Record<string, string> = {
|
|||||||
gemini: '#4285F4',
|
gemini: '#4285F4',
|
||||||
grok: '#000000',
|
grok: '#000000',
|
||||||
openai: '#10A37F',
|
openai: '#10A37F',
|
||||||
|
minimax: '#E45735',
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取AI模型图标的函数
|
// 获取AI模型图标的函数
|
||||||
@@ -44,6 +45,9 @@ export const getModelIcon = (modelType: string, props: IconProps = {}) => {
|
|||||||
case 'openai':
|
case 'openai':
|
||||||
iconPath = '/icons/openai.svg'
|
iconPath = '/icons/openai.svg'
|
||||||
break
|
break
|
||||||
|
case 'minimax':
|
||||||
|
iconPath = '/icons/minimax.svg'
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ function AIAvatar({ name, size = 24 }: { name: string; size?: number }) {
|
|||||||
kimi: { bg: 'bg-purple-500', text: 'text-white', letter: 'K' },
|
kimi: { bg: 'bg-purple-500', text: 'text-white', letter: 'K' },
|
||||||
qwen: { bg: 'bg-indigo-500', text: 'text-white', letter: 'Q' },
|
qwen: { bg: 'bg-indigo-500', text: 'text-white', letter: 'Q' },
|
||||||
openai: { bg: 'bg-emerald-600', text: 'text-white', letter: 'O' },
|
openai: { bg: 'bg-emerald-600', text: 'text-white', letter: 'O' },
|
||||||
|
minimax: { bg: 'bg-red-500', text: 'text-white', letter: 'M' },
|
||||||
gpt: { bg: 'bg-emerald-600', text: 'text-white', letter: 'O' },
|
gpt: { bg: 'bg-emerald-600', text: 'text-white', letter: 'O' },
|
||||||
}
|
}
|
||||||
const lower = name.toLowerCase()
|
const lower = name.toLowerCase()
|
||||||
|
|||||||
Reference in New Issue
Block a user