From 9ca254efa31ad55b746a6be22672e851540c4b73 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Wed, 3 Jun 2026 12:54:50 +0800 Subject: [PATCH] feat: upgrade MiniMax default model to M3 - Add MiniMax provider with M3 as the default model (512K context, 128K max output, image input support) - Keep MiniMax-M2.7 and MiniMax-M2.7-highspeed as alternatives - Configure agent defaults (simple, primary_agent, assistant, generator, etc.) on M3 - Register MiniMax provider type, env vars (MINIMAX_API_KEY, MINIMAX_SERVER_URL, MINIMAX_PROVIDER) - Add unit tests covering config loading, models list, prefix behavior, and missing API key --- backend/pkg/config/config.go | 6 + backend/pkg/providers/minimax/config.yml | 130 ++++++++++ backend/pkg/providers/minimax/minimax.go | 194 +++++++++++++++ backend/pkg/providers/minimax/minimax_test.go | 229 ++++++++++++++++++ backend/pkg/providers/minimax/models.yml | 20 ++ backend/pkg/providers/provider/provider.go | 2 + backend/pkg/providers/providers.go | 26 ++ backend/pkg/server/models/providers.go | 3 +- 8 files changed, 609 insertions(+), 1 deletion(-) create mode 100644 backend/pkg/providers/minimax/config.yml create mode 100644 backend/pkg/providers/minimax/minimax.go create mode 100644 backend/pkg/providers/minimax/minimax_test.go create mode 100644 backend/pkg/providers/minimax/models.yml diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index 9ad47c973..a0dce8732 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -135,6 +135,11 @@ type Config struct { QwenServerURL string `env:"QWEN_SERVER_URL" envDefault:"https://dashscope-us.aliyuncs.com/compatible-mode/v1"` QwenProvider string `env:"QWEN_PROVIDER"` + // === LLM Provider: MiniMax === + MiniMaxAPIKey string `env:"MINIMAX_API_KEY"` + MiniMaxServerURL string `env:"MINIMAX_SERVER_URL" envDefault:"https://api.minimax.io/v1"` + MiniMaxProvider string `env:"MINIMAX_PROVIDER"` + // === Search Engine: DuckDuckGo === DuckDuckGoEnabled bool `env:"DUCKDUCKGO_ENABLED" envDefault:"true"` DuckDuckGoRegion string `env:"DUCKDUCKGO_REGION"` @@ -322,6 +327,7 @@ func (c *Config) GetSecretPatterns() []patterns.Pattern { {c.GLMAPIKey, "GLM Key"}, {c.KimiAPIKey, "Kimi Key"}, {c.QwenAPIKey, "Qwen Key"}, + {c.MiniMaxAPIKey, "MiniMax Key"}, {c.GoogleAPIKey, "Google API Key"}, {c.GoogleCXKey, "Google CX Key"}, {c.OAuthGoogleClientID, "Google Client ID"}, diff --git a/backend/pkg/providers/minimax/config.yml b/backend/pkg/providers/minimax/config.yml new file mode 100644 index 000000000..c9ddde53d --- /dev/null +++ b/backend/pkg/providers/minimax/config.yml @@ -0,0 +1,130 @@ +simple: + model: MiniMax-M3 + temperature: 0.5 + top_p: 0.5 + n: 1 + max_tokens: 8192 + price: + input: 0.60 + output: 2.40 + +simple_json: + model: MiniMax-M3 + temperature: 0.5 + top_p: 0.5 + n: 1 + max_tokens: 4096 + json: true + price: + input: 0.60 + output: 2.40 + +primary_agent: + model: MiniMax-M3 + temperature: 0.7 + top_p: 0.8 + n: 1 + max_tokens: 16384 + price: + input: 0.60 + output: 2.40 + +assistant: + model: MiniMax-M3 + temperature: 0.7 + top_p: 0.8 + n: 1 + max_tokens: 16384 + price: + input: 0.60 + output: 2.40 + +generator: + model: MiniMax-M3 + temperature: 0.7 + top_p: 0.8 + n: 1 + max_tokens: 32768 + price: + input: 0.60 + output: 2.40 + +refiner: + model: MiniMax-M3 + temperature: 0.7 + top_p: 0.8 + n: 1 + max_tokens: 20480 + price: + input: 0.60 + output: 2.40 + +adviser: + model: MiniMax-M3 + temperature: 0.7 + top_p: 0.8 + n: 1 + max_tokens: 8192 + price: + input: 0.60 + output: 2.40 + +reflector: + model: MiniMax-M3 + temperature: 0.5 + top_p: 0.5 + n: 1 + max_tokens: 4096 + price: + input: 0.60 + output: 2.40 + +searcher: + model: MiniMax-M3 + temperature: 0.7 + top_p: 0.8 + n: 1 + max_tokens: 4096 + price: + input: 0.60 + output: 2.40 + +enricher: + model: MiniMax-M3 + temperature: 0.7 + top_p: 0.8 + n: 1 + max_tokens: 4096 + price: + input: 0.60 + output: 2.40 + +coder: + model: MiniMax-M3 + temperature: 0.5 + top_p: 0.5 + n: 1 + max_tokens: 20480 + price: + input: 0.60 + output: 2.40 + +installer: + model: MiniMax-M3 + temperature: 0.5 + top_p: 0.5 + n: 1 + max_tokens: 16384 + price: + input: 0.60 + output: 2.40 + +pentester: + model: MiniMax-M3 + temperature: 0.5 + top_p: 0.5 + n: 1 + max_tokens: 16384 + price: + input: 0.60 + output: 2.40 diff --git a/backend/pkg/providers/minimax/minimax.go b/backend/pkg/providers/minimax/minimax.go new file mode 100644 index 000000000..ffd69ec21 --- /dev/null +++ b/backend/pkg/providers/minimax/minimax.go @@ -0,0 +1,194 @@ +package minimax + +import ( + "context" + "embed" + "fmt" + + "pentagi/pkg/config" + "pentagi/pkg/providers/pconfig" + "pentagi/pkg/providers/provider" + "pentagi/pkg/system" + "pentagi/pkg/templates" + + "github.com/vxcontrol/langchaingo/llms" + "github.com/vxcontrol/langchaingo/llms/openai" + "github.com/vxcontrol/langchaingo/llms/streaming" +) + +//go:embed config.yml models.yml +var configFS embed.FS + +// MiniMaxAgentModel is the fallback model used when no agent-specific configuration exists. +// MiniMax-M3 is the latest flagship model (512K context, up to 128K output, image input support) +// and serves as the default for all agent types. +const MiniMaxAgentModel = "MiniMax-M3" + +// MiniMaxToolCallIDTemplate is the pattern template for tool call IDs used by MiniMax. +const MiniMaxToolCallIDTemplate = "call_{r:24:b}" + +func BuildProviderConfig(configData []byte) (*pconfig.ProviderConfig, error) { + defaultOptions := []llms.CallOption{ + llms.WithModel(MiniMaxAgentModel), + llms.WithN(1), + llms.WithMaxTokens(4000), + } + + providerConfig, err := pconfig.LoadConfigData(configData, defaultOptions) + if err != nil { + return nil, err + } + + return providerConfig, nil +} + +func DefaultProviderConfig() (*pconfig.ProviderConfig, error) { + configData, err := configFS.ReadFile("config.yml") + if err != nil { + return nil, err + } + + return BuildProviderConfig(configData) +} + +func DefaultModels() (pconfig.ModelsConfig, error) { + configData, err := configFS.ReadFile("models.yml") + if err != nil { + return nil, err + } + + return pconfig.LoadModelsConfigData(configData) +} + +type minimaxProvider struct { + llm *openai.LLM + models pconfig.ModelsConfig + providerName provider.ProviderName + providerConfig *pconfig.ProviderConfig + providerPrefix string +} + +func New( + cfg *config.Config, + providerName provider.ProviderName, + providerConfig *pconfig.ProviderConfig, +) (provider.Provider, error) { + if cfg.MiniMaxAPIKey == "" { + return nil, fmt.Errorf("missing MINIMAX_API_KEY environment variable") + } + + httpClient, err := system.GetHTTPClient(cfg) + if err != nil { + return nil, err + } + + models, err := DefaultModels() + if err != nil { + return nil, err + } + + client, err := openai.New( + openai.WithToken(cfg.MiniMaxAPIKey), + openai.WithModel(MiniMaxAgentModel), + openai.WithBaseURL(cfg.MiniMaxServerURL), + openai.WithHTTPClient(httpClient), + ) + if err != nil { + return nil, err + } + + return &minimaxProvider{ + llm: client, + models: models, + providerName: providerName, + providerConfig: providerConfig, + providerPrefix: cfg.MiniMaxProvider, + }, nil +} + +func (p *minimaxProvider) Type() provider.ProviderType { + return provider.ProviderMiniMax +} + +func (p *minimaxProvider) Name() provider.ProviderName { + return p.providerName +} + +func (p *minimaxProvider) GetRawConfig() []byte { + return p.providerConfig.GetRawConfig() +} + +func (p *minimaxProvider) GetProviderConfig() *pconfig.ProviderConfig { + return p.providerConfig +} + +func (p *minimaxProvider) GetPriceInfo(opt pconfig.ProviderOptionsType) *pconfig.PriceInfo { + return p.providerConfig.GetPriceInfoForType(opt) +} + +func (p *minimaxProvider) GetModels() pconfig.ModelsConfig { + return p.models +} + +func (p *minimaxProvider) Model(opt pconfig.ProviderOptionsType) string { + model := MiniMaxAgentModel + opts := llms.CallOptions{Model: &model} + for _, option := range p.providerConfig.GetOptionsForType(opt) { + option(&opts) + } + + return opts.GetModel() +} + +func (p *minimaxProvider) ModelWithPrefix(opt pconfig.ProviderOptionsType) string { + return provider.ApplyModelPrefix(p.Model(opt), p.providerPrefix) +} + +func (p *minimaxProvider) Call( + ctx context.Context, + opt pconfig.ProviderOptionsType, + prompt string, +) (string, error) { + return provider.WrapGenerateFromSinglePrompt( + ctx, p, opt, p.llm, prompt, + p.providerConfig.GetOptionsForType(opt)..., + ) +} + +func (p *minimaxProvider) CallEx( + ctx context.Context, + opt pconfig.ProviderOptionsType, + chain []llms.MessageContent, + streamCb streaming.Callback, +) (*llms.ContentResponse, error) { + return provider.WrapGenerateContent( + ctx, p, opt, p.llm.GenerateContent, chain, + append([]llms.CallOption{ + llms.WithStreamingFunc(streamCb), + }, p.providerConfig.GetOptionsForType(opt)...)..., + ) +} + +func (p *minimaxProvider) CallWithTools( + ctx context.Context, + opt pconfig.ProviderOptionsType, + chain []llms.MessageContent, + tools []llms.Tool, + streamCb streaming.Callback, +) (*llms.ContentResponse, error) { + return provider.WrapGenerateContent( + ctx, p, opt, p.llm.GenerateContent, chain, + append([]llms.CallOption{ + llms.WithTools(tools), + llms.WithStreamingFunc(streamCb), + }, p.providerConfig.GetOptionsForType(opt)...)..., + ) +} + +func (p *minimaxProvider) GetUsage(info map[string]any) pconfig.CallUsage { + return pconfig.NewCallUsage(info) +} + +func (p *minimaxProvider) GetToolCallIDTemplate(ctx context.Context, prompter templates.Prompter) (string, error) { + return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter, MiniMaxToolCallIDTemplate) +} diff --git a/backend/pkg/providers/minimax/minimax_test.go b/backend/pkg/providers/minimax/minimax_test.go new file mode 100644 index 000000000..e4aa77e36 --- /dev/null +++ b/backend/pkg/providers/minimax/minimax_test.go @@ -0,0 +1,229 @@ +package minimax + +import ( + "testing" + + "pentagi/pkg/config" + "pentagi/pkg/providers/pconfig" + "pentagi/pkg/providers/provider" +) + +func TestConfigLoading(t *testing.T) { + cfg := &config.Config{ + MiniMaxAPIKey: "test-key", + MiniMaxServerURL: "https://api.minimax.io/v1", + } + + providerConfig, err := DefaultProviderConfig() + if err != nil { + t.Fatalf("Failed to create provider config: %v", err) + } + + prov, err := New(cfg, provider.DefaultProviderNameMiniMax, providerConfig) + if err != nil { + t.Fatalf("Failed to create provider: %v", err) + } + + rawConfig := prov.GetRawConfig() + if len(rawConfig) == 0 { + t.Fatal("Raw config should not be empty") + } + + providerConfig = prov.GetProviderConfig() + if providerConfig == nil { + t.Fatal("Provider config should not be nil") + } + + for _, agentType := range pconfig.AllAgentTypes { + model := prov.Model(agentType) + if model == "" { + t.Errorf("Agent type %v should have a model assigned", agentType) + } + } + + for _, agentType := range pconfig.AllAgentTypes { + priceInfo := prov.GetPriceInfo(agentType) + if priceInfo == nil { + t.Errorf("Agent type %v should have price information", agentType) + } else { + if priceInfo.Input <= 0 || priceInfo.Output <= 0 { + t.Errorf("Agent type %v should have positive input (%f) and output (%f) prices", + agentType, priceInfo.Input, priceInfo.Output) + } + } + } +} + +func TestProviderType(t *testing.T) { + cfg := &config.Config{ + MiniMaxAPIKey: "test-key", + MiniMaxServerURL: "https://api.minimax.io/v1", + } + + providerConfig, err := DefaultProviderConfig() + if err != nil { + t.Fatalf("Failed to create provider config: %v", err) + } + + prov, err := New(cfg, provider.DefaultProviderNameMiniMax, providerConfig) + if err != nil { + t.Fatalf("Failed to create provider: %v", err) + } + + if prov.Type() != provider.ProviderMiniMax { + t.Errorf("Expected provider type %v, got %v", provider.ProviderMiniMax, prov.Type()) + } +} + +func TestModelsLoading(t *testing.T) { + models, err := DefaultModels() + if err != nil { + t.Fatalf("Failed to load models: %v", err) + } + + if len(models) == 0 { + t.Fatal("Models list should not be empty") + } + + wantModels := map[string]bool{ + "MiniMax-M3": false, + "MiniMax-M2.7": false, + "MiniMax-M2.7-highspeed": false, + } + + for _, model := range models { + if model.Name == "" { + t.Error("Model name should not be empty") + } + + if model.Price == nil { + t.Errorf("Model %s should have price information", model.Name) + continue + } + + if model.Price.Input <= 0 { + t.Errorf("Model %s should have positive input price", model.Name) + } + + if model.Price.Output <= 0 { + t.Errorf("Model %s should have positive output price", model.Name) + } + + if _, ok := wantModels[model.Name]; ok { + wantModels[model.Name] = true + } + } + + for name, found := range wantModels { + if !found { + t.Errorf("Expected model %q in models list, but it was not found", name) + } + } + + if models[0].Name != "MiniMax-M3" { + t.Errorf("Expected first model to be MiniMax-M3 (default), got %q", models[0].Name) + } + + for _, model := range models { + if model.Name == "MiniMax-M2.5" || model.Name == "MiniMax-M2.5-highspeed" || + model.Name == "MiniMax-M2.1" || model.Name == "MiniMax-M2" || model.Name == "MiniMax-M1" { + t.Errorf("Legacy model %q should not be in the model list", model.Name) + } + } +} + +func TestModelWithPrefix(t *testing.T) { + cfg := &config.Config{ + MiniMaxAPIKey: "test-key", + MiniMaxServerURL: "https://api.minimax.io/v1", + MiniMaxProvider: "minimax", + } + + providerConfig, err := DefaultProviderConfig() + if err != nil { + t.Fatalf("Failed to create provider config: %v", err) + } + + prov, err := New(cfg, provider.DefaultProviderNameMiniMax, providerConfig) + if err != nil { + t.Fatalf("Failed to create provider: %v", err) + } + + for _, agentType := range pconfig.AllAgentTypes { + modelWithPrefix := prov.ModelWithPrefix(agentType) + model := prov.Model(agentType) + + expected := "minimax/" + model + if modelWithPrefix != expected { + t.Errorf("Agent type %v: expected prefixed model %q, got %q", agentType, expected, modelWithPrefix) + } + } +} + +func TestModelWithoutPrefix(t *testing.T) { + cfg := &config.Config{ + MiniMaxAPIKey: "test-key", + MiniMaxServerURL: "https://api.minimax.io/v1", + } + + providerConfig, err := DefaultProviderConfig() + if err != nil { + t.Fatalf("Failed to create provider config: %v", err) + } + + prov, err := New(cfg, provider.DefaultProviderNameMiniMax, providerConfig) + if err != nil { + t.Fatalf("Failed to create provider: %v", err) + } + + for _, agentType := range pconfig.AllAgentTypes { + modelWithPrefix := prov.ModelWithPrefix(agentType) + model := prov.Model(agentType) + + if modelWithPrefix != model { + t.Errorf("Agent type %v: without prefix, ModelWithPrefix (%q) should equal Model (%q)", + agentType, modelWithPrefix, model) + } + } +} + +func TestMissingAPIKey(t *testing.T) { + cfg := &config.Config{ + MiniMaxServerURL: "https://api.minimax.io/v1", + } + + providerConfig, err := DefaultProviderConfig() + if err != nil { + t.Fatalf("Failed to create provider config: %v", err) + } + + _, err = New(cfg, provider.DefaultProviderNameMiniMax, providerConfig) + if err == nil { + t.Fatal("Expected error when API key is missing") + } +} + +func TestGetUsage(t *testing.T) { + cfg := &config.Config{ + MiniMaxAPIKey: "test-key", + MiniMaxServerURL: "https://api.minimax.io/v1", + } + + providerConfig, err := DefaultProviderConfig() + if err != nil { + t.Fatalf("Failed to create provider config: %v", err) + } + + prov, err := New(cfg, provider.DefaultProviderNameMiniMax, providerConfig) + if err != nil { + t.Fatalf("Failed to create provider: %v", err) + } + + usage := prov.GetUsage(map[string]any{ + "PromptTokens": 100, + "CompletionTokens": 50, + }) + if usage.Input != 100 || usage.Output != 50 { + t.Errorf("Expected usage input=100 output=50, got input=%d output=%d", usage.Input, usage.Output) + } +} diff --git a/backend/pkg/providers/minimax/models.yml b/backend/pkg/providers/minimax/models.yml new file mode 100644 index 000000000..5ede908e0 --- /dev/null +++ b/backend/pkg/providers/minimax/models.yml @@ -0,0 +1,20 @@ +- name: MiniMax-M3 + description: MiniMax-M3 - Latest flagship model with 512K context window, up to 128K output tokens, and image input support. Suitable for general dialogue, code generation, complex reasoning, and tool calling. + thinking: false + price: + input: 0.60 + output: 2.40 + +- name: MiniMax-M2.7 + description: MiniMax-M2.7 - Previous flagship model with enhanced reasoning and coding capabilities. Supports tool calling, JSON output, and streaming. + thinking: false + price: + input: 0.40 + output: 1.10 + +- name: MiniMax-M2.7-highspeed + description: MiniMax-M2.7-highspeed - High-speed version of M2.7 for low-latency scenarios. Same capabilities as M2.7 with improved response latency. + thinking: false + price: + input: 0.40 + output: 1.10 diff --git a/backend/pkg/providers/provider/provider.go b/backend/pkg/providers/provider/provider.go index ecff39e42..b80b4aef3 100644 --- a/backend/pkg/providers/provider/provider.go +++ b/backend/pkg/providers/provider/provider.go @@ -30,6 +30,7 @@ const ( ProviderGLM ProviderType = "glm" ProviderKimi ProviderType = "kimi" ProviderQwen ProviderType = "qwen" + ProviderMiniMax ProviderType = "minimax" ) type ProviderName string @@ -49,6 +50,7 @@ const ( DefaultProviderNameGLM ProviderName = ProviderName(ProviderGLM) DefaultProviderNameKimi ProviderName = ProviderName(ProviderKimi) DefaultProviderNameQwen ProviderName = ProviderName(ProviderQwen) + DefaultProviderNameMiniMax ProviderName = ProviderName(ProviderMiniMax) ) type Provider interface { diff --git a/backend/pkg/providers/providers.go b/backend/pkg/providers/providers.go index 1c243f7ba..c6b98d51a 100644 --- a/backend/pkg/providers/providers.go +++ b/backend/pkg/providers/providers.go @@ -27,6 +27,7 @@ import ( "pentagi/pkg/providers/gemini" "pentagi/pkg/providers/glm" "pentagi/pkg/providers/kimi" + "pentagi/pkg/providers/minimax" "pentagi/pkg/providers/ollama" "pentagi/pkg/providers/openai" "pentagi/pkg/providers/pconfig" @@ -235,6 +236,12 @@ func NewProviderController( defaultConfigs[provider.ProviderQwen] = config } + if config, err := minimax.DefaultProviderConfig(); err != nil { + return nil, fmt.Errorf("failed to create minimax provider config: %w", err) + } else { + defaultConfigs[provider.ProviderMiniMax] = config + } + if cfg.OpenAIKey != "" { p, err := openai.New(cfg, provider.DefaultProviderNameOpenAI, defaultConfigs[provider.ProviderOpenAI]) if err != nil { @@ -328,6 +335,15 @@ func NewProviderController( providers[provider.DefaultProviderNameQwen] = p } + if cfg.MiniMaxAPIKey != "" { + p, err := minimax.New(cfg, provider.DefaultProviderNameMiniMax, defaultConfigs[provider.ProviderMiniMax]) + if err != nil { + return nil, fmt.Errorf("failed to create minimax provider: %w", err) + } + + providers[provider.DefaultProviderNameMiniMax] = p + } + summarizerAgent := csum.NewSummarizer(csum.SummarizerConfig{ PreserveLast: cfg.SummarizerPreserveLast, UseQA: cfg.SummarizerUseQA, @@ -734,6 +750,8 @@ func (pc *providerController) GetProvider( return pc.Providers.Get(provider.DefaultProviderNameKimi) case provider.DefaultProviderNameQwen: return pc.Providers.Get(provider.DefaultProviderNameQwen) + case provider.DefaultProviderNameMiniMax: + return pc.Providers.Get(provider.DefaultProviderNameMiniMax) } return nil, fmt.Errorf("provider '%s' not found", prvname) @@ -840,6 +858,12 @@ func (pc *providerController) NewProvider(prv database.Provider) (provider.Provi return nil, fmt.Errorf("failed to build qwen provider config: %w", err) } return qwen.New(pc.cfg, providerName, qwenConfig) + case provider.ProviderMiniMax: + minimaxConfig, err := minimax.BuildProviderConfig(prv.Config) + if err != nil { + return nil, fmt.Errorf("failed to build minimax provider config: %w", err) + } + return minimax.New(pc.cfg, providerName, minimaxConfig) default: return nil, fmt.Errorf("unknown provider type: %s", prv.Type) } @@ -1178,6 +1202,8 @@ func (pc *providerController) buildProviderFromConfig( return kimi.New(pc.cfg, prvname, config) case provider.ProviderQwen: return qwen.New(pc.cfg, prvname, config) + case provider.ProviderMiniMax: + return minimax.New(pc.cfg, prvname, config) default: return nil, fmt.Errorf("unknown provider type: %s", prvtype) } diff --git a/backend/pkg/server/models/providers.go b/backend/pkg/server/models/providers.go index e20a46ed8..12483ffe8 100644 --- a/backend/pkg/server/models/providers.go +++ b/backend/pkg/server/models/providers.go @@ -29,7 +29,8 @@ func (s ProviderType) Valid() error { provider.ProviderDeepSeek, provider.ProviderGLM, provider.ProviderKimi, - provider.ProviderQwen: + provider.ProviderQwen, + provider.ProviderMiniMax: return nil default: return fmt.Errorf("invalid ProviderType: %s", s)