Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 57 additions & 34 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,18 @@ 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 展示顺序(第一个为默认)。
// "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"
Expand All @@ -85,32 +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": {
// 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,
},
}

Expand Down Expand Up @@ -146,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,
},
}
Expand All @@ -164,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
Expand Down
66 changes: 55 additions & 11 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +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},
{"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")
Expand All @@ -28,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)
}
}
}
Expand All @@ -42,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)
}
}