diff --git a/README.md b/README.md index ac78a5b8c1..a282b3943d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ English | [中文](README_CN.md) -A proxy server that provides OpenAI/Gemini/Claude/Codex compatible API interfaces for CLI. +A proxy server that provides OpenAI/Gemini/Claude/Codex/MiniMax compatible API interfaces for CLI. It now also supports OpenAI Codex (GPT models) and Claude Code via OAuth. @@ -53,7 +53,8 @@ Get 10% OFF GLM CODING PLAN:https://z.ai/subscribe?ic=8JVLJQFSKB - Qwen Code multi-account load balancing - iFlow multi-account load balancing - OpenAI Codex multi-account load balancing -- OpenAI-compatible upstream providers via config (e.g., OpenRouter) +- MiniMax support via OpenAI-compatible configuration (with native `reasoning_split` thinking) +- OpenAI-compatible upstream providers via config (e.g., OpenRouter, MiniMax) - Reusable Go SDK for embedding the proxy (see `docs/sdk-usage.md`) ## Getting Started diff --git a/README_CN.md b/README_CN.md index 7ee7db43e5..263de1d5d9 100644 --- a/README_CN.md +++ b/README_CN.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -一个为 CLI 提供 OpenAI/Gemini/Claude/Codex 兼容 API 接口的代理服务器。 +一个为 CLI 提供 OpenAI/Gemini/Claude/Codex/MiniMax 兼容 API 接口的代理服务器。 现已支持通过 OAuth 登录接入 OpenAI Codex(GPT 系列)和 Claude Code。 @@ -53,7 +53,8 @@ GLM CODING PLAN 是专为AI编码打造的订阅套餐,每月最低仅需20元 - 支持 Qwen Code 多账户轮询 - 支持 iFlow 多账户轮询 - 支持 OpenAI Codex 多账户轮询 -- 通过配置接入上游 OpenAI 兼容提供商(例如 OpenRouter) +- 支持 MiniMax 接入(通过 OpenAI 兼容配置,原生支持 `reasoning_split` 思维模式) +- 通过配置接入上游 OpenAI 兼容提供商(例如 OpenRouter、MiniMax) - 可复用的 Go SDK(见 `docs/sdk-usage_CN.md`) ## 新手入门 diff --git a/config.example.yaml b/config.example.yaml index 3718a07a1e..455653098a 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -211,6 +211,17 @@ nonstream-keepalive-interval: 0 # alias: "claude-opus-4.66" # - name: "kimi-k2.5" # alias: "claude-opus-4.66" +# +# # MiniMax provider (OpenAI-compatible API with reasoning_split thinking support) +# - name: "minimax" +# base-url: "https://api.minimax.io/v1" +# api-key-entries: +# - api-key: "your-minimax-api-key" +# models: +# - name: "MiniMax-M2.7" +# - name: "MiniMax-M2.7-highspeed" +# - name: "MiniMax-M2.5" +# - name: "MiniMax-M2.5-highspeed" # Vertex API keys (Vertex-compatible endpoints, base-url is optional) # vertex-api-key: diff --git a/internal/constant/constant.go b/internal/constant/constant.go index 58b388a138..fce7a35f9a 100644 --- a/internal/constant/constant.go +++ b/internal/constant/constant.go @@ -24,4 +24,7 @@ const ( // Antigravity represents the Antigravity response format identifier. Antigravity = "antigravity" + + // MiniMax represents the MiniMax provider identifier. + MiniMax = "minimax" ) diff --git a/internal/registry/model_definitions.go b/internal/registry/model_definitions.go index 14e2852ea7..284925b188 100644 --- a/internal/registry/model_definitions.go +++ b/internal/registry/model_definitions.go @@ -21,6 +21,7 @@ type staticModelsJSON struct { IFlow []*ModelInfo `json:"iflow"` Kimi []*ModelInfo `json:"kimi"` Antigravity []*ModelInfo `json:"antigravity"` + MiniMax []*ModelInfo `json:"minimax"` } // GetClaudeModels returns the standard Claude model definitions. @@ -88,6 +89,11 @@ func GetAntigravityModels() []*ModelInfo { return cloneModelInfos(getModels().Antigravity) } +// GetMiniMaxModels returns the standard MiniMax model definitions. +func GetMiniMaxModels() []*ModelInfo { + return cloneModelInfos(getModels().MiniMax) +} + // cloneModelInfos returns a shallow copy of the slice with each element deep-cloned. func cloneModelInfos(models []*ModelInfo) []*ModelInfo { if len(models) == 0 { @@ -114,6 +120,7 @@ func cloneModelInfos(models []*ModelInfo) []*ModelInfo { // - iflow // - kimi // - antigravity +// - minimax func GetStaticModelDefinitionsByChannel(channel string) []*ModelInfo { key := strings.ToLower(strings.TrimSpace(channel)) switch key { @@ -137,6 +144,8 @@ func GetStaticModelDefinitionsByChannel(channel string) []*ModelInfo { return GetKimiModels() case "antigravity": return GetAntigravityModels() + case "minimax": + return GetMiniMaxModels() default: return nil } @@ -161,6 +170,7 @@ func LookupStaticModelInfo(modelID string) *ModelInfo { data.IFlow, data.Kimi, data.Antigravity, + data.MiniMax, } for _, models := range allModels { for _, m := range models { diff --git a/internal/registry/models/models.json b/internal/registry/models/models.json index 9a30478801..59d538283d 100644 --- a/internal/registry/models/models.json +++ b/internal/registry/models/models.json @@ -2679,5 +2679,91 @@ "context_length": 114000, "max_completion_tokens": 32768 } + ], + "minimax": [ + { + "id": "MiniMax-M2.7", + "object": "model", + "created": 1742000000, + "owned_by": "minimax", + "type": "minimax", + "display_name": "MiniMax M2.7", + "context_length": 1000000, + "max_completion_tokens": 8192, + "thinking": { + "levels": [ + "none", + "auto", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, + { + "id": "MiniMax-M2.7-highspeed", + "object": "model", + "created": 1742000000, + "owned_by": "minimax", + "type": "minimax", + "display_name": "MiniMax M2.7 Highspeed", + "context_length": 1000000, + "max_completion_tokens": 8192, + "thinking": { + "levels": [ + "none", + "auto", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, + { + "id": "MiniMax-M2.5", + "object": "model", + "created": 1740000000, + "owned_by": "minimax", + "type": "minimax", + "display_name": "MiniMax M2.5", + "context_length": 204000, + "max_completion_tokens": 16384, + "thinking": { + "levels": [ + "none", + "auto", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, + { + "id": "MiniMax-M2.5-highspeed", + "object": "model", + "created": 1740000000, + "owned_by": "minimax", + "type": "minimax", + "display_name": "MiniMax M2.5 Highspeed", + "context_length": 204000, + "max_completion_tokens": 16384, + "thinking": { + "levels": [ + "none", + "auto", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + } ] -} \ No newline at end of file +} diff --git a/internal/runtime/executor/thinking_providers.go b/internal/runtime/executor/thinking_providers.go index b961db9035..4ff784513f 100644 --- a/internal/runtime/executor/thinking_providers.go +++ b/internal/runtime/executor/thinking_providers.go @@ -8,5 +8,6 @@ import ( _ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/geminicli" _ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/iflow" _ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/kimi" + _ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/minimax" _ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/openai" ) diff --git a/internal/thinking/apply.go b/internal/thinking/apply.go index c79ecd8ee1..b5bd3cb1bd 100644 --- a/internal/thinking/apply.go +++ b/internal/thinking/apply.go @@ -19,6 +19,7 @@ var providerAppliers = map[string]ProviderApplier{ "iflow": nil, "antigravity": nil, "kimi": nil, + "minimax": nil, } // GetProviderApplier returns the ProviderApplier for the given provider name. @@ -96,7 +97,17 @@ func ApplyThinking(body []byte, model string, fromFormat string, toFormat string fromFormat = providerFormat } // 1. Route check: Get provider applier + // When the provider key has its own registered thinking applier (e.g., "minimax" + // via openai-compatibility), prefer it over the generic format applier (e.g., "openai"). + // This allows providers with custom thinking formats to work correctly when + // configured through openai-compatibility. applier := GetProviderApplier(providerFormat) + if providerKey != providerFormat { + if keyApplier := GetProviderApplier(providerKey); keyApplier != nil { + applier = keyApplier + providerFormat = providerKey + } + } if applier == nil { log.WithFields(log.Fields{ "provider": providerFormat, @@ -336,6 +347,12 @@ func extractThinkingConfig(body []byte, provider string) ThinkingConfig { case "kimi": // Kimi uses OpenAI-compatible reasoning_effort format return extractOpenAIConfig(body) + case "minimax": + config := extractMiniMaxConfig(body) + if hasThinkingConfig(config) { + return config + } + return extractOpenAIConfig(body) default: return ThinkingConfig{} } @@ -495,6 +512,23 @@ func extractCodexConfig(body []byte) ThinkingConfig { return ThinkingConfig{} } +// extractMiniMaxConfig extracts thinking configuration from MiniMax format request body. +// +// MiniMax API format: +// - reasoning_split: boolean (true to enable, false to disable) +// +// Returns ModeBudget with Budget=1 as a sentinel value indicating "enabled". +// Budget=1 is used because MiniMax models don't use numeric budgets; they only support on/off. +func extractMiniMaxConfig(body []byte) ThinkingConfig { + if split := gjson.GetBytes(body, "reasoning_split"); split.Exists() { + if split.Bool() { + return ThinkingConfig{Mode: ModeBudget, Budget: 1} + } + return ThinkingConfig{Mode: ModeNone, Budget: 0} + } + return ThinkingConfig{} +} + // extractIFlowConfig extracts thinking configuration from iFlow format request body. // // iFlow API format (supports multiple model families): diff --git a/internal/thinking/provider/minimax/apply.go b/internal/thinking/provider/minimax/apply.go new file mode 100644 index 0000000000..a5bbf26fb4 --- /dev/null +++ b/internal/thinking/provider/minimax/apply.go @@ -0,0 +1,91 @@ +// Package minimax implements thinking configuration for MiniMax models. +// +// MiniMax models use a boolean toggle for thinking: +// - reasoning_split: true/false +// +// Level values are converted to boolean: none=false, all others=true +package minimax + +import ( + "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// Applier implements thinking.ProviderApplier for MiniMax models. +// +// MiniMax-specific behavior: +// - Uses reasoning_split boolean toggle +// - Level to boolean: none=false, others=true +// - No quantized support (only on/off) +type Applier struct{} + +var _ thinking.ProviderApplier = (*Applier)(nil) + +// NewApplier creates a new MiniMax thinking applier. +func NewApplier() *Applier { + return &Applier{} +} + +func init() { + thinking.RegisterProvider("minimax", NewApplier()) +} + +// Apply applies thinking configuration to MiniMax request body. +// +// Expected output format: +// +// { +// "reasoning_split": true/false +// } +func (a *Applier) Apply(body []byte, config thinking.ThinkingConfig, modelInfo *registry.ModelInfo) ([]byte, error) { + if !thinking.IsUserDefinedModel(modelInfo) && modelInfo.Thinking == nil { + return body, nil + } + return applyMiniMax(body, config), nil +} + +// configToBoolean converts ThinkingConfig to boolean for MiniMax models. +// +// Conversion rules: +// - ModeNone: false +// - ModeAuto: true +// - ModeBudget + Budget=0: false +// - ModeBudget + Budget>0: true +// - ModeLevel + Level="none": false +// - ModeLevel + any other level: true +// - Default (unknown mode): true +func configToBoolean(config thinking.ThinkingConfig) bool { + switch config.Mode { + case thinking.ModeNone: + return false + case thinking.ModeAuto: + return true + case thinking.ModeBudget: + return config.Budget > 0 + case thinking.ModeLevel: + return config.Level != thinking.LevelNone + default: + return true + } +} + +// applyMiniMax applies thinking configuration for MiniMax models. +// +// Output format: +// +// {"reasoning_split": true/false} +func applyMiniMax(body []byte, config thinking.ThinkingConfig) []byte { + reasoningSplit := configToBoolean(config) + + if len(body) == 0 || !gjson.ValidBytes(body) { + body = []byte(`{}`) + } + + // Remove any OpenAI-style reasoning_effort that may have been set + body, _ = sjson.DeleteBytes(body, "reasoning_effort") + body, _ = sjson.SetBytes(body, "reasoning_split", reasoningSplit) + + return body +} diff --git a/internal/thinking/strip.go b/internal/thinking/strip.go index 85498c010c..c345915c33 100644 --- a/internal/thinking/strip.go +++ b/internal/thinking/strip.go @@ -51,6 +51,11 @@ func StripThinkingConfig(body []byte, provider string) []byte { "reasoning_split", "reasoning_effort", } + case "minimax": + paths = []string{ + "reasoning_split", + "reasoning_effort", + } default: return body }