From b34c7c9b162adf5e8e301bcbf75c3c2ec248af9b Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Thu, 30 Jul 2026 04:59:06 +0000 Subject: [PATCH 1/2] feat(config): add MiniMax as a native model provider --- config/config.go | 15 ++++++++++++--- config/config_test.go | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/config/config.go b/config/config.go index a39687f..761fb2c 100644 --- a/config/config.go +++ b/config/config.go @@ -67,7 +67,7 @@ type modelT struct { // ProviderOptions 是配置时可选的模型供应商,顺序即 UI 展示顺序(第一个为默认)。 // "custom" 为「其它」自定义:flash/pro 各自填 base_url/model/api_key/max_tokens/context_window, // 全部兼容 OpenAI 接口。预设供应商(deepseek/mimo)只需填 api_key,套用 modelConfig 默认。 -var ProviderOptions = []string{"deepseek", "mimo", "kimi", "qwen", "custom"} +var ProviderOptions = []string{"deepseek", "mimo", "kimi", "qwen", "minimax", "custom"} // ProviderCustom 是「其它」自定义供应商的 id。 const ProviderCustom = "custom" @@ -102,16 +102,25 @@ var modelConfig = map[string]modelT{ URL: "https://api.moonshot.cn/v1", // 必须带 /v1,端点为 /v1/chat/completions FlashModel: "kimi-k2.5", ProModel: "kimi-k2.6", - MaxTokens: 0, // 0 = 不发 max_tokens,走模型默认输出上限(见 agent.chatRequest omitempty) + MaxTokens: 0, // 0 = 不发 max_tokens,走模型默认输出上限(见 agent.chatRequest omitempty) ContextWindow: 262144, // 256K }, "qwen": { URL: "https://dashscope.aliyuncs.com/compatible-mode/v1", // 阿里云北京;端点 /v1/chat/completions FlashModel: "qwen3.7-plus", ProModel: "qwen3.7-max", - MaxTokens: 0, // 0 = 不发 max_tokens,走模型默认输出上限 + MaxTokens: 0, // 0 = 不发 max_tokens,走模型默认输出上限 ContextWindow: 1_048_576, // 1M }, + "minimax": { + // 全球站 OpenAI 兼容端点,端点为 /v1/chat/completions(agent 追加 /chat/completions)。 + // 中国站为 https://api.minimaxi.com/v1,高级用户可用 custom 供应商或直接改 model.yaml 切换。 + URL: "https://api.minimax.io/v1", + FlashModel: "MiniMax-M2.7", // 入门:上下文 204800 + ProModel: "MiniMax-M3", // 旗舰:上下文 1M,与下方 ContextWindow 对齐 + MaxTokens: 0, // 0 = 不发 max_tokens,走模型默认输出上限 + ContextWindow: 1_000_000, // 1M(MiniMax-M3) + }, } // Path 返回 ~/.deepx/model.yaml 绝对路径。 diff --git a/config/config_test.go b/config/config_test.go index aa1daa6..10bae92 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -15,6 +15,7 @@ func TestDefaultForProviders(t *testing.T) { }{ {"deepseek", "https://api.deepseek.com", "deepseek-v4-flash", "deepseek-v4-pro", 393216, 1_048_576}, {"mimo", "https://api.xiaomimimo.com/v1", "mimo-v2.5", "mimo-v2.5-pro", 131072, 1_048_576}, + {"minimax", "https://api.minimax.io/v1", "MiniMax-M2.7", "MiniMax-M3", 0, 1_000_000}, {"unknown-provider", "https://api.deepseek.com", "deepseek-v4-flash", "deepseek-v4-pro", 393216, 1_048_576}, // 回退 deepseek } for _, c := range cases { From d64da1246b7ff777c16cdc020c09776d8382ba52 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:20:38 +0800 Subject: [PATCH 2/2] fix(config): preserve MiniMax model limits --- config/config.go | 94 +++++++++++++++++++++++++------------------ config/config_test.go | 67 ++++++++++++++++++++++++------ 2 files changed, 109 insertions(+), 52 deletions(-) diff --git a/config/config.go b/config/config.go index 761fb2c..8085772 100644 --- a/config/config.go +++ b/config/config.go @@ -57,11 +57,12 @@ const ( ) type modelT struct { - URL string - FlashModel string - ProModel string - MaxTokens int // 该供应商默认的单次 completion 上限 - ContextWindow int // 该供应商默认的上下文窗口大小 + URL string + FlashModel string + ProModel string + MaxTokens int // Default completion limit for this provider. + FlashContextWindow int + ProContextWindow int } // ProviderOptions 是配置时可选的模型供应商,顺序即 UI 展示顺序(第一个为默认)。 @@ -85,41 +86,46 @@ const ( // ".../chat/completions/chat/completions" 而请求失败。 var modelConfig = map[string]modelT{ "deepseek": { - URL: defaultBaseURL, // https://api.deepseek.com - FlashModel: defaultFlashModel, - ProModel: defaultProModel, - MaxTokens: 393216, - ContextWindow: 1_048_576, // 1M + URL: defaultBaseURL, + FlashModel: defaultFlashModel, + ProModel: defaultProModel, + MaxTokens: 393216, + FlashContextWindow: 1_048_576, // 1M + ProContextWindow: 1_048_576, // 1M }, "mimo": { - URL: "https://api.xiaomimimo.com/v1", - FlashModel: "mimo-v2.5", - ProModel: "mimo-v2.5-pro", - MaxTokens: 131072, // mimo 单次 completion 上限 - ContextWindow: 1_048_576, // 1M + URL: "https://api.xiaomimimo.com/v1", + FlashModel: "mimo-v2.5", + ProModel: "mimo-v2.5-pro", + MaxTokens: 131072, + FlashContextWindow: 1_048_576, // 1M + ProContextWindow: 1_048_576, // 1M }, "kimi": { - URL: "https://api.moonshot.cn/v1", // 必须带 /v1,端点为 /v1/chat/completions - FlashModel: "kimi-k2.5", - ProModel: "kimi-k2.6", - MaxTokens: 0, // 0 = 不发 max_tokens,走模型默认输出上限(见 agent.chatRequest omitempty) - ContextWindow: 262144, // 256K + URL: "https://api.moonshot.cn/v1", + FlashModel: "kimi-k2.5", + ProModel: "kimi-k2.6", + MaxTokens: 0, + FlashContextWindow: 262144, // 256K + ProContextWindow: 262144, // 256K }, "qwen": { - URL: "https://dashscope.aliyuncs.com/compatible-mode/v1", // 阿里云北京;端点 /v1/chat/completions - FlashModel: "qwen3.7-plus", - ProModel: "qwen3.7-max", - MaxTokens: 0, // 0 = 不发 max_tokens,走模型默认输出上限 - ContextWindow: 1_048_576, // 1M + URL: "https://dashscope.aliyuncs.com/compatible-mode/v1", + FlashModel: "qwen3.7-plus", + ProModel: "qwen3.7-max", + MaxTokens: 0, + FlashContextWindow: 1_048_576, // 1M + ProContextWindow: 1_048_576, // 1M }, "minimax": { - // 全球站 OpenAI 兼容端点,端点为 /v1/chat/completions(agent 追加 /chat/completions)。 - // 中国站为 https://api.minimaxi.com/v1,高级用户可用 custom 供应商或直接改 model.yaml 切换。 - URL: "https://api.minimax.io/v1", - FlashModel: "MiniMax-M2.7", // 入门:上下文 204800 - ProModel: "MiniMax-M3", // 旗舰:上下文 1M,与下方 ContextWindow 对齐 - MaxTokens: 0, // 0 = 不发 max_tokens,走模型默认输出上限 - ContextWindow: 1_000_000, // 1M(MiniMax-M3) + // The global endpoint is the native default. The China endpoint is + // https://api.minimaxi.com/v1 and remains available through custom overrides. + URL: "https://api.minimax.io/v1", + FlashModel: "MiniMax-M2.7", + ProModel: "MiniMax-M3", + MaxTokens: 0, + FlashContextWindow: 204_800, + ProContextWindow: 1_000_000, }, } @@ -155,14 +161,14 @@ func DefaultFor(provider, apiKey string) *Config { BaseURL: mc.URL, Model: mc.FlashModel, APIKey: apiKey, - ContextWindow: mc.ContextWindow, + ContextWindow: mc.FlashContextWindow, MaxTokens: mc.MaxTokens, }, Pro: ModelEntry{ BaseURL: mc.URL, Model: mc.ProModel, APIKey: apiKey, - ContextWindow: mc.ContextWindow, + ContextWindow: mc.ProContextWindow, MaxTokens: mc.MaxTokens, }, } @@ -173,21 +179,29 @@ func Default(apiKey string) *Config { return DefaultFor(defaultProvider, apiKey) } -// defaultContextWindow 根据模型名推断上下文窗口,给旧 yaml(没写 context_window)兜底用。 -// 已知供应商(deepseek / mimo)默认 1M tokens,其它未知模型保守取 64K。 +// defaultContextWindow infers a context window for legacy YAML without one. func defaultContextWindow(model string) int { m := strings.ToLower(model) + if strings.Contains(m, "minimax-m3") { + return 1_000_000 + } + if strings.Contains(m, "minimax-m2.7") { + return 204_800 + } if strings.Contains(m, "deepseek") || strings.Contains(m, "mimo") { return 1_048_576 } return 65_536 } -// defaultMaxTokens 根据模型名推断单次 completion 上限。含 deepseek 的模型沿用既有 384K; -// 其它模型保守取 131072(mimo 等的上限),避免超过模型实际允许值被拒。 -// 给旧 model.yaml(没写 max_tokens)兜底用。 +// defaultMaxTokens infers a completion limit for legacy YAML. MiniMax keeps +// zero so requests use the model's own output limit. func defaultMaxTokens(model string) int { - if strings.Contains(strings.ToLower(model), "deepseek") { + m := strings.ToLower(model) + if strings.Contains(m, "minimax") { + return 0 + } + if strings.Contains(m, "deepseek") { return 393216 } return 131072 diff --git a/config/config_test.go b/config/config_test.go index 10bae92..e48c3fd 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -6,17 +6,18 @@ import "testing" // 关键回归:base_url 只到域名(agent 再追加 /chat/completions),mimo 的 max_tokens=131072。 func TestDefaultForProviders(t *testing.T) { cases := []struct { - provider string - wantBaseURL string - wantFlash string - wantPro string - wantMaxTok int - wantCtxWin int + provider string + wantBaseURL string + wantFlash string + wantPro string + wantMaxTok int + wantFlashCtx int + wantProCtx int }{ - {"deepseek", "https://api.deepseek.com", "deepseek-v4-flash", "deepseek-v4-pro", 393216, 1_048_576}, - {"mimo", "https://api.xiaomimimo.com/v1", "mimo-v2.5", "mimo-v2.5-pro", 131072, 1_048_576}, - {"minimax", "https://api.minimax.io/v1", "MiniMax-M2.7", "MiniMax-M3", 0, 1_000_000}, - {"unknown-provider", "https://api.deepseek.com", "deepseek-v4-flash", "deepseek-v4-pro", 393216, 1_048_576}, // 回退 deepseek + {"deepseek", "https://api.deepseek.com", "deepseek-v4-flash", "deepseek-v4-pro", 393216, 1_048_576, 1_048_576}, + {"mimo", "https://api.xiaomimimo.com/v1", "mimo-v2.5", "mimo-v2.5-pro", 131072, 1_048_576, 1_048_576}, + {"minimax", "https://api.minimax.io/v1", "MiniMax-M2.7", "MiniMax-M3", 0, 204_800, 1_000_000}, + {"unknown-provider", "https://api.deepseek.com", "deepseek-v4-flash", "deepseek-v4-pro", 393216, 1_048_576, 1_048_576}, // Falls back to deepseek. } for _, c := range cases { cfg := DefaultFor(c.provider, "sk-test") @@ -29,8 +30,8 @@ func TestDefaultForProviders(t *testing.T) { if cfg.Flash.MaxTokens != c.wantMaxTok || cfg.Pro.MaxTokens != c.wantMaxTok { t.Errorf("%s max_tokens = %d/%d, want %d", c.provider, cfg.Flash.MaxTokens, cfg.Pro.MaxTokens, c.wantMaxTok) } - if cfg.Flash.ContextWindow != c.wantCtxWin || cfg.Pro.ContextWindow != c.wantCtxWin { - t.Errorf("%s context_window = %d/%d, want %d", c.provider, cfg.Flash.ContextWindow, cfg.Pro.ContextWindow, c.wantCtxWin) + if cfg.Flash.ContextWindow != c.wantFlashCtx || cfg.Pro.ContextWindow != c.wantProCtx { + t.Errorf("%s context_window = %d/%d, want %d/%d", c.provider, cfg.Flash.ContextWindow, cfg.Pro.ContextWindow, c.wantFlashCtx, c.wantProCtx) } } } @@ -43,4 +44,46 @@ func TestDefaultMaxTokens(t *testing.T) { if got := defaultMaxTokens("mimo-v2.5"); got != 131072 { t.Errorf("mimo max_tokens = %d, want 131072", got) } + if got := defaultMaxTokens("MiniMax-M3"); got != 0 { + t.Errorf("MiniMax max_tokens = %d, want 0", got) + } +} + +func TestMiniMaxDefaultsSurviveRoundTrip(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + + if err := Save(DefaultFor("minimax", "sk-test")); err != nil { + t.Fatal(err) + } + cfg, err := Load() + if err != nil { + t.Fatal(err) + } + if cfg.Flash.ContextWindow != 204_800 || cfg.Pro.ContextWindow != 1_000_000 { + t.Fatalf("MiniMax context windows = %d/%d, want 204800/1000000", cfg.Flash.ContextWindow, cfg.Pro.ContextWindow) + } + if cfg.Flash.MaxTokens != 0 || cfg.Pro.MaxTokens != 0 { + t.Fatalf("MiniMax max_tokens = %d/%d, want 0/0", cfg.Flash.MaxTokens, cfg.Pro.MaxTokens) + } +} + +func TestProviderOptionsIncludesMiniMax(t *testing.T) { + miniMaxIndex := -1 + customIndex := -1 + for i, provider := range ProviderOptions { + switch provider { + case "minimax": + miniMaxIndex = i + case ProviderCustom: + customIndex = i + } + } + if miniMaxIndex < 0 { + t.Fatal("ProviderOptions does not include minimax") + } + if customIndex < 0 || miniMaxIndex > customIndex { + t.Fatalf("minimax must appear before custom, got %v", ProviderOptions) + } }