diff --git a/api/backtest.go b/api/backtest.go
index acdfe378..82b5eecb 100644
--- a/api/backtest.go
+++ b/api/backtest.go
@@ -832,6 +832,8 @@ func (s *Server) hydrateBacktestAIConfig(cfg *backtest.BacktestConfig) error {
provider = "google"
} else if strings.Contains(modelNameLower, "deepseek") {
provider = "deepseek"
+ } else if strings.Contains(modelNameLower, "minimax") {
+ provider = "minimax"
} else if model.CustomAPIURL != "" {
provider = "custom"
} else {
diff --git a/api/server.go b/api/server.go
index fc545a54..16759092 100644
--- a/api/server.go
+++ b/api/server.go
@@ -1683,6 +1683,7 @@ func (s *Server) handleGetModelConfigs(c *gin.Context) {
{ID: "gemini", Name: "Gemini AI", Provider: "gemini", Enabled: false},
{ID: "grok", Name: "Grok AI", Provider: "grok", 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)
return
@@ -3252,6 +3253,7 @@ func (s *Server) handleGetSupportedModels(c *gin.Context) {
{"id": "gemini", "name": "Google Gemini", "provider": "gemini", "defaultModel": "gemini-3-pro-preview"},
{"id": "grok", "name": "Grok (xAI)", "provider": "grok", "defaultModel": "grok-3-latest"},
{"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)
diff --git a/api/strategy.go b/api/strategy.go
index 5f724ab6..ec11f7a9 100644
--- a/api/strategy.go
+++ b/api/strategy.go
@@ -625,6 +625,9 @@ func (s *Server) runRealAITest(userID, modelID, systemPrompt, userPrompt string)
case "openai":
aiClient = mcp.NewOpenAIClient()
aiClient.SetAPIKey(apiKey, model.CustomAPIURL, model.CustomModelName)
+ case "minimax":
+ aiClient = mcp.NewMiniMaxClient()
+ aiClient.SetAPIKey(apiKey, model.CustomAPIURL, model.CustomModelName)
default:
// Use generic client
aiClient = mcp.NewClient()
diff --git a/backtest/ai_client.go b/backtest/ai_client.go
index 4eeb0855..74c34761 100644
--- a/backtest/ai_client.go
+++ b/backtest/ai_client.go
@@ -71,6 +71,13 @@ func configureMCPClient(cfg BacktestConfig, base mcp.AIClient) (mcp.AIClient, er
oaiC := mcp.NewOpenAIClientWithOptions()
oaiC.(*mcp.OpenAIClient).SetAPIKey(cfg.AICfg.APIKey, cfg.AICfg.BaseURL, cfg.AICfg.Model)
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":
if cfg.AICfg.BaseURL == "" || cfg.AICfg.APIKey == "" || cfg.AICfg.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
return &cp
}
+ case *mcp.MiniMaxClient:
+ if c != nil && c.Client != nil {
+ cp := *c.Client
+ return &cp
+ }
}
// Fall back to a new default client
return mcp.NewClient().(*mcp.Client)
diff --git a/debate/engine.go b/debate/engine.go
index 4700d2ff..9d9dffb1 100644
--- a/debate/engine.go
+++ b/debate/engine.go
@@ -97,6 +97,8 @@ func (e *DebateEngine) InitializeClients(participants []*store.DebateParticipant
client = mcp.NewGrokClient()
case "kimi":
client = mcp.NewKimiClient()
+ case "minimax":
+ client = mcp.NewMiniMaxClient()
default:
client = mcp.New()
}
diff --git a/mcp/minimax_client.go b/mcp/minimax_client.go
new file mode 100644
index 00000000..7bedb15a
--- /dev/null
+++ b/mcp/minimax_client.go
@@ -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)
+}
diff --git a/mcp/minimax_client_test.go b/mcp/minimax_client_test.go
new file mode 100644
index 00000000..21e45e22
--- /dev/null
+++ b/mcp/minimax_client_test.go
@@ -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)
+ }
+}
diff --git a/mcp/options.go b/mcp/options.go
index 3e962627..2a6fb03d 100644
--- a/mcp/options.go
+++ b/mcp/options.go
@@ -160,3 +160,17 @@ func WithQwenConfig(apiKey string) ClientOption {
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
+ }
+}
diff --git a/trader/auto_trader.go b/trader/auto_trader.go
index 9145e476..baedeaa6 100644
--- a/trader/auto_trader.go
+++ b/trader/auto_trader.go
@@ -201,6 +201,11 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au
mcpClient.SetAPIKey(config.CustomAPIKey, config.CustomAPIURL, config.CustomModelName)
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":
mcpClient = mcp.NewQwenClient()
apiKey := config.QwenKey
diff --git a/web/public/icons/minimax.svg b/web/public/icons/minimax.svg
new file mode 100644
index 00000000..332eb759
--- /dev/null
+++ b/web/public/icons/minimax.svg
@@ -0,0 +1,4 @@
+
diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx
index f0de17b2..71a29fe1 100644
--- a/web/src/components/AITradersPage.tsx
+++ b/web/src/components/AITradersPage.tsx
@@ -96,6 +96,11 @@ const AI_PROVIDER_CONFIG: Record = {
gemini: '#4285F4',
grok: '#000000',
openai: '#10A37F',
+ minimax: '#E45735',
}
// ่ทๅAIๆจกๅๅพๆ ็ๅฝๆฐ
@@ -44,6 +45,9 @@ export const getModelIcon = (modelType: string, props: IconProps = {}) => {
case 'openai':
iconPath = '/icons/openai.svg'
break
+ case 'minimax':
+ iconPath = '/icons/minimax.svg'
+ break
default:
return null
}
diff --git a/web/src/pages/DebateArenaPage.tsx b/web/src/pages/DebateArenaPage.tsx
index 21df55ad..eda7b9bd 100644
--- a/web/src/pages/DebateArenaPage.tsx
+++ b/web/src/pages/DebateArenaPage.tsx
@@ -104,6 +104,7 @@ function AIAvatar({ name, size = 24 }: { name: string; size?: number }) {
kimi: { bg: 'bg-purple-500', text: 'text-white', letter: 'K' },
qwen: { bg: 'bg-indigo-500', text: 'text-white', letter: 'Q' },
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' },
}
const lower = name.toLowerCase()