diff --git a/agent/app/provider/catalog.go b/agent/app/provider/catalog.go index e993b5279..b8be439ed 100644 --- a/agent/app/provider/catalog.go +++ b/agent/app/provider/catalog.go @@ -82,7 +82,6 @@ var catalog = map[string]Meta{ {ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"}, {ID: "glm-5", Name: "GLM-5"}, - {ID: "MiniMax-M2.5", Name: "MiniMax M2.5"}, {ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"}, {ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"}, }, @@ -114,7 +113,7 @@ var catalog = map[string]Meta{ {APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"}, {APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"}, }, - Models: []Model{{ID: "MiniMax-M3", Name: "MiniMax M3"}, {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}}, + Models: []Model{{ID: "MiniMax-M3.1-Flash", Name: "MiniMax M3.1 Flash"}, {ID: "MiniMax-M3", Name: "MiniMax M3"}}, }, "xiaomi": { Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY", diff --git a/agent/app/provider/minimax_test.go b/agent/app/provider/minimax_test.go new file mode 100644 index 000000000..b0efe792f --- /dev/null +++ b/agent/app/provider/minimax_test.go @@ -0,0 +1,100 @@ +package provider + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "testing" +) + +func TestMiniMaxCatalog(t *testing.T) { + meta, ok := Get("minimax") + if !ok || !reflect.DeepEqual(meta.Models, []Model{{ID: "MiniMax-M3.1-Flash", Name: "MiniMax M3.1 Flash"}, {ID: "MiniMax-M3", Name: "MiniMax M3"}}) { + t.Fatalf("unexpected MiniMax models: %+v", meta.Models) + } + if meta.DefaultAPIType != "anthropic-messages" { + t.Fatalf("changed default protocol: %s", meta.DefaultAPIType) + } + for _, provider := range All() { + for _, model := range provider.Models { + if strings.Contains(strings.ToLower(model.ID), "minimax") && model.ID != "MiniMax-M3.1-Flash" && model.ID != "MiniMax-M3" { + t.Errorf("legacy suggestion in %s: %s", provider.Key, model.ID) + } + } + } +} + +func TestMiniMaxVerificationRequests(t *testing.T) { + for _, region := range []string{"https://api.minimaxi.com", "https://api.minimax.io"} { + for _, protocol := range []string{"anthropic-messages", "openai-completions"} { + for _, auth := range []string{AuthModeXAPIKey, AuthModeBearer} { + for _, model := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed", "custom-model"} { + t.Run(region+"/"+protocol+"/"+auth+"/"+model, func(t *testing.T) { + base, endpoint := region+"/v1", "/chat/completions" + if protocol == "anthropic-messages" { + base, endpoint = region+"/anthropic", "/v1/messages" + } + built := BuildVerifyRequest("minimax", protocol, auth, base, "test-key", model) + if built.URL != base+endpoint { + t.Fatalf("URL = %s", built.URL) + } + var captured map[string]interface{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost || r.URL.Path != strings.TrimPrefix(base, region)+endpoint { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } + if protocol == "anthropic-messages" && auth == AuthModeXAPIKey { + if r.Header.Get("x-api-key") != "test-key" || r.Header.Get("Authorization") != "" { + t.Error("incorrect API key authentication") + } + } else if r.Header.Get("Authorization") != "Bearer test-key" { + t.Error("incorrect bearer authentication") + } + if err := json.NewDecoder(r.Body).Decode(&captured); err != nil { + t.Error(err) + } + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + if err := VerifyAccount("minimax", protocol, auth, server.URL+strings.TrimPrefix(base, region), "test-key", model); err != nil { + t.Fatal(err) + } + if captured["model"] != model || captured["stream"] != false || captured["max_tokens"] != float64(defaultVerifyMaxTokens) { + t.Fatalf("unexpected serialized body: %#v", captured) + } + if model == "MiniMax-M3.1-Flash" { + if protocol == "anthropic-messages" { + if !reflect.DeepEqual(captured["thinking"], map[string]interface{}{"type": "adaptive"}) || !reflect.DeepEqual(captured["output_config"], map[string]interface{}{"effort": "max"}) || captured["reasoning_effort"] != nil { + t.Fatalf("invalid Anthropic Flash reasoning: %#v", captured) + } + } else if captured["reasoning_effort"] != "max" || captured["thinking"] != nil || captured["output_config"] != nil { + t.Fatalf("invalid OpenAI Flash reasoning: %#v", captured) + } + } else if captured["reasoning_effort"] != nil || captured["thinking"] != nil || captured["output_config"] != nil { + t.Fatalf("changed legacy/custom reasoning: %#v", captured) + } + }) + } + } + } + } +} + +func TestMiniMaxExplicitGatewayAndCustomModels(t *testing.T) { + for _, provider := range []string{"bailian-coding-plan", "custom", "openrouter"} { + for _, protocol := range []string{"openai-completions", "anthropic-messages"} { + for _, model := range []string{"MiniMax-M2.5", "MiniMax-M3.1-Flash", "my-custom-model"} { + request := BuildVerifyRequest(provider, protocol, AuthModeBearer, "https://example.test/v1", "test-key", model) + var body map[string]interface{} + if err := json.Unmarshal(request.Body, &body); err != nil { + t.Fatal(err) + } + if body["model"] != model || body["reasoning_effort"] != nil || body["output_config"] != nil || body["thinking"] != nil { + t.Fatalf("changed explicit %s request: %#v", provider, body) + } + } + } + } +} diff --git a/agent/app/provider/verify.go b/agent/app/provider/verify.go index f7da24811..810450b44 100644 --- a/agent/app/provider/verify.go +++ b/agent/app/provider/verify.go @@ -118,10 +118,15 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri headers["x-api-key"] = apiKey } headers["anthropic-version"] = "2023-06-01" - request.Body = mustJSON(map[string]interface{}{ + body := map[string]interface{}{ "model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false, "messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}}, - }) + } + if provider == "minimax" && model == "MiniMax-M3.1-Flash" { + body["thinking"] = map[string]string{"type": "adaptive"} + body["output_config"] = map[string]string{"effort": "max"} + } + request.Body = mustJSON(body) case "openai-responses": request.URL = baseURL + "/responses" headers["Authorization"] = "Bearer " + apiKey @@ -131,9 +136,13 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" { headers["Authorization"] = "Bearer " + apiKey } - request.Body = mustJSON(map[string]interface{}{ + body := map[string]interface{}{ "model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false, - }) + } + if provider == "minimax" && model == "MiniMax-M3.1-Flash" { + body["reasoning_effort"] = "max" + } + request.Body = mustJSON(body) } return request } diff --git a/agent/app/service/agents_minimax_test.go b/agent/app/service/agents_minimax_test.go new file mode 100644 index 000000000..8a0e66a93 --- /dev/null +++ b/agent/app/service/agents_minimax_test.go @@ -0,0 +1,115 @@ +package service + +import ( + "encoding/json" + "path/filepath" + "reflect" + "testing" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider" +) + +func TestMiniMaxOpenclawModelMetadata(t *testing.T) { + for _, provider := range []string{"minimax", "custom", "bailian-coding-plan"} { + for _, protocol := range []string{"anthropic-messages", "openai-completions"} { + for _, id := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5", "saved-custom"} { + t.Run(provider+"/"+protocol+"/"+id, func(t *testing.T) { + account := &model.AgentAccount{Provider: provider, APIType: protocol, BaseURL: "https://example.test/v1", APIKey: "test-key"} + _, entry, _, _, err := buildOpenclawAccountModelConfig(account, dto.AgentAccountModel{ID: id, Name: "Saved name"}) + if err != nil { + t.Fatal(err) + } + payload, err := json.Marshal(entry) + if err != nil { + t.Fatal(err) + } + var actual modelEntry + if err := json.Unmarshal(payload, &actual); err != nil { + t.Fatal(err) + } + if actual.ID != id || actual.Name != "Saved name" { + t.Fatalf("changed saved identity: %s", payload) + } + if provider == "minimax" && id == "MiniMax-M3.1-Flash" { + if actual.ContextWindow != 1000000 || actual.MaxTokens != 524288 || !reflect.DeepEqual(actual.Input, []string{"text", "image"}) { + t.Fatalf("incorrect direct Flash metadata: %s", payload) + } + } else if actual.ContextWindow != 0 || actual.MaxTokens != 0 { + t.Fatalf("changed existing/gateway limits: %s", payload) + } + }) + } + } + } +} + +func TestMiniMaxPreservesSavedOpenclawMetadata(t *testing.T) { + current := &modelsConfig{Mode: "merge", Providers: map[string]modelProvider{ + "minimax": {Models: []modelEntry{{ID: "MiniMax-M3.1-Flash", Input: []string{"text"}, ContextWindow: 32000, MaxTokens: 4096}}}, + }} + conf := map[string]interface{}{} + if err := applyOpenclawModelsConfig(conf, current); err != nil { + t.Fatal(err) + } + next := &modelsConfig{Mode: "merge", Providers: map[string]modelProvider{ + "minimax": {Models: []modelEntry{{ID: "MiniMax-M3.1-Flash", Input: []string{"text", "image"}, ContextWindow: 1000000, MaxTokens: 524288}}}, + }} + preserveOpenclawModelMetadata(conf, next) + if !reflect.DeepEqual(next.Providers["minimax"].Models, current.Providers["minimax"].Models) { + t.Fatalf("saved metadata changed: %+v", next) + } +} + +func TestMiniMaxDefaultAccountRequest(t *testing.T) { + for _, protocol := range []string{"anthropic-messages", "openai-completions"} { + account := &model.AgentAccount{Provider: "minimax", APIType: protocol} + models, err := buildInitialAgentAccountModels(account, nil) + if err != nil { + t.Fatal(err) + } + selected, err := resolveAgentAccountVerifyModel("minimax", "", models) + if err != nil || selected != "MiniMax-M3.1-Flash" || len(models) != 2 || models[1].ID != "MiniMax-M3" { + t.Fatalf("unexpected default: %s, %+v, %v", selected, models, err) + } + request := providercatalog.BuildVerifyRequest("minimax", protocol, "", "https://example.test", "test-key", selected) + var body map[string]interface{} + if err := json.Unmarshal(request.Body, &body); err != nil || body["model"] != selected { + t.Fatalf("default request: %s, %v", request.Body, err) + } + } +} + +func TestMiniMaxHermesConfiguration(t *testing.T) { + for _, protocol := range []string{"anthropic-messages", "openai-completions"} { + for _, id := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "my-custom-model"} { + dir := t.TempDir() + account := &model.AgentAccount{Provider: "minimax", APIType: protocol, BaseURL: "https://api.minimaxi.com/anthropic", APIKey: "test-key"} + if err := writeHermesConfig(dir, account, id, ""); err != nil { + t.Fatal(err) + } + conf, err := readHermesConfig(filepath.Join(dir, "config.yaml")) + if err != nil { + t.Fatal(err) + } + if conf.Model.Default != id || conf.Model.Provider != "minimax-cn" || conf.Model.BaseURL != account.BaseURL { + t.Fatalf("changed Hermes selection: %+v", conf.Model) + } + } + } +} + +func TestMiniMaxExplicitAccountModels(t *testing.T) { + for _, provider := range []string{"minimax", "bailian-coding-plan", "custom"} { + account := &model.AgentAccount{Provider: provider, APIType: "openai-completions"} + requested := []dto.AgentAccountModel{{ID: "MiniMax-M2.5", Name: "Saved legacy"}, {ID: "my-custom-model", Name: "Saved custom"}} + actual, err := buildInitialAgentAccountModels(account, requested) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(actual, requested) { + t.Fatalf("changed explicit %s models: %+v", provider, actual) + } + } +} diff --git a/agent/app/service/agents_utils.go b/agent/app/service/agents_utils.go index 8fea839fa..1386f91fe 100644 --- a/agent/app/service/agents_utils.go +++ b/agent/app/service/agents_utils.go @@ -1263,7 +1263,12 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen if err != nil { return "", modelEntry{}, "", modelProvider{}, err } - return providerPatch.PrimaryModel, buildOpenclawModelEntry(providerPatch.ModelID, model), providerPatch.ProviderKey, modelProvider{ + entry := buildOpenclawModelEntry(providerPatch.ModelID, model) + if account.Provider == "minimax" && providerPatch.ModelID == "MiniMax-M3.1-Flash" { + entry.ContextWindow = 1000000 + entry.MaxTokens = 524288 + } + return providerPatch.PrimaryModel, entry, providerPatch.ProviderKey, modelProvider{ ApiKey: providerPatch.APIKey, BaseUrl: providerPatch.BaseURL, Api: providerPatch.APIType,