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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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。

Expand Down Expand Up @@ -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`)

## 新手入门
Expand Down
11 changes: 11 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions internal/constant/constant.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ const (

// Antigravity represents the Antigravity response format identifier.
Antigravity = "antigravity"

// MiniMax represents the MiniMax provider identifier.
MiniMax = "minimax"
)
10 changes: 10 additions & 0 deletions internal/registry/model_definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -137,6 +144,8 @@ func GetStaticModelDefinitionsByChannel(channel string) []*ModelInfo {
return GetKimiModels()
case "antigravity":
return GetAntigravityModels()
case "minimax":
return GetMiniMaxModels()
default:
return nil
}
Expand All @@ -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 {
Expand Down
88 changes: 87 additions & 1 deletion internal/registry/models/models.json
Original file line number Diff line number Diff line change
Expand Up @@ -2679,5 +2679,91 @@
"context_length": 114000,
"max_completion_tokens": 32768
}
],
"minimax": [

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Register MiniMax in static model registry

Adding a minimax section in models.json is not sufficient because internal/registry/model_definitions.go still does not define or traverse a minimax field in staticModelsJSON, GetStaticModelDefinitionsByChannel, or LookupStaticModelInfo; the new entries are therefore dropped on unmarshal and never found by lookup. In practice, LookupModelInfo(model, "minimax") returns nil for these models, so MiniMax requests are treated as user-defined and bypass the capability metadata/validation this commit is trying to introduce.

Useful? React with 👍 / 👎.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch! Fixed in 3e4482c — added MiniMax field to staticModelsJSON, GetMiniMaxModels() getter, case "minimax" in GetStaticModelDefinitionsByChannel, and data.MiniMax to LookupStaticModelInfo.

{
"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"
]
}
}
]
}
}
1 change: 1 addition & 0 deletions internal/runtime/executor/thinking_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
34 changes: 34 additions & 0 deletions internal/thinking/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Comment on lines +105 to +108

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse static MiniMax metadata for compat models

When MiniMax is configured through the new openai-compatibility example, sdk/cliproxy/service.go:955-970 still registers compat.Models as UserDefined: true without copying any Thinking support, and registry.LookupModelInfo prefers that dynamic entry over the static minimax definitions (internal/registry/model_registry.go:173-176). So this new provider-key override still sends MiniMax-M2.7 down the applyUserDefinedModel path, meaning the levels/capabilities added in models.json are never used for the advertised config path and MiniMax requests keep bypassing validation/clamping.

Useful? React with 👍 / 👎.

}
}
if applier == nil {
log.WithFields(log.Fields{
"provider": providerFormat,
Expand Down Expand Up @@ -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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse OpenAI Responses thinking for MiniMax

The MiniMax branch only falls back to extractOpenAIConfig, which reads reasoning_effort, but OpenAI Responses requests use reasoning.effort (the openai-response shape used by /responses/compact in OpenAICompatExecutor.Execute). In that path, extractThinkingConfig returns empty, hasThinkingConfig is false, and the MiniMax applier is never invoked, so reasoning_split is not produced and MiniMax-specific thinking support silently does nothing for responses traffic.

Useful? React with 👍 / 👎.

default:
return ThinkingConfig{}
}
Expand Down Expand Up @@ -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):
Expand Down
91 changes: 91 additions & 0 deletions internal/thinking/provider/minimax/apply.go
Original file line number Diff line number Diff line change
@@ -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)
Comment on lines +86 to +88

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Drop Responses reasoning fields before setting reasoning_split

/responses/compact traffic stays in the openai-response schema, and the translator returns the raw payload when source and target are both openai-response (sdk/translator/registry.go:43-52). For requests that reach this applier via a thinking suffix, an existing reasoning.effort object therefore survives because this code only deletes top-level reasoning_effort. The forwarded MiniMax request can end up carrying both reasoning_split and the stale OpenAI Responses reasoning block, which is a schema mismatch specific to compact/Responses callers.

Useful? React with 👍 / 👎.


return body
}
5 changes: 5 additions & 0 deletions internal/thinking/strip.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down