feat(minimax): 增加 MiniMax 平台支持

把 MiniMax 做成与 Kimi / 智谱 / DeepSeek 同级的国产供应商平台:账号创建/编辑、分组、Composite 路由、原生 Token Plan / Coding Plan 额度,以及 OpenAI 兼容模型列表。

额度走 GET api.minimaxi.com|io /v1/api/openplatform/coding_plan/remains(Bearer)。只解析 model_name=general 的 remaining percent;周窗口仅在 current_weekly_status=1 时计入。

Related to #6742
This commit is contained in:
刘佳林
2026-09-07 18:22:56 +08:00
parent 909b8c7ef4
commit 19382f275e
101 changed files with 704 additions and 151 deletions
+2 -1
View File
@@ -182,6 +182,7 @@ const (
ProviderKimi Provider = "kimi"
ProviderZhipu Provider = "zhipu"
ProviderDeepseek Provider = "deepseek"
ProviderMinimax Provider = "minimax"
)
func (pr Provider) String() string {
@@ -191,7 +192,7 @@ func (pr Provider) String() string {
// ProviderValidator is a validator for the "provider" field enum values. It is called by the builders before save.
func ProviderValidator(pr Provider) error {
switch pr {
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderGrok, ProviderAntigravity, ProviderKimi, ProviderZhipu, ProviderDeepseek:
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderGrok, ProviderAntigravity, ProviderKimi, ProviderZhipu, ProviderDeepseek, ProviderMinimax:
return nil
default:
return fmt.Errorf("channelmonitor: invalid enum value for provider field: %q", pr)
@@ -108,6 +108,7 @@ const (
ProviderKimi Provider = "kimi"
ProviderZhipu Provider = "zhipu"
ProviderDeepseek Provider = "deepseek"
ProviderMinimax Provider = "minimax"
)
func (pr Provider) String() string {
@@ -117,7 +118,7 @@ func (pr Provider) String() string {
// ProviderValidator is a validator for the "provider" field enum values. It is called by the builders before save.
func ProviderValidator(pr Provider) error {
switch pr {
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderGrok, ProviderAntigravity, ProviderKimi, ProviderZhipu, ProviderDeepseek:
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderGrok, ProviderAntigravity, ProviderKimi, ProviderZhipu, ProviderDeepseek, ProviderMinimax:
return nil
default:
return fmt.Errorf("channelmonitorrequesttemplate: invalid enum value for provider field: %q", pr)
+2 -2
View File
@@ -623,7 +623,7 @@ var (
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "name", Type: field.TypeString, Size: 100},
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini", "grok", "antigravity", "kimi", "zhipu", "deepseek"}},
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini", "grok", "antigravity", "kimi", "zhipu", "deepseek", "minimax"}},
{Name: "check_mode", Type: field.TypeString, Size: 32, Default: "probe"},
{Name: "account_id", Type: field.TypeInt64, Nullable: true},
{Name: "api_mode", Type: field.TypeString, Size: 32, Default: "chat_completions"},
@@ -776,7 +776,7 @@ var (
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "name", Type: field.TypeString, Size: 100},
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini", "grok", "antigravity", "kimi", "zhipu", "deepseek"}},
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini", "grok", "antigravity", "kimi", "zhipu", "deepseek", "minimax"}},
{Name: "api_mode", Type: field.TypeString, Size: 32, Default: "chat_completions"},
{Name: "description", Type: field.TypeString, Nullable: true, Size: 500, Default: ""},
{Name: "extra_headers", Type: field.TypeJSON},
+1 -1
View File
@@ -36,7 +36,7 @@ func (ChannelMonitor) Fields() []ent.Field {
MaxLen(100),
field.Enum("provider").
Values("openai", "anthropic", "gemini", "grok",
"antigravity", "kimi", "zhipu", "deepseek"),
"antigravity", "kimi", "zhipu", "deepseek", "minimax"),
// check_mode: 'probe' | 'quota' | 'quota_probe'
// probe - LLM 探活(默认,原有行为)
// quota - 仅查关联账号的用量/余额(零 LLM 成本;endpoint/api_key 可空)
@@ -40,7 +40,7 @@ func (ChannelMonitorRequestTemplate) Fields() []ent.Field {
MaxLen(100),
field.Enum("provider").
Values("openai", "anthropic", "gemini", "grok",
"antigravity", "kimi", "zhipu", "deepseek"),
"antigravity", "kimi", "zhipu", "deepseek", "minimax"),
field.String("api_mode").
Default("chat_completions").
MaxLen(32).
+1 -1
View File
@@ -42,7 +42,7 @@ func (UserPlatformQuota) Fields() []ent.Field {
// 此处为 ent 构建期约束,需与 service.AllowedQuotaPlatforms 保持同步。
switch s {
case "anthropic", "openai", "gemini", "antigravity", "grok",
"kimi", "zhipu", "deepseek":
"kimi", "zhipu", "deepseek", "minimax":
return nil
default:
return fmt.Errorf("platform %q is not allowed", s)
+4
View File
@@ -2047,6 +2047,10 @@ func setDefaults() {
"api.moonshot.cn",
"open.bigmodel.cn",
"api.minimaxi.com",
"api.minimax.cn",
"api.minimax.io",
"www.minimaxi.com",
"www.minimax.io",
"generativelanguage.googleapis.com",
"cloudcode-pa.googleapis.com",
"*.openai.azure.com",
+2 -1
View File
@@ -27,6 +27,7 @@ const (
PlatformKimi = "kimi" // Kimi (月之暗面 / Moonshot)
PlatformZhipu = "zhipu" // 智谱 GLM (bigmodel)
PlatformDeepseek = "deepseek" // DeepSeek
PlatformMiniMax = "minimax" // MiniMax (M 系列)
PlatformComposite = "composite"
)
@@ -43,7 +44,7 @@ const (
const (
APIProtocolChatCompletions = "chat_completions" // OpenAI Chat Completions(默认)
APIProtocolAnthropic = "anthropic" // 原生 Anthropic /v1/messages(适配 Claude Code)
APIProtocolResponses = "responses" // OpenAI Responses(deepseek / kimi 原生端点,适配 Codex)
APIProtocolResponses = "responses" // OpenAI Responses(deepseek / kimi / minimax 原生端点,适配 Codex)
APIProtocolAdaptive = "adaptive" // 按入站协议优先选择供应商原生端点
)
@@ -643,6 +643,7 @@ var platformToLiteLLMProvider = map[string]string{
service.PlatformKimi: "moonshot",
service.PlatformZhipu: "zhipu",
service.PlatformDeepseek: "deepseek",
service.PlatformMiniMax: "minimax",
}
// SyncPricingModels 返回 LiteLLM 定价目录中指定平台的最新模型列表
@@ -549,7 +549,7 @@ func TestSyncPricingModels_ValidPlatform_EmptyService(t *testing.T) {
svc := service.NewPricingService(nil, nil)
router := setupSyncPricingModelsRouter(svc)
for _, platform := range []string{"anthropic", "openai", "gemini", "antigravity", "grok", "kimi", "zhipu", "deepseek"} {
for _, platform := range []string{"anthropic", "openai", "gemini", "antigravity", "grok", "kimi", "zhipu", "deepseek", "minimax"} {
req := httptest.NewRequest(http.MethodGet, "/channels/pricing/sync-models?platform="+platform, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
@@ -40,7 +40,7 @@ func NewChannelMonitorHandler(monitorService *service.ChannelMonitorService) *Ch
type channelMonitorCreateRequest struct {
Name string `json:"name" binding:"required,max=100"`
Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini grok antigravity kimi zhipu deepseek"`
Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini grok antigravity kimi zhipu deepseek minimax"`
APIMode string `json:"api_mode" binding:"omitempty,oneof=chat_completions responses"`
Endpoint string `json:"endpoint" binding:"omitempty,max=500"`
APIKey string `json:"api_key" binding:"omitempty,max=2000"`
@@ -64,7 +64,7 @@ type channelMonitorCreateRequest struct {
type channelMonitorUpdateRequest struct {
Name *string `json:"name" binding:"omitempty,max=100"`
Provider *string `json:"provider" binding:"omitempty,oneof=openai anthropic gemini grok antigravity kimi zhipu deepseek"`
Provider *string `json:"provider" binding:"omitempty,oneof=openai anthropic gemini grok antigravity kimi zhipu deepseek minimax"`
APIMode *string `json:"api_mode" binding:"omitempty,oneof=chat_completions responses"`
Endpoint *string `json:"endpoint" binding:"omitempty,max=500"`
APIKey *string `json:"api_key" binding:"omitempty,max=2000"`
@@ -184,7 +184,7 @@ func sanitizeUpdateGroupRequestForSimpleMode(req *UpdateGroupRequest) {
type CreateGroupRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek composite"`
Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek minimax composite"`
RateMultiplier float64 `json:"rate_multiplier"`
IsExclusive bool `json:"is_exclusive"`
SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"`
@@ -258,7 +258,7 @@ type CreateGroupRequest struct {
type UpdateGroupRequest struct {
Name string `json:"name"`
Description *string `json:"description"`
Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek composite"`
Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek minimax composite"`
RateMultiplier *float64 `json:"rate_multiplier"`
IsExclusive *bool `json:"is_exclusive"`
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
@@ -332,7 +332,7 @@ type UpdateGroupRequest struct {
type CompositeRouteRequest struct {
PublicModel string `json:"public_model" binding:"required"`
MatchType string `json:"match_type" binding:"omitempty,oneof=exact prefix"`
TargetPlatform string `json:"target_platform" binding:"required,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek"`
TargetPlatform string `json:"target_platform" binding:"required,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek minimax"`
UpstreamModel string `json:"upstream_model"`
Endpoint string `json:"endpoint" binding:"omitempty,oneof=any messages count_tokens responses chat_completions embeddings images gemini"`
Priority int `json:"priority"`
@@ -27,7 +27,7 @@ func bindGroupPlatformJSON(t *testing.T, target any, body string) error {
func TestGroupPlatformBinding_AllowedPlatforms(t *testing.T) {
allowed := []string{
"anthropic", "openai", "gemini", "antigravity", "grok",
"kimi", "zhipu", "deepseek", "composite",
"kimi", "zhipu", "deepseek", "minimax", "composite",
}
for _, platform := range allowed {
t.Run("create_"+platform, func(t *testing.T) {
@@ -72,7 +72,7 @@ func TestGroupPlatformBinding_RejectsInvalidPlatforms(t *testing.T) {
}
func TestCompositeRouteTargetPlatform_AllowsCNProviders(t *testing.T) {
for _, platform := range []string{"kimi", "zhipu", "deepseek"} {
for _, platform := range []string{"kimi", "zhipu", "deepseek", "minimax"} {
var req CompositeRouteRequest
body := fmt.Sprintf(`{"public_model":"m","target_platform":%q}`, platform)
require.NoError(t, bindGroupPlatformJSON(t, &req, body))
@@ -35,6 +35,7 @@ func TestOpenAICompatibleTextTargetAllowsCompositeProviders(t *testing.T) {
{model: "k3", platform: service.PlatformKimi},
{model: "glm-5.2", platform: service.PlatformZhipu},
{model: "deepseek-v3.2", platform: service.PlatformDeepseek},
{model: "MiniMax-M3", platform: service.PlatformMiniMax},
}
for _, path := range []string{"/v1/messages", "/v1/chat/completions", "/v1/responses", "/v1/responses/input_tokens", "/v1/messages/count_tokens"} {
for _, provider := range providers {
@@ -56,7 +57,7 @@ func TestResponsesWebSocketCompositePlatformGuardKeepsOpenAIAndGrokOnly(t *testi
require.True(t, isResponsesWebSocketCompositePlatform(service.PlatformOpenAI))
require.True(t, isResponsesWebSocketCompositePlatform(service.PlatformGrok))
for _, platform := range []string{
service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek,
service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax,
service.PlatformAnthropic, service.PlatformGemini,
} {
require.False(t, isResponsesWebSocketCompositePlatform(platform), "platform=%s", platform)
+3 -1
View File
@@ -1488,6 +1488,8 @@ func defaultCodexModelIDsForPlatform(platform string) []string {
switch platform {
case service.PlatformDeepseek:
return []string{"deepseek-v4-pro", "deepseek-v4-flash"}
case service.PlatformMiniMax:
return []string{"MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5"}
default:
return defaultModelIDsForPlatform(platform)
}
@@ -1517,7 +1519,7 @@ func defaultModelIDsForPlatform(platform string) []string {
case service.PlatformComposite:
ids := make([]string, 0)
seen := make(map[string]struct{})
for _, concretePlatform := range []string{service.PlatformAnthropic, service.PlatformGemini, service.PlatformOpenAI, service.PlatformAntigravity, service.PlatformGrok, service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek} {
for _, concretePlatform := range []string{service.PlatformAnthropic, service.PlatformGemini, service.PlatformOpenAI, service.PlatformAntigravity, service.PlatformGrok, service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax} {
for _, id := range defaultModelIDsForPlatform(concretePlatform) {
if _, ok := seen[id]; ok {
continue
@@ -801,13 +801,14 @@ func TestDefaultModelIDsForPlatform_CNProvidersKeepClaudeDefaults(t *testing.T)
for _, model := range claude.DefaultModels {
want = append(want, model.ID)
}
for _, platform := range []string{service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek} {
for _, platform := range []string{service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax} {
require.Equal(t, want, defaultModelIDsForPlatform(platform), "platform=%s", platform)
}
}
func TestDefaultCodexModelIDsForPlatform_DeepSeekUsesDeepSeekModels(t *testing.T) {
require.Equal(t, []string{"deepseek-v4-pro", "deepseek-v4-flash"}, defaultCodexModelIDsForPlatform(service.PlatformDeepseek))
require.Equal(t, []string{"MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5"}, defaultCodexModelIDsForPlatform(service.PlatformMiniMax))
require.Equal(t, defaultModelIDsForPlatform(service.PlatformAnthropic), defaultCodexModelIDsForPlatform(service.PlatformAnthropic))
}
@@ -18,7 +18,7 @@ import (
func TestAllowOpenAICompatibleMessagesDispatch_CNProvidersExempt(t *testing.T) {
require.True(t, allowOpenAICompatibleMessagesDispatch(nil, nil), "无 key 保持放行")
for _, platform := range []string{service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformGrok} {
for _, platform := range []string{service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax, service.PlatformGrok} {
apiKey := &service.APIKey{Group: &service.Group{Platform: platform, AllowMessagesDispatch: false}}
require.True(t, allowOpenAICompatibleMessagesDispatch(nil, apiKey),
"%s 分组必须豁免 allow_messages_dispatch 闸门", platform)
@@ -321,7 +321,7 @@ func allowOpenAICompatibleMessagesDispatch(c *gin.Context, apiKey *service.APIKe
func openAICompatibleTextTargetAllowed(c *gin.Context, apiKey *service.APIKey, model string) bool {
return compositeTargetPlatformAllowed(c, apiKey, model,
service.PlatformOpenAI, service.PlatformGrok,
service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek)
service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax)
}
// isResponsesWebSocketCompositePlatform 限定 composite 分组在 Responses WebSocket
@@ -44,6 +44,7 @@ const (
PlatformKimi = domain.PlatformKimi
PlatformZhipu = domain.PlatformZhipu
PlatformDeepseek = domain.PlatformDeepseek
PlatformMiniMax = domain.PlatformMiniMax
)
// AllPlatforms 返回所有支持的平台列表
@@ -57,6 +58,7 @@ func AllPlatforms() []string {
PlatformKimi,
PlatformZhipu,
PlatformDeepseek,
PlatformMiniMax,
}
}
@@ -16,5 +16,6 @@ func TestAllPlatformsIncludesEveryConcretePlatform(t *testing.T) {
"kimi",
"zhipu",
"deepseek",
"minimax",
}, AllPlatforms())
}
@@ -18,7 +18,7 @@ const (
// ollamaCloudUsagePlatformsSQL 是 service.isOllamaCloudUsagePlatform 的 SQL
// 镜像:Ollama Cloud key 允许挂在 openai/anthropic 与国产 OpenAI 兼容平台
// 下复用。所有平台白名单 SQL 只允许引用本常量,不得各处重写字面量,防止漂移。
ollamaCloudUsagePlatformsSQL = "'openai', 'anthropic', 'kimi', 'zhipu', 'deepseek'"
ollamaCloudUsagePlatformsSQL = "'openai', 'anthropic', 'kimi', 'zhipu', 'deepseek', 'minimax'"
ollamaCloudUsageEligibleSQL = `
platform IN (` + ollamaCloudUsagePlatformsSQL + `)
AND type = 'apikey'
@@ -179,7 +179,7 @@ func TestListOllamaCloudUsageGroupAccountsUsesOneStrictBatchQuery(t *testing.T)
require.Empty(t, accounts)
query := normalizeSQLWhitespace(capturedSQL)
require.Contains(t, query, "credentials ->> 'api_key' = ANY($1)")
require.Contains(t, query, "platform IN ('openai', 'anthropic', 'kimi', 'zhipu', 'deepseek')")
require.Contains(t, query, "platform IN ('openai', 'anthropic', 'kimi', 'zhipu', 'deepseek', 'minimax')")
require.Contains(t, query, "jsonb_typeof(credentials -> 'api_key') = 'string'")
require.Contains(t, query, ollamaCloudBaseURLMatchesSQL("credentials ->> 'base_url'"))
require.NotContains(t, query, "~*")
@@ -207,7 +207,7 @@ func TestListDueOllamaCloudUsageAccountsFiltersOrdersAndLimits(t *testing.T) {
for _, clause := range []string{
"deleted_at IS NULL",
"status = 'active'",
"platform IN ('openai', 'anthropic', 'kimi', 'zhipu', 'deepseek')",
"platform IN ('openai', 'anthropic', 'kimi', 'zhipu', 'deepseek', 'minimax')",
"type = 'apikey'",
ollamaCloudBaseURLMatchesSQL("credentials ->> 'base_url'"),
"jsonb_typeof(extra -> 'ollama_cloud_usage_session') = 'string'",
@@ -251,7 +251,7 @@ func TestBulkUpdateOllamaIdentityCleanupIsValueConditional(t *testing.T) {
require.Contains(t, query, "NOT ("+ollamaCloudBaseURLMatchesSQL("credentials ->> 'base_url'"))
require.Contains(t, query, ollamaCloudBaseURLMatchesSQL("$1::jsonb ->> 'base_url'"))
require.NotContains(t, query, "~*")
require.Contains(t, query, "platform IN ('openai', 'anthropic', 'kimi', 'zhipu', 'deepseek') AND type = 'apikey'")
require.Contains(t, query, "platform IN ('openai', 'anthropic', 'kimi', 'zhipu', 'deepseek', 'minimax') AND type = 'apikey'")
require.Contains(t, query, "- 'ollama_cloud_usage_session' - 'ollama_cloud_usage_auto_refresh' - 'ollama_cloud_usage_snapshot'")
payload, ok := exec.execArgs[0][0].([]byte)
require.True(t, ok)
@@ -326,10 +326,10 @@ func TestOllamaCloudUsagePlatformWhitelistMatchesServicePredicate(t *testing.T)
for _, match := range matches {
sqlPlatforms[match[1]] = struct{}{}
}
require.Len(t, sqlPlatforms, 5)
require.Len(t, sqlPlatforms, 6)
for _, platform := range []string{
service.PlatformOpenAI, service.PlatformAnthropic,
service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek,
service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax,
service.PlatformGemini, service.PlatformGrok, service.PlatformAntigravity,
service.PlatformComposite, "kiro",
} {
@@ -373,7 +373,7 @@ func TestUpdateCredentialsPlainCNAPIKeyAccountCleanupStaysSemanticallyEquivalent
require.NoError(t, err)
query := normalizeSQLWhitespace(capturedSQL)
require.Contains(t, query,
"platform IN ('openai', 'anthropic', 'kimi', 'zhipu', 'deepseek') AND type = 'apikey' AND credentials IS DISTINCT FROM $1::jsonb")
"platform IN ('openai', 'anthropic', 'kimi', 'zhipu', 'deepseek', 'minimax') AND type = 'apikey' AND credentials IS DISTINCT FROM $1::jsonb")
require.Contains(t, query,
"THEN COALESCE(extra, '{}'::jsonb) - 'upstream_billing_probe' - 'ollama_cloud_usage_session' - 'ollama_cloud_usage_auto_refresh' - 'ollama_cloud_usage_snapshot'")
require.NotContains(t, query, "- 'upstream_billing_probe_enabled'")
@@ -33,7 +33,7 @@ func TestProxyUpdateInvalidatesBoundProbeSnapshotsAndEnqueuesOutboxAtomically(t
WithArgs(int64(9)).
WillReturnResult(sqlmock.NewResult(0, 0))
expectProxyUpdateReload(mock, 9, "new.example", "user", "pass")
mock.ExpectQuery(`(?s)UPDATE accounts.*- 'upstream_billing_probe'.*- 'ollama_cloud_usage_snapshot'.*type = 'apikey'.*extra \? 'upstream_billing_probe'.*platform IN \('openai', 'anthropic', 'kimi', 'zhipu', 'deepseek'\).*extra \? 'ollama_cloud_usage_snapshot'.*RETURNING id`).
mock.ExpectQuery(`(?s)UPDATE accounts.*- 'upstream_billing_probe'.*- 'ollama_cloud_usage_snapshot'.*type = 'apikey'.*extra \? 'upstream_billing_probe'.*platform IN \('openai', 'anthropic', 'kimi', 'zhipu', 'deepseek', 'minimax'\).*extra \? 'ollama_cloud_usage_snapshot'.*RETURNING id`).
WithArgs(int64(9)).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(int64(17)).AddRow(int64(18)))
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO scheduler_outbox (event_type, account_id, group_id, payload)")).
@@ -76,7 +76,7 @@ func TestProxyUpdateRollsBackWhenProbeInvalidationOutboxFails(t *testing.T) {
WithArgs(int64(9)).
WillReturnResult(sqlmock.NewResult(0, 0))
expectProxyUpdateReload(mock, 9, "new.example", "", "")
mock.ExpectQuery(`(?s)UPDATE accounts.*- 'upstream_billing_probe'.*- 'ollama_cloud_usage_snapshot'.*type = 'apikey'.*extra \? 'upstream_billing_probe'.*platform IN \('openai', 'anthropic', 'kimi', 'zhipu', 'deepseek'\).*extra \? 'ollama_cloud_usage_snapshot'.*RETURNING id`).
mock.ExpectQuery(`(?s)UPDATE accounts.*- 'upstream_billing_probe'.*- 'ollama_cloud_usage_snapshot'.*type = 'apikey'.*extra \? 'upstream_billing_probe'.*platform IN \('openai', 'anthropic', 'kimi', 'zhipu', 'deepseek', 'minimax'\).*extra \? 'ollama_cloud_usage_snapshot'.*RETURNING id`).
WithArgs(int64(9)).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(int64(17)))
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO scheduler_outbox (event_type, account_id, group_id, payload)")).
@@ -116,11 +116,12 @@ func TestUserPlatformQuotaRepository_BulkInsertInitial_CNProvidersAllowed(t *tes
{UserID: userID, Platform: "kimi", DailyLimitUSD: &daily},
{UserID: userID, Platform: "zhipu"},
{UserID: userID, Platform: "deepseek"},
{UserID: userID, Platform: "minimax"},
}
require.NoError(t, repo.BulkInsertInitial(txCtx, records),
"kimi/zhipu/deepseek 平台应可写入(迁移 224 后 CHECK 约束已含国产供应商)")
"kimi/zhipu/deepseek/minimax 平台应可写入(CHECK 约束已含国产供应商)")
for _, platform := range []string{"kimi", "zhipu", "deepseek"} {
for _, platform := range []string{"kimi", "zhipu", "deepseek", "minimax"} {
rec, err := repo.GetByUserPlatform(txCtx, userID, platform)
require.NoError(t, err)
require.NotNil(t, rec, "%s 配额行应已写入", platform)
+2 -2
View File
@@ -863,7 +863,7 @@ func TestAPIContracts(t *testing.T) {
"force_email_on_third_party_signup": false,
"default_concurrency": 5,
"default_balance": 1.25,
"default_platform_quotas": {"anthropic":{"daily":null,"weekly":null,"monthly":null},"antigravity":{"daily":null,"weekly":null,"monthly":null},"deepseek":{"daily":null,"weekly":null,"monthly":null},"gemini":{"daily":null,"weekly":null,"monthly":null},"grok":{"daily":null,"weekly":null,"monthly":null},"kimi":{"daily":null,"weekly":null,"monthly":null},"openai":{"daily":null,"weekly":null,"monthly":null},"zhipu":{"daily":null,"weekly":null,"monthly":null}},
"default_platform_quotas": {"anthropic":{"daily":null,"weekly":null,"monthly":null},"antigravity":{"daily":null,"weekly":null,"monthly":null},"deepseek":{"daily":null,"weekly":null,"monthly":null},"gemini":{"daily":null,"weekly":null,"monthly":null},"grok":{"daily":null,"weekly":null,"monthly":null},"kimi":{"daily":null,"weekly":null,"monthly":null},"minimax":{"daily":null,"weekly":null,"monthly":null},"openai":{"daily":null,"weekly":null,"monthly":null},"zhipu":{"daily":null,"weekly":null,"monthly":null}},
"auth_source_default_email_platform_quotas": null,
"auth_source_default_github_platform_quotas": null,
"auth_source_default_google_platform_quotas": null,
@@ -1180,7 +1180,7 @@ func TestAPIContracts(t *testing.T) {
"purchase_subscription_url": "",
"table_default_page_size": 20,
"table_page_size_options": [10, 20, 50],
"default_platform_quotas": {"anthropic":{"daily":null,"weekly":null,"monthly":null},"antigravity":{"daily":null,"weekly":null,"monthly":null},"deepseek":{"daily":null,"weekly":null,"monthly":null},"gemini":{"daily":null,"weekly":null,"monthly":null},"grok":{"daily":null,"weekly":null,"monthly":null},"kimi":{"daily":null,"weekly":null,"monthly":null},"openai":{"daily":null,"weekly":null,"monthly":null},"zhipu":{"daily":null,"weekly":null,"monthly":null}},
"default_platform_quotas": {"anthropic":{"daily":null,"weekly":null,"monthly":null},"antigravity":{"daily":null,"weekly":null,"monthly":null},"deepseek":{"daily":null,"weekly":null,"monthly":null},"gemini":{"daily":null,"weekly":null,"monthly":null},"grok":{"daily":null,"weekly":null,"monthly":null},"kimi":{"daily":null,"weekly":null,"monthly":null},"minimax":{"daily":null,"weekly":null,"monthly":null},"openai":{"daily":null,"weekly":null,"monthly":null},"zhipu":{"daily":null,"weekly":null,"monthly":null}},
"auth_source_default_email_platform_quotas": null,
"auth_source_default_github_platform_quotas": null,
"auth_source_default_google_platform_quotas": null,
+3 -3
View File
@@ -48,8 +48,8 @@ func RegisterGatewayRoutes(
isOpenAIResponsesCompatibleGatewayPlatform := func(c *gin.Context) bool {
switch getGroupPlatform(c) {
case service.PlatformOpenAI, service.PlatformGrok,
service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek:
// 国产 OpenAI 兼容供应商(kimi/zhipu/deepseek)与 openai/grok 一样经 OpenAI 网关转发。
service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax:
// 国产 OpenAI 兼容供应商与 openai/grok 一样经 OpenAI 网关转发。
return true
default:
return false
@@ -57,7 +57,7 @@ func RegisterGatewayRoutes(
}
countTokensHandler := func(c *gin.Context) {
switch getGroupPlatform(c) {
case service.PlatformOpenAI, service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek:
case service.PlatformOpenAI, service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax:
h.OpenAIGateway.CountTokens(c)
case service.PlatformGrok:
h.OpenAIGateway.GrokCountTokens(c)
+26 -7
View File
@@ -285,17 +285,20 @@ func (a *Account) IsDeepseek() bool {
return a.Platform == PlatformDeepseek
}
// IsCNProvider 报告是否为国产 OpenAI 兼容供应商(kimi/zhipu/deepseek)。
func (a *Account) IsMiniMax() bool {
return a.Platform == PlatformMiniMax
}
// IsCNProvider 报告是否为国产 OpenAI 兼容供应商(kimi/zhipu/deepseek/minimax)。
func (a *Account) IsCNProvider() bool {
return a != nil && IsCNProvider(a.Platform)
}
// IsOpenAICompatible 报告账号是否走 OpenAI 网关(OpenAI 协议族)。
// openai/grok 原生走 OpenAI 网关;kimi/zhipu/deepseek 同为 OpenAI Chat Completions
// openai/grok 原生走 OpenAI 网关;国产供应商同为 OpenAI Chat Completions
// 兼容上游,也经 OpenAI 网关转发。
func (a *Account) IsOpenAICompatible() bool {
return a != nil && (a.Platform == PlatformOpenAI || a.Platform == PlatformGrok ||
a.Platform == PlatformKimi || a.Platform == PlatformZhipu || a.Platform == PlatformDeepseek)
return a != nil && (a.Platform == PlatformOpenAI || a.Platform == PlatformGrok || a.IsCNProvider())
}
func (a *Account) GeminiOAuthType() string {
@@ -1362,6 +1365,8 @@ func (a *Account) GetOpenAIBaseURL() string {
return DefaultZhipuPayGBaseURL
case PlatformDeepseek:
return DefaultDeepseekBaseURL
case PlatformMiniMax:
return DefaultMiniMaxBaseURL
default:
return "https://api.openai.com"
}
@@ -1387,7 +1392,7 @@ func (a *Account) IsCodingPlan() bool {
// GetAPIProtocol 返回国产供应商账号的上游 API 协议。存储于
// credentials["api_protocol"];缺失或与平台不匹配时回退 chat_completions
// (与既有行为完全一致)。responses 协议仅 deepseek / kimi 支持(官方原生
// (与既有行为完全一致)。responses 协议仅 deepseek / kimi / minimax 支持(官方原生
// Responses 端点,适配 Codex);zhipu 无此端点。
func (a *Account) GetAPIProtocol() string {
if a == nil || !a.IsCNProvider() {
@@ -1410,13 +1415,13 @@ func (a *Account) GetAPIProtocol() string {
// SupportsNativeCNResponses 报告该国产供应商是否提供原生 Responses 端点。
// DeepSeek 官方为 /responses(无 /v1);Kimi 按量付费与 Coding Plan 均为
// /v1/responses(moonshot.cn / kimi.com/coding)。
// /v1/responses(moonshot.cn / kimi.com/coding);MiniMax 为 /v1/responses。
func (a *Account) SupportsNativeCNResponses() bool {
if a == nil {
return false
}
switch a.Platform {
case PlatformDeepseek, PlatformKimi:
case PlatformDeepseek, PlatformKimi, PlatformMiniMax:
return true
default:
return false
@@ -1477,6 +1482,8 @@ func (a *Account) defaultCNProtocolBaseURL(protocol string) string {
return DefaultZhipuAnthropicBaseURL
case PlatformDeepseek:
return DefaultDeepseekAnthropicBaseURL
case PlatformMiniMax:
return DefaultMiniMaxAnthropicBaseURL
}
case APIProtocolChatCompletions, APIProtocolResponses:
switch a.Platform {
@@ -1492,6 +1499,8 @@ func (a *Account) defaultCNProtocolBaseURL(protocol string) string {
return DefaultZhipuPayGBaseURL
case PlatformDeepseek:
return DefaultDeepseekBaseURL
case PlatformMiniMax:
return DefaultMiniMaxBaseURL
}
}
return ""
@@ -1528,6 +1537,8 @@ func (a *Account) GetAnthropicProtocolBaseURL() string {
return DefaultZhipuAnthropicBaseURL
case PlatformDeepseek:
return DefaultDeepseekAnthropicBaseURL
case PlatformMiniMax:
return DefaultMiniMaxAnthropicBaseURL
default:
return ""
}
@@ -1555,6 +1566,8 @@ func (a *Account) GetOpenAIFormatBaseURL() string {
return DefaultZhipuPayGBaseURL
case PlatformDeepseek:
return DefaultDeepseekBaseURL
case PlatformMiniMax:
return DefaultMiniMaxBaseURL
default:
return a.GetOpenAIBaseURL()
}
@@ -1582,6 +1595,12 @@ func (a *Account) GetCodingPlanProvider() string {
return PlatformKimi
case strings.Contains(baseURL, "bigmodel.cn"), strings.Contains(baseURL, "api.z.ai"):
return PlatformZhipu
case strings.Contains(baseURL, "minimax.io"),
strings.Contains(baseURL, "minimaxi.com"),
strings.Contains(baseURL, "minimax.com"),
a.Platform == PlatformMiniMax:
// MiniMax 额度固定打 api.minimaxi.com / api.minimax.io,自定义中转仍按账号平台路由。
return PlatformMiniMax
default:
return ""
}
@@ -78,7 +78,7 @@ func (a *Account) IsHeaderOverrideEligible() bool {
return false
}
switch a.Platform {
case PlatformAnthropic, PlatformOpenAI, PlatformKimi, PlatformZhipu, PlatformDeepseek:
case PlatformAnthropic, PlatformOpenAI, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax:
return a.Type == AccountTypeAPIKey
case PlatformGrok:
return a.Type == AccountTypeAPIKey || a.Type == AccountTypeOAuth
+1 -1
View File
@@ -515,7 +515,7 @@ func (s *AccountService) TestCredentials(ctx context.Context, id int64) error {
case PlatformGrok:
// Grok OAuth credentials are validated via token exchange/refresh and request-path probes.
return nil
case PlatformKimi, PlatformZhipu, PlatformDeepseek:
case PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax:
// 国产 OpenAI 兼容供应商:凭证为 API Key,实际可用性经余额/额度探测与转发路径验证。
return nil
default:
+1 -1
View File
@@ -314,7 +314,7 @@ func defaultAllowImageGenerationForPlatform(platform string) bool {
func compositeDefaultModelsListCandidateIDs() []string {
seen := make(map[string]struct{})
ids := make([]string, 0)
for _, platform := range []string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek} {
for _, platform := range []string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax} {
for _, id := range defaultModelsListCandidateIDs(platform) {
if _, ok := seen[id]; ok {
continue
@@ -213,7 +213,7 @@ func TestAdminService_CNProviderModelsListCandidatesKeepClaudeDefaults(t *testin
for _, model := range claude.DefaultModels {
want = append(want, model.ID)
}
for _, platform := range []string{PlatformKimi, PlatformZhipu, PlatformDeepseek} {
for _, platform := range []string{PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax} {
require.Equal(t, want, defaultModelsListCandidateIDs(platform), "platform=%s", platform)
}
}
@@ -175,6 +175,7 @@ var providerAdapters = map[string]providerAdapter{
MonitorProviderKimi: providerKimiChatAdapter,
MonitorProviderZhipu: providerZhipuChatAdapter,
MonitorProviderDeepseek: providerDeepseekChatAdapter,
MonitorProviderMiniMax: providerMiniMaxChatAdapter,
MonitorProviderAnthropic: {
buildPath: func(string) string { return providerAnthropicPath },
buildBody: func(model, prompt string) ([]byte, error) {
@@ -226,6 +227,9 @@ var providerZhipuChatAdapter = newOpenAICompatibleChatAdapter(providerZhipuPath)
//nolint:gochecknoglobals // 适配器表是只读静态数据,初始化后不变更。
var providerDeepseekChatAdapter = newOpenAICompatibleChatAdapter(providerOpenAIPath)
//nolint:gochecknoglobals // 适配器表是只读静态数据,初始化后不变更。
var providerMiniMaxChatAdapter = newOpenAICompatibleChatAdapter(providerOpenAIPath)
func newOpenAICompatibleChatAdapter(path string) providerAdapter {
return providerAdapter{
buildPath: func(string) string { return path },
@@ -458,6 +462,7 @@ var bodyMergeKeyDenyList = map[string]map[string]bool{
MonitorProviderKimi: {"model": true, "messages": true, "stream": true},
MonitorProviderZhipu: {"model": true, "messages": true, "stream": true},
MonitorProviderDeepseek: {"model": true, "messages": true, "stream": true},
MonitorProviderMiniMax: {"model": true, "messages": true, "stream": true},
}
func checkAPIMode(opts *CheckOptions) string {
@@ -479,7 +484,7 @@ func bodyMergeDenyKey(provider, apiMode string) string {
func isOpenAICompatibleChatProvider(provider string) bool {
switch provider {
case MonitorProviderOpenAI, MonitorProviderGrok,
MonitorProviderKimi, MonitorProviderZhipu, MonitorProviderDeepseek:
MonitorProviderKimi, MonitorProviderZhipu, MonitorProviderDeepseek, MonitorProviderMiniMax:
return true
default:
return false
@@ -69,6 +69,7 @@ const (
MonitorProviderKimi = "kimi"
MonitorProviderZhipu = "zhipu"
MonitorProviderDeepseek = "deepseek"
MonitorProviderMiniMax = "minimax"
// MonitorCheckMode 检测模式(channel_monitors.check_mode)。
// probe - LLM 探活(默认,原有行为)
@@ -151,7 +152,7 @@ var (
"CHANNEL_MONITOR_NOT_FOUND", "channel monitor not found",
)
ErrChannelMonitorInvalidProvider = infraerrors.BadRequest(
"CHANNEL_MONITOR_INVALID_PROVIDER", "provider must be one of openai/anthropic/gemini/grok/antigravity/kimi/zhipu/deepseek",
"CHANNEL_MONITOR_INVALID_PROVIDER", "provider must be one of openai/anthropic/gemini/grok/antigravity/kimi/zhipu/deepseek/minimax",
)
ErrChannelMonitorInvalidCheckMode = infraerrors.BadRequest(
"CHANNEL_MONITOR_INVALID_CHECK_MODE", "check_mode must be one of probe/quota/quota_probe; antigravity only supports quota",
@@ -163,7 +164,7 @@ var (
"CHANNEL_MONITOR_PROVIDER_INCOMPATIBLE", "monitor provider must match the linked account platform",
)
ErrChannelMonitorAccountNotSupportable = infraerrors.BadRequest(
"CHANNEL_MONITOR_ACCOUNT_NOT_SUPPORTABLE", "linked account cannot serve as a quota data source (cn coding plan must be kimi/zhipu, cn payg must be kimi/deepseek, openai requires an oauth account, anthropic requires oauth or setup-token)",
"CHANNEL_MONITOR_ACCOUNT_NOT_SUPPORTABLE", "linked account cannot serve as a quota data source (cn coding plan must be kimi/zhipu/minimax, cn payg must be kimi/deepseek, openai requires an oauth account, anthropic requires oauth or setup-token)",
)
ErrChannelMonitorInvalidAPIMode = infraerrors.BadRequest(
"CHANNEL_MONITOR_INVALID_API_MODE", "api_mode must be chat_completions or responses; responses is only supported for openai",
@@ -458,11 +458,20 @@ func TestMonitorAccountQuotaCapability_Matrix(t *testing.T) {
name: "zhipu coding default endpoint ok",
account: &Account{ID: 4, Platform: domain.PlatformZhipu, Credentials: map[string]any{"account_mode": AccountModeCoding}},
},
{
name: "minimax coding default endpoint ok",
account: &Account{ID: 14, Platform: domain.PlatformMiniMax, Credentials: map[string]any{"account_mode": AccountModeCoding}},
},
{
name: "zhipu payg has no balance endpoint",
account: &Account{ID: 5, Platform: domain.PlatformZhipu},
wantErr: ErrChannelMonitorAccountNotSupportable,
},
{
name: "minimax payg has no balance endpoint",
account: &Account{ID: 15, Platform: domain.PlatformMiniMax},
wantErr: ErrChannelMonitorAccountNotSupportable,
},
{
name: "kimi payg ok",
account: &Account{ID: 6, Platform: domain.PlatformKimi},
@@ -219,14 +219,14 @@ func normalizeMonitorPrimaryModel(provider, checkMode, model string) string {
// - gemini/grok/antigravity:本地统计/值通道降级,不会永久 error,放行
func monitorAccountQuotaCapability(account *Account) error {
switch account.Platform {
case PlatformKimi, PlatformZhipu, PlatformDeepseek:
case PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax:
if account.IsCodingPlan() {
if p := account.GetCodingPlanProvider(); p != PlatformKimi && p != PlatformZhipu {
if p := account.GetCodingPlanProvider(); p != PlatformKimi && p != PlatformZhipu && p != PlatformMiniMax {
return ErrChannelMonitorAccountNotSupportable
}
return nil
}
if account.Platform == PlatformZhipu {
if account.Platform == PlatformZhipu || account.Platform == PlatformMiniMax {
return ErrChannelMonitorAccountNotSupportable
}
return nil
+1 -1
View File
@@ -357,7 +357,7 @@ func isPlatformPricingMatch(groupPlatform, pricingPlatform string) bool {
// fallback used before a request target has been resolved.
func matchingPlatforms(groupPlatform string) []string {
if groupPlatform == PlatformComposite {
return []string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek}
return []string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax}
}
return []string{groupPlatform}
}
@@ -2069,7 +2069,7 @@ func TestMatchingPlatforms(t *testing.T) {
{"anthropic returns itself", PlatformAnthropic, []string{PlatformAnthropic}},
{"gemini returns itself", PlatformGemini, []string{PlatformGemini}},
{"openai returns itself", PlatformOpenAI, []string{PlatformOpenAI}},
{"composite returns concrete platforms", PlatformComposite, []string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek}},
{"composite returns concrete platforms", PlatformComposite, []string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax}},
}
for _, tt := range tests {
@@ -126,9 +126,9 @@ func (s *CNProviderBalanceCheckService) runOnce() {
quotaTargets = append(quotaTargets, quotaTarget{id: account.ID, platform: account.Platform})
continue
}
// payg 余额探测仅 kimi/deepseek(智谱无公开余额端点,payg 账号
// 依赖响应式 402/429 处理)。
if platform != PlatformZhipu && account.Schedulable {
// payg 余额探测仅 kimi/deepseek(智谱 / MiniMax 无公开余额端点,
// payg 账号依赖响应式 402/429 处理)。
if platform != PlatformZhipu && platform != PlatformMiniMax && account.Schedulable {
paygTargets = append(paygTargets, account)
}
}
@@ -141,13 +141,15 @@ func (s *CNProviderBalanceCheckService) runOnce() {
}
collect(platform, accounts)
}
// 智谱无余额端点,仅进额度探测。
// 智谱 / MiniMax 无余额端点,仅进额度探测。
if s.quotaService != nil {
accounts, err := s.accountRepo.ListByPlatform(context.Background(), PlatformZhipu)
for _, platform := range []string{PlatformZhipu, PlatformMiniMax} {
accounts, err := s.accountRepo.ListByPlatform(context.Background(), platform)
if err != nil {
log.Printf("[CNBalance] list %s accounts failed: %v", PlatformZhipu, err)
} else {
collect(PlatformZhipu, accounts)
log.Printf("[CNBalance] list %s accounts failed: %v", platform, err)
continue
}
collect(platform, accounts)
}
}
@@ -49,10 +49,13 @@ func TestCNProviderBalanceCheckRunOnceProbesCodingPlanQuota(t *testing.T) {
Credentials: map[string]any{"account_mode": "coding"}}
zhipuCoding := Account{ID: 4, Platform: PlatformZhipu, Type: AccountTypeAPIKey, Status: StatusActive,
Credentials: map[string]any{"account_mode": "coding"}}
minimaxCoding := Account{ID: 5, Platform: PlatformMiniMax, Type: AccountTypeAPIKey, Status: StatusActive,
Credentials: map[string]any{"account_mode": "coding"}}
repo := &fakeCNCheckRepo{byPlatform: map[string][]Account{
PlatformKimi: {kimiActive, kimiPaused, kimiInactive},
PlatformZhipu: {zhipuCoding},
PlatformMiniMax: {minimaxCoding},
}}
prober := &fakeCNQuotaProber{}
svc := &CNProviderBalanceCheckService{
@@ -63,7 +66,7 @@ func TestCNProviderBalanceCheckRunOnceProbesCodingPlanQuota(t *testing.T) {
svc.runOnce()
require.ElementsMatch(t, []int64{1, 2, 4}, prober.probed)
require.ElementsMatch(t, []int64{1, 2, 4, 5}, prober.probed)
}
// runOnceZhipuQuota 在 quotaService 缺失时安全跳过(Start 门控不启动的老部署路径)。
@@ -46,6 +46,7 @@ func TestValidateCodingPlanAccount_Matrix(t *testing.T) {
{name: "payg has no quota endpoint", account: paygAccount(PlatformKimi), wantReason: "CN_QUOTA_NOT_CODING_PLAN"},
{name: "kimi coding ok", account: codingAccount(PlatformKimi)},
{name: "zhipu coding ok", account: codingAccount(PlatformZhipu)},
{name: "minimax coding ok", account: codingAccount(PlatformMiniMax)},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -129,8 +129,8 @@ func (s *CNProviderQuotaService) QueryUsageForAccount(ctx context.Context, accou
func (s *CNProviderQuotaService) queryUsageForAccount(ctx context.Context, account *Account) (*CNProviderQuotaProbeResult, error) {
provider := account.GetCodingPlanProvider()
if provider != PlatformKimi && provider != PlatformZhipu {
return nil, infraerrors.New(http.StatusBadRequest, "CN_QUOTA_NOT_CODING_PLAN", "account is not a kimi/zhipu coding plan account")
if provider != PlatformKimi && provider != PlatformZhipu && provider != PlatformMiniMax {
return nil, infraerrors.New(http.StatusBadRequest, "CN_QUOTA_NOT_CODING_PLAN", "account is not a kimi/zhipu/minimax coding plan account")
}
apiKey := strings.TrimSpace(account.GetCNAPIKey())
@@ -158,6 +158,9 @@ func (s *CNProviderQuotaService) queryUsageForAccount(ctx context.Context, accou
if zhipuOrg != "" {
targetURL += "?type=2"
}
case PlatformMiniMax:
targetURL = minimaxQuotaURL(baseURL)
authHeader = "Bearer " + apiKey
}
// 探测发起前过出站 URL 安全策略(与网关转发/Grok 探测同一套校验):
@@ -177,7 +180,7 @@ func (s *CNProviderQuotaService) queryUsageForAccount(ctx context.Context, accou
}
req.Header.Set("Authorization", authHeader)
req.Header.Set("Accept", "application/json")
if provider == PlatformZhipu {
if provider == PlatformZhipu || provider == PlatformMiniMax {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept-Language", "en-US,en")
if zhipuOrg != "" {
@@ -234,6 +237,17 @@ func (s *CNProviderQuotaService) queryUsageForAccount(ctx context.Context, accou
case PlatformZhipu:
tiers = parseZhipuTokenTiers(gjson.GetBytes(bodyBytes, "data"))
result.PlanLevel = strings.TrimSpace(gjson.GetBytes(bodyBytes, "data.level").String())
case PlatformMiniMax:
if status := gjson.GetBytes(bodyBytes, "base_resp.status_code"); status.Exists() && status.Int() != 0 {
msg := strings.TrimSpace(gjson.GetBytes(bodyBytes, "base_resp.status_msg").String())
if msg == "" {
msg = "unknown minimax quota error"
}
result.Error = fmt.Sprintf("API error (%d): %s", status.Int(), msg)
return result, nil
}
tiers = parseMiniMaxUsageTiers(bodyBytes)
result.PlanLevel = strings.TrimSpace(gjson.GetBytes(bodyBytes, "current_subscribe_title").String())
}
result.Tiers = tiers
result.Success = true
@@ -305,6 +319,16 @@ func kimiQuotaURL(baseURL string) string {
return base + "/v1/usages"
}
// minimaxQuotaURL 根据推理域名选择 Token Plan / Coding Plan 额度主机。
// 官方 FAQ 写 www.minimax.io / www.minimaxi.com,实际以 Bearer Key 打 api.*。
// 国际站 api.minimax.io;国内站 api.minimaxi.com(含 api.minimax.com 与自定义回落)。
func minimaxQuotaURL(baseURL string) string {
if strings.Contains(strings.ToLower(baseURL), "minimax.io") {
return "https://api.minimax.io/v1/api/openplatform/coding_plan/remains"
}
return "https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains"
}
func zhipuQuotaHost(baseURL string) string {
switch u := strings.ToLower(baseURL); {
case strings.Contains(u, "bigmodel.cn"):
@@ -372,6 +396,71 @@ func parseKimiUsageTiers(body []byte) []CNQuotaTier {
return tiers
}
// parseMiniMaxUsageTiers 解析 MiniMax Token Plan / Coding Plan remains 响应。
//
// 只取 model_name == "general"(编程套餐),跳过 video。字段是剩余百分比,
// 展示已用 = 100 - remaining:
// - 5h:current_interval_remaining_percent + end_time
// - 周限额:仅 current_weekly_status == 1 时用 current_weekly_remaining_percent + weekly_end_time
func parseMiniMaxUsageTiers(body []byte) []CNQuotaTier {
remains := gjson.GetBytes(body, "model_remains")
if !remains.IsArray() {
return nil
}
var general gjson.Result
remains.ForEach(func(_, item gjson.Result) bool {
if strings.EqualFold(strings.TrimSpace(item.Get("model_name").String()), "general") {
general = item
return false
}
return true
})
if !general.Exists() {
return nil
}
var tiers []CNQuotaTier
if remaining, ok := cnParseF64(general.Get("current_interval_remaining_percent").Value()); ok {
used := 100 - remaining
if used < 0 {
used = 0
}
tiers = append(tiers, CNQuotaTier{
Window: "5h",
UsedPercent: used,
ResetAt: minimaxResetTime(general.Get("end_time")),
})
}
if general.Get("current_weekly_status").Int() == 1 {
if remaining, ok := cnParseF64(general.Get("current_weekly_remaining_percent").Value()); ok {
used := 100 - remaining
if used < 0 {
used = 0
}
tiers = append(tiers, CNQuotaTier{
Window: "weekly",
UsedPercent: used,
ResetAt: minimaxResetTime(general.Get("weekly_end_time")),
})
}
}
return tiers
}
func minimaxResetTime(v gjson.Result) string {
if !v.Exists() {
return ""
}
ms := v.Int()
if ms <= 0 {
return cnNormalizeResetTime(v.Value())
}
if ms < 1_000_000_000_000 {
return time.Unix(ms, 0).UTC().Format(time.RFC3339)
}
return time.UnixMilli(ms).UTC().Format(time.RFC3339)
}
// cnZhipuWindow 标识智谱 TOKENS_LIMIT 条目所属窗口。
type cnZhipuWindow int
@@ -229,6 +229,79 @@ func TestKimiQuotaURL(t *testing.T) {
require.Equal(t, "https://api.kimi.com/coding/v1/usages", kimiQuotaURL("https://api.kimi.com/coding/v1/"))
}
func TestMiniMaxQuotaURL(t *testing.T) {
t.Parallel()
require.Equal(t, "https://api.minimax.io/v1/api/openplatform/coding_plan/remains",
minimaxQuotaURL("https://api.minimax.io/v1"))
require.Equal(t, "https://api.minimax.io/v1/api/openplatform/coding_plan/remains",
minimaxQuotaURL("https://api.minimax.io/anthropic"))
require.Equal(t, "https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains",
minimaxQuotaURL("https://api.minimaxi.com/v1"))
require.Equal(t, "https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains",
minimaxQuotaURL("https://api.minimax.com/v1"))
require.Equal(t, "https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains",
minimaxQuotaURL("https://custom.example.com"))
}
func TestParseMiniMaxUsageTiers(t *testing.T) {
t.Parallel()
endMs := int64(1_700_000_000_000)
weeklyEndMs := endMs + 7*24*60*60*1000
body := []byte(`{
"base_resp": {"status_code": 0},
"current_subscribe_title": "Max",
"model_remains": [
{"model_name": "video", "current_interval_remaining_percent": 10},
{
"model_name": "general",
"current_interval_remaining_percent": 25,
"end_time": 1700000000000,
"current_weekly_status": 1,
"current_weekly_remaining_percent": 40,
"weekly_end_time": 1700604800000
}
]
}`)
tiers := parseMiniMaxUsageTiers(body)
require.Len(t, tiers, 2)
require.Equal(t, "5h", tiers[0].Window)
require.InDelta(t, 75, tiers[0].UsedPercent, 1e-9)
require.Equal(t, time.UnixMilli(endMs).UTC().Format(time.RFC3339), tiers[0].ResetAt)
require.Equal(t, "weekly", tiers[1].Window)
require.InDelta(t, 60, tiers[1].UsedPercent, 1e-9)
require.Equal(t, time.UnixMilli(weeklyEndMs).UTC().Format(time.RFC3339), tiers[1].ResetAt)
noWeekly := parseMiniMaxUsageTiers([]byte(`{
"model_remains": [{
"model_name": "general",
"current_interval_remaining_percent": 80,
"end_time": 1700000000000,
"current_weekly_status": 3,
"current_weekly_remaining_percent": 10
}]
}`))
require.Len(t, noWeekly, 1)
require.Equal(t, "5h", noWeekly[0].Window)
require.InDelta(t, 20, noWeekly[0].UsedPercent, 1e-9)
require.Nil(t, parseMiniMaxUsageTiers([]byte(`{"model_remains":[{"model_name":"video","current_interval_remaining_percent":5}]}`)))
}
func TestGetCodingPlanProvider_MiniMax(t *testing.T) {
t.Parallel()
coding := &Account{Platform: PlatformMiniMax, Type: AccountTypeAPIKey, Credentials: map[string]any{
"account_mode": AccountModeCoding,
"base_url": "https://api.minimaxi.com/v1",
}}
require.Equal(t, PlatformMiniMax, coding.GetCodingPlanProvider())
intl := &Account{Platform: PlatformMiniMax, Type: AccountTypeAPIKey, Credentials: map[string]any{
"account_mode": AccountModeCoding,
"base_url": "https://api.minimax.io/anthropic",
}}
require.Equal(t, PlatformMiniMax, intl.GetCodingPlanProvider())
require.Empty(t, (&Account{Platform: PlatformMiniMax, Credentials: map[string]any{"account_mode": AccountModePayG}}).GetCodingPlanProvider())
}
// TestCNBalanceURL Kimi 固定端点;DeepSeek 基于 base_url 拼接。
func TestCNBalanceURL(t *testing.T) {
t.Parallel()
@@ -451,6 +524,8 @@ func TestBuildUpstreamModelsRequest_CNProviders(t *testing.T) {
{"zhipu default", PlatformZhipu, "", "https://open.bigmodel.cn/api/paas/v4/models"},
{"zhipu coding", PlatformZhipu, AccountModeCoding, "https://open.bigmodel.cn/api/coding/paas/v4/models"},
{"deepseek", PlatformDeepseek, "", "https://api.deepseek.com/v1/models"},
{"minimax default", PlatformMiniMax, "", "https://api.minimaxi.com/v1/models"},
{"minimax coding", PlatformMiniMax, AccountModeCoding, "https://api.minimaxi.com/v1/models"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -487,7 +562,9 @@ func TestGetAPIProtocol(t *testing.T) {
require.Equal(t, APIProtocolAnthropic, mk(PlatformDeepseek, APIProtocolAnthropic).GetAPIProtocol())
require.Equal(t, APIProtocolResponses, mk(PlatformDeepseek, APIProtocolResponses).GetAPIProtocol())
require.Equal(t, APIProtocolResponses, mk(PlatformKimi, APIProtocolResponses).GetAPIProtocol())
require.Equal(t, APIProtocolResponses, mk(PlatformMiniMax, APIProtocolResponses).GetAPIProtocol())
require.Equal(t, APIProtocolAdaptive, mk(PlatformKimi, APIProtocolAdaptive).GetAPIProtocol())
require.Equal(t, APIProtocolAdaptive, mk(PlatformMiniMax, APIProtocolAdaptive).GetAPIProtocol())
require.Equal(t, APIProtocolAdaptive, mk(PlatformZhipu, APIProtocolAdaptive).GetAPIProtocol())
require.Equal(t, APIProtocolAdaptive, mk(PlatformDeepseek, APIProtocolAdaptive).GetAPIProtocol())
require.Equal(t, APIProtocolChatCompletions, mk(PlatformZhipu, APIProtocolResponses).GetAPIProtocol(), "zhipu 无 responses 端点")
@@ -500,6 +577,7 @@ func TestSupportsNativeCNResponses(t *testing.T) {
require.True(t, (&Account{Platform: PlatformDeepseek}).SupportsNativeCNResponses())
require.True(t, (&Account{Platform: PlatformKimi}).SupportsNativeCNResponses())
require.True(t, (&Account{Platform: PlatformKimi, Credentials: map[string]any{"account_mode": AccountModeCoding}}).SupportsNativeCNResponses())
require.True(t, (&Account{Platform: PlatformMiniMax}).SupportsNativeCNResponses())
require.False(t, (&Account{Platform: PlatformZhipu}).SupportsNativeCNResponses())
require.False(t, (&Account{Platform: PlatformOpenAI}).SupportsNativeCNResponses())
}
@@ -520,6 +598,8 @@ func TestAdaptiveProtocolBaseURLs(t *testing.T) {
{"zhipu payg", PlatformZhipu, AccountModePayG, DefaultZhipuPayGBaseURL, DefaultZhipuAnthropicBaseURL, DefaultZhipuPayGBaseURL},
{"zhipu coding", PlatformZhipu, AccountModeCoding, DefaultZhipuCodingBaseURL, DefaultZhipuAnthropicBaseURL, DefaultZhipuCodingBaseURL},
{"deepseek", PlatformDeepseek, AccountModePayG, DefaultDeepseekBaseURL, DefaultDeepseekAnthropicBaseURL, DefaultDeepseekBaseURL},
{"minimax payg", PlatformMiniMax, AccountModePayG, DefaultMiniMaxBaseURL, DefaultMiniMaxAnthropicBaseURL, DefaultMiniMaxBaseURL},
{"minimax coding", PlatformMiniMax, AccountModeCoding, DefaultMiniMaxBaseURL, DefaultMiniMaxAnthropicBaseURL, DefaultMiniMaxBaseURL},
}
for _, tc := range cases {
@@ -577,6 +657,10 @@ func TestAnthropicProtocolBaseURL(t *testing.T) {
Platform: PlatformDeepseek, Type: AccountTypeAPIKey,
Credentials: map[string]any{"api_protocol": APIProtocolAnthropic},
}).GetAnthropicProtocolBaseURL())
require.Equal(t, "https://api.minimaxi.com/anthropic", (&Account{
Platform: PlatformMiniMax, Type: AccountTypeAPIKey,
Credentials: map[string]any{"api_protocol": APIProtocolAnthropic},
}).GetAnthropicProtocolBaseURL())
// 凭证 base_url 覆盖默认值
require.Equal(t, "https://custom.example.com/anthropic", (&Account{
@@ -112,6 +112,8 @@ func DetectModelPlatform(model string) (string, bool) {
return PlatformZhipu, true
case "deepseek":
return PlatformDeepseek, true
case "minimax":
return PlatformMiniMax, true
}
if rest != "" {
normalized = strings.TrimPrefix(rest, "models/")
@@ -148,6 +150,9 @@ func DetectModelPlatform(model string) (string, bool) {
return PlatformZhipu, true
case strings.HasPrefix(normalized, "deepseek-"):
return PlatformDeepseek, true
case strings.HasPrefix(normalized, "minimax-"),
strings.HasPrefix(normalized, "abab"):
return PlatformMiniMax, true
default:
return "", false
}
@@ -195,7 +200,7 @@ func (s *GatewayService) resolveCompositeRouteDecision(ctx context.Context, grou
func isConcreteRequestPlatform(platform string) bool {
switch platform {
case PlatformAnthropic, PlatformOpenAI, PlatformGemini, PlatformAntigravity, PlatformGrok,
PlatformKimi, PlatformZhipu, PlatformDeepseek:
PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax:
return true
default:
return false
@@ -176,6 +176,9 @@ func TestDetectModelPlatform(t *testing.T) {
{name: "moonshot prefix", model: "moonshot/moonshot-v1-32k", platform: PlatformKimi, ok: true},
{name: "zhipu", model: "glm-5.2", platform: PlatformZhipu, ok: true},
{name: "deepseek", model: "deepseek-v4-pro", platform: PlatformDeepseek, ok: true},
{name: "minimax", model: "MiniMax-M3", platform: PlatformMiniMax, ok: true},
{name: "minimax prefix", model: "minimax/MiniMax-M2.5", platform: PlatformMiniMax, ok: true},
{name: "abab legacy", model: "abab6.5-chat", platform: PlatformMiniMax, ok: true},
{name: "unknown k3 alias", model: "k3-preview", ok: false},
{name: "unknown", model: "llama-4-maverick", ok: false},
}
@@ -211,13 +214,13 @@ func TestCompositeGroupSchedulerHasAllCanonicalPlatformBuckets(t *testing.T) {
platforms = append(platforms, platform)
}
require.ElementsMatch(t,
[]string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek},
[]string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax},
platforms,
)
}
func TestCompositeConcretePlatformsIncludeCNProviders(t *testing.T) {
for _, platform := range []string{PlatformKimi, PlatformZhipu, PlatformDeepseek} {
for _, platform := range []string{PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax} {
require.True(t, isConcreteRequestPlatform(platform))
require.True(t, canCopyAccountsFromGroupPlatform(PlatformComposite, platform))
}
+10 -4
View File
@@ -47,6 +47,7 @@ const (
PlatformKimi = domain.PlatformKimi
PlatformZhipu = domain.PlatformZhipu
PlatformDeepseek = domain.PlatformDeepseek
PlatformMiniMax = domain.PlatformMiniMax
PlatformComposite = domain.PlatformComposite
// PlatformKiro is retained for unsupported-platform threshold tests and legacy
// account rows. Scheduling-threshold evaluation never pauses kiro accounts.
@@ -75,6 +76,8 @@ const (
DefaultZhipuPayGBaseURL = "https://open.bigmodel.cn/api/paas/v4"
DefaultZhipuCodingBaseURL = "https://open.bigmodel.cn/api/coding/paas/v4"
DefaultDeepseekBaseURL = "https://api.deepseek.com"
// MiniMax 按量付费与 Coding/Token Plan 共用推理域名,靠 API Key 区分套餐。
DefaultMiniMaxBaseURL = "https://api.minimaxi.com/v1"
)
// 国产供应商 Anthropic 协议端点的默认 base_url(上游路径为 {base}/v1/messages)。
@@ -84,12 +87,13 @@ const (
DefaultKimiCodingAnthropicBaseURL = "https://api.kimi.com/coding"
DefaultZhipuAnthropicBaseURL = "https://open.bigmodel.cn/api/anthropic"
DefaultDeepseekAnthropicBaseURL = "https://api.deepseek.com/anthropic"
DefaultMiniMaxAnthropicBaseURL = "https://api.minimaxi.com/anthropic"
)
// IsCNProvider 报告 platform 是否为国产 OpenAI 兼容供应商(kimi/zhipu/deepseek)。
// IsCNProvider 报告 platform 是否为国产 OpenAI 兼容供应商(kimi/zhipu/deepseek/minimax)。
func IsCNProvider(platform string) bool {
switch platform {
case PlatformKimi, PlatformZhipu, PlatformDeepseek:
case PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax:
return true
default:
return false
@@ -108,17 +112,19 @@ var AllowedQuotaPlatforms = []string{
PlatformKimi,
PlatformZhipu,
PlatformDeepseek,
PlatformMiniMax,
}
// AllowedSchedulingThresholdPlatforms 是允许设置账号自动停调阈值的平台列表。
// openai/anthropic/grok 有原生用量窗口;kimi/zhipu 的 Coding Plan 同样暴露 5h/weekly
// 滚动窗口,纳入阈值评估。deepseek 为余额型,走余额检测而非阈值。
// openai/anthropic/grok 有原生用量窗口;kimi/zhipu/minimax 的 Coding Plan 同样暴露
// 5h/weekly 滚动窗口,纳入阈值评估。deepseek 为余额型,走余额检测而非阈值。
var AllowedSchedulingThresholdPlatforms = []string{
PlatformOpenAI,
PlatformAnthropic,
PlatformGrok,
PlatformKimi,
PlatformZhipu,
PlatformMiniMax,
}
// IsAllowedQuotaPlatform 报告 s 是否为合法的 quota platform 标识。
@@ -1014,7 +1014,7 @@ func OllamaCloudUsageStateFromAccount(account *Account) *OllamaCloudUsageState {
// (ollamaCloudUsagePlatformsSQL)是本列表的镜像,两侧必须同步修改。
func isOllamaCloudUsagePlatform(platform string) bool {
switch platform {
case PlatformOpenAI, PlatformAnthropic, PlatformKimi, PlatformZhipu, PlatformDeepseek:
case PlatformOpenAI, PlatformAnthropic, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax:
return true
default:
return false
@@ -497,7 +497,7 @@ func TestIsOllamaCloudUsageAccountStrictOfficialHost(t *testing.T) {
// oauth 类型账号即使平台与 base_url 都命中也不进用量窗口(仅 apikey 账号)。
func TestIsOllamaCloudUsageAccountRejectsOAuthType(t *testing.T) {
for _, platform := range []string{PlatformOpenAI, PlatformAnthropic, PlatformKimi, PlatformZhipu, PlatformDeepseek} {
for _, platform := range []string{PlatformOpenAI, PlatformAnthropic, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax} {
account := ollamaUsageAccount(1)
account.Platform = platform
account.Type = AccountTypeOAuth
@@ -24,7 +24,7 @@ import (
)
func TestResolveMessagesDispatchModel_CNProvidersNoDispatchMapping(t *testing.T) {
for _, platform := range []string{PlatformKimi, PlatformZhipu, PlatformDeepseek} {
for _, platform := range []string{PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax} {
g := &Group{Platform: platform}
require.Empty(t, g.ResolveMessagesDispatchModel("claude-sonnet-4-5"),
"CN 分组(%s)不得返回调度级映射模型(openai 默认值会发给 CN 上游)", platform)
@@ -104,7 +104,7 @@ func TestResponsesStreamingFromNativeAnthropic_ClientDisconnectDrainsUsage(t *te
}
func TestHandle403_CNProviderHTMLBodySkipsAccountPenalty(t *testing.T) {
for _, platform := range []string{PlatformKimi, PlatformZhipu, PlatformDeepseek} {
for _, platform := range []string{PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax} {
repo := &rateLimitAccountRepoStub{}
service := NewRateLimitService(repo, nil, &config.Config{}, nil, nil)
account := &Account{ID: 401, Platform: platform, Type: AccountTypeAPIKey}
@@ -292,7 +292,7 @@ func (s *OpenAIGatewayService) SelectAccountForTokenCount(
// handler 调度入口仍需导出,保持导出名。)
func NormalizeOpenAICompatiblePlatform(platform string) string {
switch platform {
case PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek:
case PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax:
return platform
default:
return PlatformOpenAI
@@ -270,7 +270,7 @@ func TestSanitizeOpenAIResponsesToolSchemasForPlatform_ReplayBoundary(t *testing
// A malformed tool definition may be replayed after account failover. Every
// compatible account must repair it, while non-OpenAI providers retain their
// supported regex semantics.
for _, platform := range []string{PlatformAnthropic, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek} {
for _, platform := range []string{PlatformAnthropic, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax} {
t.Run(platform, func(t *testing.T) {
for attempt := 0; attempt < 2; attempt++ {
normalized, changed, err := sanitizeOpenAIResponsesToolSchemasForPlatform(body, platform)
@@ -115,7 +115,7 @@ func TestSchedulerBulkAccountEventScopesOpenAIRebuildToFreshPlatform(t *testing.
}
func TestSchedulerBulkAccountEventScopesCNRebuildToFreshPlatform(t *testing.T) {
for _, platform := range []string{PlatformKimi, PlatformZhipu, PlatformDeepseek} {
for _, platform := range []string{PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax} {
t.Run(platform, func(t *testing.T) {
cache := newBulkEventSnapshotCache()
repo := newBulkEventAccountRepo(&Account{ID: 1, Platform: platform, GroupIDs: []int64{12}})
@@ -609,7 +609,7 @@ func (s *SchedulerSnapshotService) handleBulkAccountEvent(ctx context.Context, p
}
accountGroupIDs := s.normalizeGroupIDs(account.GroupIDs)
switch account.Platform {
case PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek:
case PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax:
addPlatformGroups(account.Platform, accountGroupIDs)
case PlatformAntigravity:
// 批量更新可能刚关闭 mixed_scheduling,仍需清理两个兼容平台的旧快照。
@@ -824,8 +824,8 @@ func (s *SchedulerSnapshotService) rebuildByAccount(ctx context.Context, account
return s.rebuildBuckets(ctx, buckets, reason)
}
func schedulerSnapshotPlatforms() [8]string {
return [8]string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek}
func schedulerSnapshotPlatforms() [9]string {
return [9]string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax}
}
// 生命周期辅助函数有意排除 group0;full rebuild 构造 group0 canonical 集时必须显式调用 canonical helper。
@@ -1013,7 +1013,7 @@ func IsUpstreamBillingProbeIdentity(platform, accountType string) bool {
}
switch platform {
case PlatformOpenAI, PlatformAnthropic, PlatformGemini, PlatformAntigravity, PlatformGrok,
PlatformKimi, PlatformZhipu, PlatformDeepseek:
PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax:
return true
default:
return false
@@ -16,7 +16,7 @@ import (
func TestUpstreamBillingProbeIdentityCoversAllAPIKeyPlatforms(t *testing.T) {
for _, platform := range []string{
PlatformOpenAI, PlatformGrok, PlatformAnthropic, PlatformGemini, PlatformAntigravity,
PlatformKimi, PlatformZhipu, PlatformDeepseek,
PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax,
} {
require.True(t, IsUpstreamBillingProbeIdentity(platform, AccountTypeAPIKey), platform)
require.True(t, isUpstreamBillingProbeAccount(&Account{Platform: platform, Type: AccountTypeAPIKey}), platform)
@@ -0,0 +1,60 @@
-- 把 MiniMax 加入国产供应商平台白名单:
-- 1. user_platform_quotas.platform CHECK
-- 2. composite_model_routes.target_platform CHECK
-- 3. channel_monitors / channel_monitor_request_templates.provider CHECK
--
-- 与 224/226/227 同型:DROP IF EXISTS 后重建超集约束,存量行瞬时校验通过。
ALTER TABLE user_platform_quotas
DROP CONSTRAINT IF EXISTS user_platform_quotas_platform_check;
ALTER TABLE user_platform_quotas
ADD CONSTRAINT user_platform_quotas_platform_check
CHECK (platform IN ('anthropic', 'openai', 'gemini', 'antigravity', 'grok',
'kimi', 'zhipu', 'deepseek', 'minimax'));
ALTER TABLE composite_model_routes
DROP CONSTRAINT IF EXISTS composite_model_routes_target_platform_check;
ALTER TABLE composite_model_routes
ADD CONSTRAINT composite_model_routes_target_platform_check
CHECK (target_platform IN ('anthropic', 'openai', 'gemini', 'antigravity', 'grok',
'kimi', 'zhipu', 'deepseek', 'minimax'));
DO $$
DECLARE
monitor_constraint_def TEXT;
template_constraint_def TEXT;
BEGIN
SELECT pg_get_constraintdef(c.oid)
INTO monitor_constraint_def
FROM pg_constraint c
JOIN pg_class t ON t.oid = c.conrelid
WHERE t.relname = 'channel_monitors'
AND c.conname = 'channel_monitors_provider_check';
IF monitor_constraint_def IS NULL OR position('minimax' IN monitor_constraint_def) = 0 THEN
ALTER TABLE channel_monitors
DROP CONSTRAINT IF EXISTS channel_monitors_provider_check;
ALTER TABLE channel_monitors
ADD CONSTRAINT channel_monitors_provider_check
CHECK (provider IN ('openai', 'anthropic', 'gemini', 'grok',
'antigravity', 'kimi', 'zhipu', 'deepseek', 'minimax'));
END IF;
SELECT pg_get_constraintdef(c.oid)
INTO template_constraint_def
FROM pg_constraint c
JOIN pg_class t ON t.oid = c.conrelid
WHERE t.relname = 'channel_monitor_request_templates'
AND c.conname = 'channel_monitor_request_templates_provider_check';
IF template_constraint_def IS NULL OR position('minimax' IN template_constraint_def) = 0 THEN
ALTER TABLE channel_monitor_request_templates
DROP CONSTRAINT IF EXISTS channel_monitor_request_templates_provider_check;
ALTER TABLE channel_monitor_request_templates
ADD CONSTRAINT channel_monitor_request_templates_provider_check
CHECK (provider IN ('openai', 'anthropic', 'gemini', 'grok',
'antigravity', 'kimi', 'zhipu', 'deepseek', 'minimax'));
END IF;
END $$;
@@ -0,0 +1,21 @@
package migrations
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestMiniMaxPlatformMigration(t *testing.T) {
content, err := FS.ReadFile("235_add_minimax_platform.sql")
require.NoError(t, err)
sql := strings.Join(strings.Fields(string(content)), " ")
require.Contains(t, sql,
"CHECK (platform IN ('anthropic', 'openai', 'gemini', 'antigravity', 'grok', 'kimi', 'zhipu', 'deepseek', 'minimax'))")
require.Contains(t, sql,
"CHECK (target_platform IN ('anthropic', 'openai', 'gemini', 'antigravity', 'grok', 'kimi', 'zhipu', 'deepseek', 'minimax'))")
require.Contains(t, sql,
"CHECK (provider IN ('openai', 'anthropic', 'gemini', 'grok', 'antigravity', 'kimi', 'zhipu', 'deepseek', 'minimax'))")
}
+8
View File
@@ -14,6 +14,10 @@ Composite groups can route to these concrete account platforms:
- OpenAI
- Antigravity
- Grok
- Kimi
- Zhipu GLM
- DeepSeek
- MiniMax
The selected concrete platform is used for account selection, user platform
quota checks, post-usage billing, ops error platform attribution, channel
@@ -68,6 +72,10 @@ Composite routing detects common public model IDs and provider-prefixed IDs:
- `gpt-*`, `o*`, `codex-*`, `text-embedding-*`, `dall-e-*`, and
`openai/*` route to OpenAI.
- `grok-*` and `xai/grok-*` route to Grok.
- `kimi-*`, `moonshot-*`, and `k3` route to Kimi.
- `glm-*` routes to Zhipu GLM.
- `deepseek-*` routes to DeepSeek.
- `minimax-*`, `abab*`, and `minimax/*` route to MiniMax.
Unknown or ambiguous model names fail closed with a client error instead of
guessing a provider.
+1
View File
@@ -14,6 +14,7 @@ export type Provider =
| 'kimi'
| 'zhipu'
| 'deepseek'
| 'minimax'
export type MonitorStatus = 'operational' | 'degraded' | 'failed' | 'error'
export type BodyOverrideMode = 'off' | 'merge' | 'replace'
export type APIMode = 'chat_completions' | 'responses'
+3 -1
View File
@@ -38,17 +38,19 @@ export type SchedulingThresholdPlatformType =
| "grok"
| "kimi"
| "zhipu"
| "minimax"
export type AccountSchedulingThresholdsMap = Record<SchedulingThresholdPlatformType, number>
// 与后端 AllowedSchedulingThresholdPlatforms 保持一致(deepseek 为余额型,
// 走余额检测而非用量阈值)。
// 走余额检测而非用量阈值;minimax Coding/Token Plan 有 5h/weekly 窗口)。
export const SCHEDULING_THRESHOLD_PLATFORMS: SchedulingThresholdPlatformType[] = [
"openai",
"anthropic",
"grok",
"kimi",
"zhipu",
"minimax",
]
export function normalizeAccountSchedulingThresholdsMap(
@@ -430,7 +430,7 @@
</template>
<!-- CN providers (Kimi / Zhipu / DeepSeek): coding-plan quota or payg balance -->
<template v-else-if="account.platform === 'kimi' || account.platform === 'zhipu' || account.platform === 'deepseek'">
<template v-else-if="account.platform === 'kimi' || account.platform === 'zhipu' || account.platform === 'deepseek' || account.platform === 'minimax'">
<!-- 挂在 CN 平台下的 Ollama Cloud 账号(资格由后端下发 eligible):用量由
Ollama 用量窗口负责。这类账号不是国产厂商订阅,CN 的额度/余额探测端点由
base_url 衍生,对 ollama.com 会被后端出站 URL 白名单拒绝,渲染出来只会
@@ -727,7 +727,8 @@ const showUsageWindows = computed(() => {
if (
props.account.platform === 'kimi' ||
props.account.platform === 'zhipu' ||
props.account.platform === 'deepseek'
props.account.platform === 'deepseek' ||
props.account.platform === 'minimax'
) {
return true
}
@@ -26,7 +26,7 @@ import { CN_BASE_URL_PRESETS, type CnBaseUrlPreset } from './credentialsBuilder'
// 与 Grok 预设一致,仅作快速填充,输入框仍接受任意第三方转发地址。
// 传入 protocol 时只显示该协议档的预设(协议 × 账号类型正交分档)。
const props = defineProps<{
platform: 'kimi' | 'zhipu' | 'deepseek'
platform: 'kimi' | 'zhipu' | 'deepseek' | 'minimax'
/** 当前已选账号类型,用于过滤和高亮匹配的预设 */
mode?: 'payg' | 'coding'
/** 当前已选 API 协议,用于过滤和高亮匹配的预设 */
@@ -202,6 +202,19 @@
<PlatformIcon platform="deepseek" size="sm" />
DeepSeek
</button>
<button
type="button"
@click="selectCNPlatform('minimax')"
:class="[
'flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium transition-all',
form.platform === 'minimax'
? 'bg-white text-rose-600 shadow-sm dark:bg-dark-600 dark:text-rose-400'
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
]"
>
<PlatformIcon platform="minimax" size="sm" />
MiniMax
</button>
</div>
</div>
@@ -3854,11 +3867,13 @@ import {
cnSupportsNativeResponses,
defaultCNAdaptiveBaseUrls,
defaultCNBaseUrl,
isCNProviderPlatform,
isHeaderOverrideCapable,
validateHeaderOverrideRows,
type CnAccountMode,
type CnApiProtocol,
type CnNativeApiProtocol,
type CnProviderPlatform,
type HeaderOverrideRow
} from '@/components/account/credentialsBuilder'
import {
@@ -3960,6 +3975,8 @@ const apiKeyValuePlaceholder = computed(() => {
return '<api-key>.<secret>'
case 'deepseek':
return 'sk-...'
case 'minimax':
return 'sk-...'
default:
return 'sk-ant-...'
}
@@ -4061,13 +4078,11 @@ const adaptiveBaseUrls = ref<Record<CnNativeApiProtocol, string>>({
anthropic: '',
responses: ''
})
const isCNPlatform = computed(
() => form.platform === 'kimi' || form.platform === 'zhipu' || form.platform === 'deepseek'
)
const isCNPlatform = computed(() => isCNProviderPlatform(form.platform))
// CnBaseUrlPresets 的 platform prop 是平台字面量联合类型,模板里不能写
// `as` 断言(其中的 `|` 会被 eslint 误判为 Vue2 filter 语法),经此 computed 传递。
const cnPresetPlatform = computed<'kimi' | 'zhipu' | 'deepseek'>(() => {
if (form.platform === 'kimi' || form.platform === 'zhipu' || form.platform === 'deepseek') {
const cnPresetPlatform = computed<CnProviderPlatform>(() => {
if (isCNProviderPlatform(form.platform)) {
return form.platform
}
return 'kimi'
@@ -4093,7 +4108,7 @@ const cnAdaptiveProtocolOptions = computed<Array<{ value: CnNativeApiProtocol; l
return opts
})
function resetAdaptiveBaseUrls(platform: 'kimi' | 'zhipu' | 'deepseek', mode: CnAccountMode) {
function resetAdaptiveBaseUrls(platform: CnProviderPlatform, mode: CnAccountMode) {
adaptiveBaseUrls.value = defaultCNAdaptiveBaseUrls(platform, mode)
}
// 当前选中平台的品牌色(选中卡片描边 / 图标底色),与 platformColors 取色一致。
@@ -4105,6 +4120,8 @@ const cnAccentActiveClass = computed(() => {
return 'border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20'
case 'deepseek':
return 'border-teal-500 bg-teal-50 dark:bg-teal-900/20'
case 'minimax':
return 'border-rose-500 bg-rose-50 dark:bg-rose-900/20'
default:
return 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
}
@@ -4117,13 +4134,15 @@ const cnAccentIconClass = computed(() => {
return 'bg-indigo-500 text-white'
case 'deepseek':
return 'bg-teal-500 text-white'
case 'minimax':
return 'bg-rose-500 text-white'
default:
return 'bg-primary-500 text-white'
}
})
// 切换国产供应商平台:强制 apikey 类型,deepseek 无 coding 套餐故锁定 payg,
// 协议回落 adaptive,并把 base url 重置为该平台默认端点。
function selectCNPlatform(platform: 'kimi' | 'zhipu' | 'deepseek') {
function selectCNPlatform(platform: CnProviderPlatform) {
form.platform = platform
form.type = 'apikey'
accountCategory.value = 'apikey'
@@ -4676,7 +4695,7 @@ watch(
() => form.platform,
(newPlatform) => {
// Reset base URL based on platform
if (newPlatform === 'kimi' || newPlatform === 'zhipu' || newPlatform === 'deepseek') {
if (isCNProviderPlatform(newPlatform)) {
apiKeyBaseUrl.value = defaultCNBaseUrl(newPlatform, accountMode.value, apiProtocol.value)
} else {
apiKeyBaseUrl.value =
@@ -5626,7 +5645,7 @@ const handleSubmit = async () => {
// 国产供应商:账号模式 + 协议 + 对应端点写入凭据;后端按 account_mode 路由
// 额度/余额探测,按 api_protocol 路由转发端点与格式。注意 CN apikey 走本函数
// 的通用路径(直接 doCreateAccount),不经过 createAccountAndFinish。
if (form.platform === 'kimi' || form.platform === 'zhipu' || form.platform === 'deepseek') {
if (isCNProviderPlatform(form.platform)) {
credentials.account_mode = accountMode.value
credentials.api_protocol = apiProtocol.value
if (apiProtocol.value === 'adaptive') {
@@ -2949,11 +2949,13 @@ import {
cnSupportsNativeResponses,
defaultCNAdaptiveBaseUrls,
defaultCNBaseUrl,
isCNProviderPlatform,
HEADER_OVERRIDE_ENABLED_CREDENTIAL_KEY,
HEADER_OVERRIDES_CREDENTIAL_KEY,
type CnAccountMode,
type CnApiProtocol,
type CnNativeApiProtocol,
type CnProviderPlatform,
type HeaderOverrideRow
} from '@/components/account/credentialsBuilder'
import {
@@ -3046,18 +3048,14 @@ const editApiKey = ref('')
// account_mode 决定额度/余额监控路径,api_protocol 决定转发端点与格式;
// 二者均可修正(早期创建的账号可能存错默认值),切换时重置 base_url 预置。
const isCNApiKeyAccount = computed(
() =>
props.account?.type === 'apikey' &&
(props.account.platform === 'kimi' ||
props.account.platform === 'zhipu' ||
props.account.platform === 'deepseek')
() => props.account?.type === 'apikey' && isCNProviderPlatform(props.account.platform)
)
// CnBaseUrlPresets 的 platform prop 是平台字面量联合类型,模板里不能写
// `as` 断言(其中的 `|` 会被 eslint 误判为 Vue2 filter 语法),经此 computed 传递。
const cnPresetPlatform = computed<'kimi' | 'zhipu' | 'deepseek'>(() => {
const cnPresetPlatform = computed<CnProviderPlatform>(() => {
const platform = props.account?.platform
if (platform === 'kimi' || platform === 'zhipu' || platform === 'deepseek') {
return platform
if (isCNProviderPlatform(platform ?? '')) {
return platform as CnProviderPlatform
}
return 'kimi'
})
@@ -3984,7 +3982,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
const credentials = newAccount.credentials as Record<string, unknown>
// 国产供应商:读取 account_mode 与 api_protocol 作为可编辑初始值
// (编辑弹窗允许修正两者,用于修复早期存错默认值的账号)。
if (newAccount.platform === 'kimi' || newAccount.platform === 'zhipu' || newAccount.platform === 'deepseek') {
if (isCNProviderPlatform(newAccount.platform)) {
editAccountMode.value = credentials.account_mode === 'coding' ? 'coding' : 'payg'
const storedProtocol = credentials.api_protocol
editApiProtocol.value =
@@ -208,7 +208,8 @@ const upstreamSyncPlatforms = new Set([
'grok',
'kimi',
'zhipu',
'deepseek'
'deepseek',
'minimax'
])
const canSyncUpstream = computed(() => {
if (props.accountId) {
@@ -154,7 +154,7 @@ describe('AccountUsageCell', () => {
expect(updatedAccount?.ollama_cloud_usage?.auto_refresh_enabled).toBe(false)
})
it.each(['kimi', 'zhipu', 'deepseek'] as const)(
it.each(['kimi', 'zhipu', 'deepseek', 'minimax'] as const)(
'%s apikey 账号 Ollama Cloud eligible 时渲染 Ollama 用量单元格并跳过 CN 子单元格',
async (platform) => {
const wrapper = mount(AccountUsageCell, {
@@ -217,7 +217,7 @@ describe('BulkEditAccountModal', () => {
expect(wrapper.findAll('[data-testid="grok-base-url-preset"]').length).toBe(0)
})
it.each(['kimi', 'zhipu', 'deepseek'])('全部目标为 %s API Key 时展示请求头覆写', (platform) => {
it.each(['kimi', 'zhipu', 'deepseek', 'minimax'])('全部目标为 %s API Key 时展示请求头覆写', (platform) => {
const wrapper = mountModal({
selectedPlatforms: [platform],
selectedTypes: ['apikey']
@@ -226,7 +226,7 @@ describe('BulkEditAccountModal', () => {
expect(wrapper.find('#bulk-edit-header-override-enabled').exists()).toBe(true)
})
it.each(['kimi', 'zhipu', 'deepseek'])('目标为 %s OAuth 时不展示请求头覆写', (platform) => {
it.each(['kimi', 'zhipu', 'deepseek', 'minimax'])('目标为 %s OAuth 时不展示请求头覆写', (platform) => {
const wrapper = mountModal({
selectedPlatforms: [platform],
selectedTypes: ['oauth']
@@ -446,6 +446,28 @@ describe('CreateAccountModal OpenAI long-context billing', () => {
})
})
it('submits adaptive MiniMax protocol endpoints', async () => {
const wrapper = mountModal()
await selectButtonByText(wrapper, 'MiniMax')
await wrapper.get('form#create-account-form input[type="text"]').setValue('MiniMax adaptive')
await wrapper.get('form#create-account-form input[type="password"]').setValue('sk-minimax')
await wrapper.get('form#create-account-form').trigger('submit.prevent')
await flushPromises()
expect(createAccountMock).toHaveBeenCalledTimes(1)
expect(createAccountMock.mock.calls[0]?.[0]?.credentials).toMatchObject({
account_mode: 'payg',
api_protocol: 'adaptive',
base_url: 'https://api.minimaxi.com/v1',
api_base_urls: {
chat_completions: 'https://api.minimaxi.com/v1',
anthropic: 'https://api.minimaxi.com/anthropic',
responses: 'https://api.minimaxi.com/v1'
}
})
})
it('uses the edited adaptive Chat endpoint when previewing upstream models', async () => {
const wrapper = mountModal()
await selectButtonByText(wrapper, 'Kimi')
@@ -3,9 +3,10 @@ import { describe, expect, it } from 'vitest'
import { cnSupportsNativeResponses, defaultCNAdaptiveBaseUrls } from '../credentialsBuilder'
describe('cnSupportsNativeResponses', () => {
it('is true for DeepSeek and Kimi only', () => {
it('is true for DeepSeek, Kimi, and MiniMax', () => {
expect(cnSupportsNativeResponses('deepseek')).toBe(true)
expect(cnSupportsNativeResponses('kimi')).toBe(true)
expect(cnSupportsNativeResponses('minimax')).toBe(true)
expect(cnSupportsNativeResponses('zhipu')).toBe(false)
expect(cnSupportsNativeResponses('openai')).toBe(false)
})
@@ -45,4 +46,14 @@ describe('defaultCNAdaptiveBaseUrls', () => {
responses: 'https://api.deepseek.com'
})
})
it('uses the same MiniMax CN endpoints for payg and coding', () => {
const expected = {
chat_completions: 'https://api.minimaxi.com/v1',
anthropic: 'https://api.minimaxi.com/anthropic',
responses: 'https://api.minimaxi.com/v1'
}
expect(defaultCNAdaptiveBaseUrls('minimax', 'payg')).toEqual(expected)
expect(defaultCNAdaptiveBaseUrls('minimax', 'coding')).toEqual(expected)
})
})
@@ -107,7 +107,7 @@ describe('isHeaderOverrideCapable', () => {
})
it('kimi/zhipu/deepseek only support apikey accounts', () => {
for (const platform of ['kimi', 'zhipu', 'deepseek']) {
for (const platform of ['kimi', 'zhipu', 'deepseek', 'minimax']) {
expect(isHeaderOverrideCapable(platform, 'apikey')).toBe(true)
expect(isHeaderOverrideCapable(platform, 'oauth')).toBe(false)
}
@@ -42,7 +42,8 @@ export function isHeaderOverrideCapable(platform: string, type: string): boolean
platform === 'openai' ||
platform === 'kimi' ||
platform === 'zhipu' ||
platform === 'deepseek'
platform === 'deepseek' ||
platform === 'minimax'
) {
return type === 'apikey'
}
@@ -255,14 +256,19 @@ export const GROK_BASE_URL_PRESETS: GrokBaseUrlPreset[] = [
// 两者正交。同协议请求零转换直通,跨协议组合才走转换链。
export type CnAccountMode = 'payg' | 'coding'
export type CnProviderPlatform = 'kimi' | 'zhipu' | 'deepseek' | 'minimax'
/** deepseek / kimi 支持原生 responses;adaptive 会按入站协议选择原生端点。 */
/** deepseek / kimi / minimax 支持原生 responses;adaptive 会按入站协议选择原生端点。 */
export type CnApiProtocol = 'adaptive' | 'chat_completions' | 'anthropic' | 'responses'
export type CnNativeApiProtocol = Exclude<CnApiProtocol, 'adaptive'>
/** DeepSeek 与 Kimi(按量付费 / Coding Plan)提供原生 Responses 端点。 */
export function isCNProviderPlatform(platform: string): platform is CnProviderPlatform {
return platform === 'kimi' || platform === 'zhipu' || platform === 'deepseek' || platform === 'minimax'
}
/** DeepSeek、Kimi 与 MiniMax 提供原生 Responses 端点。 */
export function cnSupportsNativeResponses(platform: string): boolean {
return platform === 'deepseek' || platform === 'kimi'
return platform === 'deepseek' || platform === 'kimi' || platform === 'minimax'
}
export interface CnBaseUrlPreset {
@@ -274,7 +280,7 @@ export interface CnBaseUrlPreset {
}
/** 各供应商按账号类型 × API 协议分档的快捷端点(点击快速填充,输入框仍可自由填写)。 */
export const CN_BASE_URL_PRESETS: Record<'kimi' | 'zhipu' | 'deepseek', CnBaseUrlPreset[]> = {
export const CN_BASE_URL_PRESETS: Record<CnProviderPlatform, CnBaseUrlPreset[]> = {
kimi: [
{ mode: 'payg', protocol: 'chat_completions', label: 'Moonshot', url: 'https://api.moonshot.cn/v1' },
{ mode: 'payg', protocol: 'anthropic', label: 'Moonshot Anthropic', url: 'https://api.moonshot.cn/anthropic' },
@@ -293,6 +299,20 @@ export const CN_BASE_URL_PRESETS: Record<'kimi' | 'zhipu' | 'deepseek', CnBaseUr
{ mode: 'payg', protocol: 'chat_completions', label: 'DeepSeek', url: 'https://api.deepseek.com' },
{ mode: 'payg', protocol: 'anthropic', label: 'DeepSeek Anthropic', url: 'https://api.deepseek.com/anthropic' },
{ mode: 'payg', protocol: 'responses', label: 'DeepSeek Responses', url: 'https://api.deepseek.com' }
],
minimax: [
{ mode: 'payg', protocol: 'chat_completions', label: 'MiniMax CN', url: 'https://api.minimaxi.com/v1' },
{ mode: 'payg', protocol: 'anthropic', label: 'MiniMax CN Anthropic', url: 'https://api.minimaxi.com/anthropic' },
{ mode: 'payg', protocol: 'responses', label: 'MiniMax CN Responses', url: 'https://api.minimaxi.com/v1' },
{ mode: 'payg', protocol: 'chat_completions', label: 'MiniMax Intl', url: 'https://api.minimax.io/v1' },
{ mode: 'payg', protocol: 'anthropic', label: 'MiniMax Intl Anthropic', url: 'https://api.minimax.io/anthropic' },
{ mode: 'payg', protocol: 'responses', label: 'MiniMax Intl Responses', url: 'https://api.minimax.io/v1' },
{ mode: 'coding', protocol: 'chat_completions', label: 'MiniMax Coding CN', url: 'https://api.minimaxi.com/v1' },
{ mode: 'coding', protocol: 'anthropic', label: 'MiniMax Coding CN Anthropic', url: 'https://api.minimaxi.com/anthropic' },
{ mode: 'coding', protocol: 'responses', label: 'MiniMax Coding CN Responses', url: 'https://api.minimaxi.com/v1' },
{ mode: 'coding', protocol: 'chat_completions', label: 'MiniMax Coding Intl', url: 'https://api.minimax.io/v1' },
{ mode: 'coding', protocol: 'anthropic', label: 'MiniMax Coding Intl Anthropic', url: 'https://api.minimax.io/anthropic' },
{ mode: 'coding', protocol: 'responses', label: 'MiniMax Coding Intl Responses', url: 'https://api.minimax.io/v1' }
]
}
@@ -310,11 +330,13 @@ export function defaultCNBaseUrl(
return 'https://open.bigmodel.cn/api/anthropic'
case 'deepseek':
return 'https://api.deepseek.com/anthropic'
case 'minimax':
return 'https://api.minimaxi.com/anthropic'
default:
return ''
}
}
// responses:Kimi / DeepSeek 的 base 与 chat_completions 相同(端点路径差异由后端处理)。
// responses:Kimi / DeepSeek / MiniMax 的 base 与 chat_completions 相同(端点路径差异由后端处理)。
switch (platform) {
case 'kimi':
return mode === 'coding' ? 'https://api.kimi.com/coding/v1' : 'https://api.moonshot.cn/v1'
@@ -324,6 +346,8 @@ export function defaultCNBaseUrl(
: 'https://open.bigmodel.cn/api/paas/v4'
case 'deepseek':
return 'https://api.deepseek.com'
case 'minimax':
return 'https://api.minimaxi.com/v1'
default:
return ''
}
@@ -331,7 +355,7 @@ export function defaultCNBaseUrl(
/** 返回自适应模式下需要配置的原生协议及其默认端点。 */
export function defaultCNAdaptiveBaseUrls(
platform: 'kimi' | 'zhipu' | 'deepseek',
platform: CnProviderPlatform,
mode: CnAccountMode
): Record<CnNativeApiProtocol, string> {
return {
@@ -346,7 +370,7 @@ export function defaultCNAdaptiveBaseUrls(
// 共用,避免多处复制条件后一处改另一处漏改。
export function cnQuotaCellVisible(platform: string, accountMode: string): boolean {
return (platform === 'kimi' || platform === 'zhipu') && accountMode === 'coding'
return (platform === 'kimi' || platform === 'zhipu' || platform === 'minimax') && accountMode === 'coding'
}
export function cnBalanceCellVisible(platform: string, accountMode: string): boolean {
@@ -75,6 +75,7 @@ import {
PROVIDER_KIMI,
PROVIDER_ZHIPU,
PROVIDER_DEEPSEEK,
PROVIDER_MINIMAX,
} from '@/constants/channelMonitor'
defineProps<{
@@ -104,6 +105,7 @@ const providerFilterOptions = computed(() => [
{ value: PROVIDER_KIMI, label: t('monitorCommon.providers.kimi') },
{ value: PROVIDER_ZHIPU, label: t('monitorCommon.providers.zhipu') },
{ value: PROVIDER_DEEPSEEK, label: t('monitorCommon.providers.deepseek') },
{ value: PROVIDER_MINIMAX, label: t('monitorCommon.providers.minimax') },
])
const enabledFilterOptions = computed(() => [
@@ -269,6 +269,7 @@ import {
PROVIDER_KIMI,
PROVIDER_ZHIPU,
PROVIDER_DEEPSEEK,
PROVIDER_MINIMAX,
API_MODE_CHAT_COMPLETIONS,
API_MODE_RESPONSES,
CHECK_MODE_PROBE,
@@ -279,6 +280,7 @@ import {
DEFAULT_KIMI_ENDPOINT,
DEFAULT_ZHIPU_ENDPOINT,
DEFAULT_DEEPSEEK_ENDPOINT,
DEFAULT_MINIMAX_ENDPOINT,
DEFAULT_INTERVAL_SECONDS,
} from '@/constants/channelMonitor'
@@ -473,6 +475,7 @@ const providerOptions = computed<ProviderOption[]>(() => [
{ value: PROVIDER_KIMI, label: t('monitorCommon.providers.kimi') },
{ value: PROVIDER_ZHIPU, label: t('monitorCommon.providers.zhipu') },
{ value: PROVIDER_DEEPSEEK, label: t('monitorCommon.providers.deepseek') },
{ value: PROVIDER_MINIMAX, label: t('monitorCommon.providers.minimax') },
])
// 国产 provider 预填的官方 endpoint(仅探活侧;配额模式 endpoint 可留空)。
@@ -480,6 +483,7 @@ const PROVIDER_DEFAULT_ENDPOINTS: Partial<Record<Provider, string>> = {
[PROVIDER_KIMI]: DEFAULT_KIMI_ENDPOINT,
[PROVIDER_ZHIPU]: DEFAULT_ZHIPU_ENDPOINT,
[PROVIDER_DEEPSEEK]: DEFAULT_DEEPSEEK_ENDPOINT,
[PROVIDER_MINIMAX]: DEFAULT_MINIMAX_ENDPOINT,
}
interface CheckModeOption {
@@ -253,6 +253,7 @@ import {
PROVIDER_KIMI,
PROVIDER_ZHIPU,
PROVIDER_DEEPSEEK,
PROVIDER_MINIMAX,
PROVIDERS,
API_MODE_CHAT_COMPLETIONS,
API_MODE_RESPONSES,
@@ -278,6 +279,7 @@ const providerTabs = computed<{ value: Provider; label: string }[]>(() => [
{ value: PROVIDER_KIMI, label: t('monitorCommon.providers.kimi') },
{ value: PROVIDER_ZHIPU, label: t('monitorCommon.providers.zhipu') },
{ value: PROVIDER_DEEPSEEK, label: t('monitorCommon.providers.deepseek') },
{ value: PROVIDER_MINIMAX, label: t('monitorCommon.providers.minimax') },
])
const activeProvider = ref<Provider>(PROVIDER_ANTHROPIC)
@@ -171,6 +171,9 @@ const labelClass = computed(() => {
if (props.platform === 'deepseek') {
return `${base} bg-teal-200/60 text-teal-800 dark:bg-teal-800/40 dark:text-teal-300`
}
if (props.platform === 'minimax') {
return `${base} bg-rose-200/60 text-rose-800 dark:bg-rose-800/40 dark:text-rose-300`
}
if (props.platform === 'composite') {
return `${base} bg-cyan-200/70 text-cyan-900 dark:bg-cyan-900/50 dark:text-cyan-300`
}
@@ -224,6 +227,11 @@ const badgeClass = computed(() => {
? 'bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-400'
: 'bg-teal-50 text-teal-700 dark:bg-teal-900/20 dark:text-teal-400'
}
if (props.platform === 'minimax') {
return isSubscription.value
? 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400'
: 'bg-rose-50 text-rose-700 dark:bg-rose-900/20 dark:text-rose-400'
}
if (props.platform === 'composite') {
return isSubscription.value
? 'bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300'
@@ -38,6 +38,12 @@
d="M11.991 23.503a.24.24 0 0 0-.244.248a.24.24 0 0 0 .244.249a.24.24 0 0 0 .245-.249a.24.24 0 0 0-.22-.247zM9.671 5.365a1.697 1.697 0 0 1 1.099 2.132l-.071.172l-.016.04l-.018.054c-.07.16-.104.32-.104.498c-.035.71.47 1.279 1.186 1.314h.366c1.309.053 2.338 1.173 2.286 2.523c-.052 1.332-1.152 2.38-2.478 2.327h-.174c-.715.018-1.274.64-1.239 1.368c0 .124.018.23.053.337c.209.373.54.658.96.8c.75.23 1.517-.125 1.9-.782l.018-.035c.402-.64 1.17-.96 1.92-.711c.854.284 1.378 1.226 1.099 2.167a1.66 1.66 0 0 1-2.077 1.102a1.7 1.7 0 0 1-.907-.711l-.017-.035c-.2-.323-.463-.58-.851-.711l-.056-.018a1.646 1.646 0 0 0-1.954.746a1.66 1.66 0 0 1-1.065.764a1.677 1.677 0 0 1-1.989-1.279c-.209-.906.332-1.83 1.257-2.043a1.5 1.5 0 0 1 .296-.035h.018c.68-.071 1.151-.622 1.116-1.333a1.3 1.3 0 0 0-.227-.693a2.5 2.5 0 0 1-.366-1.403a2.4 2.4 0 0 1 .366-1.208c.14-.195.21-.444.227-.693c.018-.71-.506-1.261-1.186-1.332l-.07-.018a1.4 1.4 0 0 1-.299-.07l-.05-.019a1.7 1.7 0 0 1-1.047-2.114a1.68 1.68 0 0 1 2.094-1.101m-5.575 10.11c.26-.264.639-.367.994-.27s.633.379.728.74c.095.362-.007.748-.267 1.013c-.402.41-1.053.41-1.455 0a1.06 1.06 0 0 1 0-1.482zm14.845-.294c.359-.09.738.024.992.297c.254.274.344.665.237 1.025s-.396.634-.756.718c-.551.128-1.1-.22-1.23-.781a1.05 1.05 0 0 1 .757-1.26zm-.064-4.39c.314.32.49.753.49 1.206s-.176.886-.49 1.206c-.315.32-.74.5-1.185.5c-.444 0-.87-.18-1.184-.5a1.727 1.727 0 0 1 0-2.412a1.654 1.654 0 0 1 2.369 0m-11.243.163c.364.484.447 1.128.218 1.691a1.665 1.665 0 0 1-2.188.923c-.855-.36-1.26-1.358-.907-2.228a1.68 1.68 0 0 1 1.33-1.038a1.66 1.66 0 0 1 1.547.652m11.545-4.221c.368 0 .708.2.892.524s.184.724 0 1.048a1.03 1.03 0 0 1-.892.524a1.04 1.04 0 0 1-1.03-1.048a1.04 1.04 0 0 1 1.03-1.048m-14.358 0c.368 0 .707.2.891.524s.184.724 0 1.048a1.03 1.03 0 0 1-.891.524a1.04 1.04 0 0 1-1.03-1.048c0-.579.461-1.048 1.03-1.048m10.031-1.475c.925 0 1.675.764 1.675 1.706s-.75 1.705-1.675 1.705s-1.674-.763-1.674-1.705s.75-1.706 1.674-1.706m-2.626-.684c.362-.082.653-.356.761-.718a1.06 1.06 0 0 0-.238-1.028a1.02 1.02 0 0 0-.996-.294c-.547.14-.881.7-.752 1.257c.13.558.675.907 1.225.783m0 16.876c.359-.087.644-.36.75-.72a1.06 1.06 0 0 0-.237-1.019a1.02 1.02 0 0 0-.985-.301a1.04 1.04 0 0 0-.762.717c-.108.361-.017.754.239 1.028c.245.263.606.377.953.305zM17.19 3.5a.63.63 0 0 0 .628-.64a.63.63 0 0 0-.628-.64a.63.63 0 0 0-.628.64c0 .355.28.64.628.64m-10.38 0a.63.63 0 0 0 .628-.64c0-.355-.28-.64-.628-.64a.63.63 0 0 0-.628.64c0 .355.279.64.628.64m-5.182 7.852a.63.63 0 0 0-.628.64c0 .354.28.639.628.639a.63.63 0 0 0 .627-.606l.001-.034a.62.62 0 0 0-.628-.64zm5.182 9.13a.63.63 0 0 0-.628.64c0 .355.279.64.628.64a.63.63 0 0 0 .628-.64c0-.355-.28-.64-.628-.64m10.38.018a.63.63 0 0 0-.628.64c0 .355.28.64.628.64a.63.63 0 0 0 .628-.64a.63.63 0 0 0-.628-.64m5.182-9.148a.63.63 0 0 0-.628.64c0 .354.279.639.628.639a.63.63 0 0 0 .628-.64c0-.355-.28-.64-.628-.64zm-.384-4.992a.24.24 0 0 0 .244-.249a.24.24 0 0 0-.244-.249a.24.24 0 0 0-.244.249c0 .142.122.249.244.249M11.991.497a.24.24 0 0 0 .245-.248A.24.24 0 0 0 11.99 0a.24.24 0 0 0-.244.249c0 .133.108.236.223.247zM2.011 6.36a.24.24 0 0 0 .245-.249a.24.24 0 0 0-.244-.249a.24.24 0 0 0-.244.249a.24.24 0 0 0 .244.249zm0 11.263a.24.24 0 0 0-.243.248a.24.24 0 0 0 .244.249a.24.24 0 0 0 .244-.249a.25.25 0 0 0-.244-.248zm19.995-.018a.24.24 0 0 0-.245.248a.24.24 0 0 0 .245.25a.24.24 0 0 0 .244-.25a.25.25 0 0 0-.244-.248z"
/>
</svg>
<!-- MiniMax official logo mark (overlapping M bars) -->
<svg v-else-if="platform === 'minimax'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor">
<path
d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z"
/>
</svg>
<!-- DeepSeek official logo mark (whale) -->
<svg v-else-if="platform === 'deepseek'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor">
<path
@@ -68,6 +68,7 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { AccountPlatform, AccountType } from '@/types'
import { platformLabel as sharedPlatformLabel } from '@/utils/platformColors'
import GrokFreeIcon from './GrokFreeIcon.vue'
import PlatformIcon from './PlatformIcon.vue'
import Icon from '@/components/icons/Icon.vue'
@@ -85,16 +86,7 @@ interface Props {
const props = defineProps<Props>()
const platformLabel = computed(() => {
if (props.platform === 'anthropic') return 'Anthropic'
if (props.platform === 'openai') return 'OpenAI'
if (props.platform === 'antigravity') return 'Antigravity'
if (props.platform === 'grok') return 'Grok'
if (props.platform === 'kimi') return 'Kimi'
if (props.platform === 'zhipu') return 'Zhipu GLM'
if (props.platform === 'deepseek') return 'DeepSeek'
return 'Gemini'
})
const platformLabel = computed(() => sharedPlatformLabel(props.platform))
const normalizedAuthMode = computed(() =>
(props.authMode || '').trim().toLowerCase().replace(/[\s_-]+/g, '')
@@ -200,6 +192,9 @@ const platformClass = computed(() => {
if (props.platform === 'deepseek') {
return 'bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-400'
}
if (props.platform === 'minimax') {
return 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400'
}
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
})
@@ -225,6 +220,9 @@ const typeClass = computed(() => {
if (props.platform === 'deepseek') {
return 'bg-teal-100 text-teal-600 dark:bg-teal-900/30 dark:text-teal-400'
}
if (props.platform === 'minimax') {
return 'bg-rose-100 text-rose-600 dark:bg-rose-900/30 dark:text-rose-400'
}
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
})
@@ -114,3 +114,19 @@ describe('PlatformTypeBadge OpenAI authentication modes', () => {
expect(wrapper.text()).toContain('OAuth')
})
})
describe('PlatformTypeBadge MiniMax', () => {
it('labels MiniMax API keys as MiniMax, not Gemini', () => {
const wrapper = mount(PlatformTypeBadge, {
props: {
platform: 'minimax',
type: 'apikey',
},
})
expect(wrapper.text()).toContain('MiniMax')
expect(wrapper.text()).toContain('Key')
expect(wrapper.text()).not.toContain('Gemini')
expect(wrapper.html()).toContain('bg-rose-100')
})
})
+19 -1
View File
@@ -469,6 +469,7 @@ const clientTabs = computed((): TabConfig[] => {
{ id: 'opencode', label: t('keys.useKeyModal.cliTabs.opencode'), icon: TerminalIcon }
]
case 'deepseek':
case 'minimax':
case 'composite':
return [
{ id: 'claude', label: t('keys.useKeyModal.cliTabs.claudeCode'), icon: TerminalIcon },
@@ -517,6 +518,7 @@ const platformDescription = computed(() => {
props.platform !== 'openai' &&
props.platform !== 'grok' &&
props.platform !== 'deepseek' &&
props.platform !== 'minimax' &&
props.platform !== 'composite') {
return t('keys.useKeyModal.routedCodex.description')
}
@@ -542,6 +544,10 @@ const platformDescription = computed(() => {
return activeClientTab.value === 'codex'
? t('keys.useKeyModal.deepseek.codexDescription')
: t('keys.useKeyModal.deepseek.description')
case 'minimax':
return activeClientTab.value === 'codex'
? t('keys.useKeyModal.minimax.codexDescription')
: t('keys.useKeyModal.minimax.description')
case 'composite':
return activeClientTab.value === 'codex'
? t('keys.useKeyModal.composite.codexDescription')
@@ -556,6 +562,7 @@ const platformNote = computed(() => {
props.platform !== 'openai' &&
props.platform !== 'grok' &&
props.platform !== 'deepseek' &&
props.platform !== 'minimax' &&
props.platform !== 'composite') {
return t('keys.useKeyModal.routedCodex.note')
}
@@ -594,6 +601,10 @@ const platformNote = computed(() => {
return activeClientTab.value === 'codex'
? t('keys.useKeyModal.deepseek.codexNote')
: t('keys.useKeyModal.note')
case 'minimax':
return activeClientTab.value === 'codex'
? t('keys.useKeyModal.minimax.codexNote')
: t('keys.useKeyModal.note')
case 'composite':
return activeClientTab.value === 'codex'
? t('keys.useKeyModal.composite.codexNote')
@@ -757,6 +768,11 @@ const currentFiles = computed((): FileConfig[] => {
return generateRoutedCodexFiles(apiBase, apiKey, 'deepseek')
}
return generateAnthropicFiles(baseRoot, apiKey)
case 'minimax':
if (activeClientTab.value === 'codex') {
return generateRoutedCodexFiles(apiBase, apiKey, 'minimax')
}
return generateAnthropicFiles(baseRoot, apiKey)
case 'composite':
if (activeClientTab.value === 'codex') {
return generateRoutedCodexFiles(apiBase, apiKey, 'composite')
@@ -1219,6 +1235,7 @@ function generateRoutedCodexFiles(
kimi: 'kimi-k2.5',
zhipu: 'glm-4.7',
deepseek: 'deepseek-v4-pro',
minimax: 'MiniMax-M3',
composite: 'gpt-5.5'
}
const preferredModel = preferredModels[platform] || ''
@@ -1232,6 +1249,7 @@ function generateRoutedCodexFiles(
kimi: 'Kimi',
zhipu: 'Zhipu',
deepseek: 'DeepSeek',
minimax: 'MiniMax',
composite: 'Composite'
}
const label = labels[platform]
@@ -1260,7 +1278,7 @@ supports_websockets = false`
path: joinConfigPath(configDir, 'config.toml', isWindows),
content: configContent,
hint: t(
platform === 'deepseek' || platform === 'composite'
platform === 'deepseek' || platform === 'minimax' || platform === 'composite'
? `keys.useKeyModal.${platform}.codexConfigTomlHint`
: 'keys.useKeyModal.routedCodex.configTomlHint'
)
@@ -796,7 +796,7 @@ describe('UseKeyModal', () => {
)
})
it.each(['anthropic', 'gemini', 'antigravity', 'kimi', 'zhipu'] as const)(
it.each(['anthropic', 'gemini', 'antigravity', 'kimi', 'zhipu', 'minimax'] as const)(
'offers Codex catalog configuration for the %s routed group',
async (platform) => {
const wrapper = mount(UseKeyModal, {
@@ -94,7 +94,12 @@ const PLATFORM_LABELS: Record<string, string> = {
anthropic: 'Claude',
openai: 'OpenAI',
gemini: 'Gemini',
antigravity: 'Antigravity'
antigravity: 'Antigravity',
grok: 'Grok',
kimi: 'Kimi',
zhipu: 'Zhipu GLM',
deepseek: 'DeepSeek',
minimax: 'MiniMax',
}
function platformLabel(platform: string): string {
@@ -251,7 +251,12 @@ const PLATFORM_LABELS: Record<string, string> = {
anthropic: 'Claude',
openai: 'OpenAI',
gemini: 'Gemini',
antigravity: 'Antigravity'
antigravity: 'Antigravity',
grok: 'Grok',
kimi: 'Kimi',
zhipu: 'Zhipu GLM',
deepseek: 'DeepSeek',
minimax: 'MiniMax',
}
const platformLabel = (p: string) => PLATFORM_LABELS[p] ?? p
@@ -78,6 +78,11 @@ const PROVIDER_ICONS: Record<Provider, IconData> = {
'M23.748 4.651c-.254-.124-.364.113-.512.233-.051.04-.094.09-.137.137-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.155-.708-.311-.955-.65-.172-.24-.219-.509-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.094.172.187.129.323-.082.28-.18.553-.266.833-.055.179-.137.218-.328.14a5.5 5.5 0 0 1-1.737-1.179c-.857-.828-1.631-1.743-2.597-2.46a12 12 0 0 0-.689-.47c-.985-.957.13-1.743.387-1.836.27-.098.094-.433-.778-.428-.872.003-1.67.295-2.687.685a3 3 0 0 1-.465.136a9.6 9.6 0 0 0-2.883-.101c-1.885.21-3.39 1.1-4.497 2.622C.082 8.776-.231 10.854.152 13.02c.403 2.284 1.568 4.175 3.36 5.653 1.857 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.132-.284 4.994-1.86.47.234.962.328 1.78.398.629.058 1.235-.031 1.705-.129.735-.155.684-.836.418-.961-2.155-1.004-1.682-.595-2.112-.926 1.095-1.295 2.768-3.598 3.284-6.733.05-.346.115-.834.108-1.114-.004-.171.035-.238.23-.257a4.2 4.2 0 0 0 1.545-.475c1.397-.763 1.96-2.016 2.093-3.517.02-.23-.004-.467-.247-.588M11.58 18.168c-2.088-1.642-3.101-2.183-3.52-2.16-.39.024-.32.472-.234.763.09.288.207.487.371.74.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.168-1.361-.801-2.5-1.86-3.301-3.306-.775-1.393-1.225-2.888-1.299-4.482-.02-.385.094-.522.477-.592a4.7 4.7 0 0 1 1.53-.038c2.131.311 3.946 1.264 5.467 2.774.868.86 1.525 1.887 2.202 2.89.72 1.066 1.494 2.082 2.48 2.915.348.291.626.513.892.677-.802.09-2.14.109-3.055-.615zm1.001-6.44a.306.306 0 0 1 .415-.287a.3.3 0 0 1 .113.074a.3.3 0 0 1 .086.214c0 .17-.136.307-.308.307a.303.303 0 0 1-.306-.307m3.11 1.596c-.2.081-.4.151-.591.16a1.25 1.25 0 0 1-.798-.254c-.274-.23-.47-.358-.551-.758a1.7 1.7 0 0 1 .015-.588c.07-.327-.007-.537-.238-.727-.188-.156-.426-.199-.689-.199a.6.6 0 0 1-.254-.078a.253.253 0 0 1-.114-.358a1 1 0 0 1 .192-.21c.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.392.451.462.576.685.915.176.264.336.536.446.848.066.194-.02.353-.25.45',
],
},
minimax: {
paths: [
'M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z',
],
},
}
const props = withDefaults(defineProps<{
@@ -21,6 +21,7 @@ import {
PROVIDER_KIMI,
PROVIDER_ZHIPU,
PROVIDER_DEEPSEEK,
PROVIDER_MINIMAX,
PROVIDERS,
STATUS_OPERATIONAL,
STATUS_DEGRADED,
@@ -114,6 +115,8 @@ export function useChannelMonitorFormat() {
return 'bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300'
case PROVIDER_DEEPSEEK:
return 'bg-teal-100 text-teal-700 dark:bg-teal-500/15 dark:text-teal-300'
case PROVIDER_MINIMAX:
return 'bg-rose-100 text-rose-700 dark:bg-rose-500/15 dark:text-rose-300'
default:
return NEUTRAL_BADGE
}
@@ -174,6 +177,10 @@ export function useChannelMonitorFormat() {
return active
? 'border-teal-500 bg-teal-50 text-teal-700 dark:bg-teal-500/15 dark:text-teal-300 dark:border-teal-400'
: 'border-gray-200 bg-white text-gray-600 hover:border-teal-300 hover:text-teal-700 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400 dark:hover:border-teal-500/50'
case PROVIDER_MINIMAX:
return active
? 'border-rose-500 bg-rose-50 text-rose-700 dark:bg-rose-500/15 dark:text-rose-300 dark:border-rose-400'
: 'border-gray-200 bg-white text-gray-600 hover:border-rose-300 hover:text-rose-700 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400 dark:hover:border-rose-500/50'
default:
return active
? 'border-gray-400 bg-gray-50 text-gray-700 dark:border-dark-500 dark:bg-dark-700 dark:text-gray-200'
@@ -258,6 +265,8 @@ export function providerGradient(provider: string): string {
return 'bg-gradient-to-br from-indigo-50 to-indigo-100 dark:from-indigo-500/10 dark:to-indigo-500/20'
case PROVIDER_DEEPSEEK:
return 'bg-gradient-to-br from-teal-50 to-teal-100 dark:from-teal-500/10 dark:to-teal-500/20'
case PROVIDER_MINIMAX:
return 'bg-gradient-to-br from-rose-50 to-rose-100 dark:from-rose-500/10 dark:to-rose-500/20'
default:
return 'bg-gradient-to-br from-gray-100 to-gray-200 dark:from-dark-700 dark:to-dark-600'
}
@@ -203,6 +203,14 @@ const doubaoModels = [
// MiniMax
const minimaxModels = [
'MiniMax-M3',
'MiniMax-M2.7',
'MiniMax-M2.7-highspeed',
'MiniMax-M2.5',
'MiniMax-M2.5-highspeed',
'MiniMax-M2.1',
'MiniMax-M2.1-highspeed',
'MiniMax-M2',
'abab6.5-chat', 'abab6.5s-chat', 'abab6.5s-chat-pro',
'abab6-chat',
'abab5.5-chat', 'abab5.5s-chat'
@@ -9,7 +9,8 @@ const concretePlatforms = [
'grok',
'kimi',
'zhipu',
'deepseek'
'deepseek',
'minimax'
]
describe('platform option catalogs', () => {
+3
View File
@@ -17,6 +17,7 @@ export const PROVIDER_ANTIGRAVITY: Provider = 'antigravity'
export const PROVIDER_KIMI: Provider = 'kimi'
export const PROVIDER_ZHIPU: Provider = 'zhipu'
export const PROVIDER_DEEPSEEK: Provider = 'deepseek'
export const PROVIDER_MINIMAX: Provider = 'minimax'
export const DEFAULT_GROK_ENDPOINT = 'https://api.x.ai'
export const DEFAULT_GROK_MODEL = 'grok-4.5'
@@ -25,6 +26,7 @@ export const DEFAULT_GROK_MODEL = 'grok-4.5'
export const DEFAULT_KIMI_ENDPOINT = 'https://api.moonshot.cn'
export const DEFAULT_ZHIPU_ENDPOINT = 'https://open.bigmodel.cn'
export const DEFAULT_DEEPSEEK_ENDPOINT = 'https://api.deepseek.com'
export const DEFAULT_MINIMAX_ENDPOINT = 'https://api.minimaxi.com'
export const CHECK_MODE_PROBE: CheckMode = 'probe'
export const CHECK_MODE_QUOTA: CheckMode = 'quota'
@@ -42,6 +44,7 @@ export const PROVIDERS: readonly Provider[] = [
PROVIDER_KIMI,
PROVIDER_ZHIPU,
PROVIDER_DEEPSEEK,
PROVIDER_MINIMAX,
]
/** 仅支持配额模式(无探活 adapter)的 provider。 */
+2 -1
View File
@@ -18,7 +18,8 @@ export const CONCRETE_PLATFORM_OPTIONS = [
{ value: 'grok', label: 'Grok' },
{ value: 'kimi', label: 'Kimi' },
{ value: 'zhipu', label: 'Zhipu GLM' },
{ value: 'deepseek', label: 'DeepSeek' }
{ value: 'deepseek', label: 'DeepSeek' },
{ value: 'minimax', label: 'MiniMax' }
] as const satisfies readonly PlatformOption<AccountPlatform>[]
/** Platforms that can own a group. */
@@ -387,6 +387,10 @@ function platformLabel(value: string) {
kiro: 'Kiro',
gemini: 'Gemini',
antigravity: 'Antigravity',
kimi: 'Kimi',
zhipu: 'Zhipu GLM',
deepseek: 'DeepSeek',
minimax: 'MiniMax',
composite: 'Composite',
} as Record<string, string>
)[value] || value
@@ -107,6 +107,7 @@ export default {
kimi: 'Kimi',
zhipu: 'Zhipu GLM',
deepseek: 'DeepSeek',
minimax: 'MiniMax',
},
cnProviders: {
accountMode: {
@@ -973,6 +973,7 @@ export default {
kimi: 'Kimi',
zhipu: 'Zhipu GLM',
deepseek: 'DeepSeek',
minimax: 'MiniMax',
composite: 'Composite',
},
deleteConfirm:
+8 -1
View File
@@ -194,6 +194,12 @@ export default {
codexConfigTomlHint: 'Download the model catalog below, save both files under the Codex config directory, and restart Codex.',
codexNote: 'Export SUB2API_API_KEY before starting Codex. The downloaded catalog contains model metadata only, not your API key.',
},
minimax: {
description: 'Configure Claude Code, Codex, or OpenCode through the current MiniMax group.',
codexDescription: 'Configure Codex with API key authentication through the current MiniMax group.',
codexConfigTomlHint: 'Download the model catalog below, save both files under the Codex config directory, and restart Codex.',
codexNote: 'Export SUB2API_API_KEY before starting Codex. The downloaded catalog contains model metadata only, not your API key.',
},
composite: {
description: 'Configure supported clients through the current Composite routing group.',
codexDescription: 'Configure Codex with API key authentication and the complete model catalog for this Composite group.',
@@ -466,7 +472,8 @@ export default {
antigravity: 'Antigravity',
kimi: 'Kimi',
zhipu: 'Zhipu GLM',
deepseek: 'DeepSeek'
deepseek: 'DeepSeek',
minimax: 'MiniMax'
},
// Check modes (how a monitor performs its checks)
checkMode: {
@@ -310,6 +310,7 @@ export default {
kimi: 'Kimi',
zhipu: 'Zhipu GLM',
deepseek: 'DeepSeek',
minimax: 'MiniMax',
},
cnProviders: {
accountMode: {
@@ -906,6 +906,7 @@ export default {
kimi: 'Kimi',
zhipu: 'Zhipu GLM',
deepseek: 'DeepSeek',
minimax: 'MiniMax',
composite: 'Composite',
},
saving: '保存中...',
+8 -1
View File
@@ -198,6 +198,12 @@ export default {
codexConfigTomlHint: '下载下方模型目录,将两个文件保存到 Codex 配置目录后重启 Codex。',
codexNote: '启动 Codex 前先导出 SUB2API_API_KEY。下载的目录只包含模型元数据,不包含 API Key。'
},
minimax: {
description: '通过当前 MiniMax 分组配置 Claude Code、Codex 或 OpenCode。',
codexDescription: '使用 API Key 配置 Codex,并通过当前 MiniMax 分组发送请求。',
codexConfigTomlHint: '下载下方模型目录,将两个文件保存到 Codex 配置目录后重启 Codex。',
codexNote: '启动 Codex 前先导出 SUB2API_API_KEY。下载的目录只包含模型元数据,不包含 API Key。'
},
composite: {
description: '通过当前 Composite 路由分组配置受支持的客户端。',
codexDescription: '使用 API Key 和当前 Composite 分组的完整模型目录配置 Codex。',
@@ -471,7 +477,8 @@ export default {
antigravity: 'Antigravity',
kimi: 'Kimi',
zhipu: '智谱 GLM',
deepseek: 'DeepSeek'
deepseek: 'DeepSeek',
minimax: 'MiniMax'
},
// 检查模式(监控条目的工作方式)
checkMode: {
+2 -2
View File
@@ -531,7 +531,7 @@ export interface PaginationConfig {
// ==================== API Key & Group Types ====================
export type GroupPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'grok' | 'kimi' | 'zhipu' | 'deepseek' | 'composite'
export type GroupPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'grok' | 'kimi' | 'zhipu' | 'deepseek' | 'minimax' | 'composite'
export type VideoModelPrices = Record<string, Record<string, number>>
@@ -911,7 +911,7 @@ export interface UpdateGroupRequest {
// ==================== Account & Proxy Types ====================
export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'grok' | 'kimi' | 'zhipu' | 'deepseek'
export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'grok' | 'kimi' | 'zhipu' | 'deepseek' | 'minimax'
export type AccountType = 'oauth' | 'setup-token' | 'apikey' | 'upstream' | 'bedrock' | 'service_account'
export type OAuthAddMethod = 'oauth' | 'setup-token'
export type ProxyProtocol = 'http' | 'https' | 'socks5' | 'socks5h'
+16
View File
@@ -14,6 +14,7 @@ export type Platform =
| 'kimi'
| 'zhipu'
| 'deepseek'
| 'minimax'
| 'composite'
// ── Badge (bg + text + border, for inline badges with border) ───────
@@ -26,6 +27,7 @@ const BADGE: Record<Platform, string> = {
kimi: 'bg-pink-500/10 text-pink-600 border-pink-500/30 dark:text-pink-400',
zhipu: 'bg-indigo-500/10 text-indigo-600 border-indigo-500/30 dark:text-indigo-400',
deepseek: 'bg-teal-500/10 text-teal-600 border-teal-500/30 dark:text-teal-400',
minimax: 'bg-rose-500/10 text-rose-600 border-rose-500/30 dark:text-rose-400',
composite: 'bg-cyan-500/10 text-cyan-700 border-cyan-500/30 dark:text-cyan-300',
}
const BADGE_DEFAULT = 'bg-slate-500/10 text-slate-600 border-slate-500/30 dark:text-slate-400'
@@ -40,6 +42,7 @@ const BADGE_LIGHT: Record<Platform, string> = {
kimi: 'bg-pink-500/10 text-pink-600 dark:bg-pink-500/10 dark:text-pink-300',
zhipu: 'bg-indigo-500/10 text-indigo-600 dark:bg-indigo-500/10 dark:text-indigo-300',
deepseek: 'bg-teal-500/10 text-teal-600 dark:bg-teal-500/10 dark:text-teal-300',
minimax: 'bg-rose-500/10 text-rose-600 dark:bg-rose-500/10 dark:text-rose-300',
composite: 'bg-cyan-500/10 text-cyan-700 dark:bg-cyan-500/10 dark:text-cyan-300',
}
@@ -53,6 +56,7 @@ const BORDER: Record<Platform, string> = {
kimi: 'border-pink-500/20 dark:border-pink-500/20',
zhipu: 'border-indigo-500/20 dark:border-indigo-500/20',
deepseek: 'border-teal-500/20 dark:border-teal-500/20',
minimax: 'border-rose-500/20 dark:border-rose-500/20',
composite: 'border-cyan-500/20 dark:border-cyan-500/20',
}
const BORDER_DEFAULT = 'border-gray-200 dark:border-dark-700'
@@ -67,6 +71,7 @@ const BORDER_STRONG: Record<Platform, string> = {
kimi: 'border-pink-500/35 dark:border-pink-500/30',
zhipu: 'border-indigo-500/35 dark:border-indigo-500/30',
deepseek: 'border-teal-500/35 dark:border-teal-500/30',
minimax: 'border-rose-500/35 dark:border-rose-500/30',
composite: 'border-cyan-500/35 dark:border-cyan-500/30',
}
const BORDER_STRONG_DEFAULT = 'border-gray-300 dark:border-dark-600'
@@ -82,6 +87,7 @@ const ACCENT: Record<Platform, string> = {
kimi: '#ec4899', // pink-500
zhipu: '#6366f1', // indigo-500
deepseek: '#14b8a6', // teal-500
minimax: '#f43f5e', // rose-500
composite: '#06b6d4', // cyan-500
}
const ACCENT_DEFAULT = '#14b8a6' // primary-500 (teal)
@@ -96,6 +102,7 @@ const ACCENT_BAR: Record<Platform, string> = {
kimi: 'bg-gradient-to-r from-pink-400 to-pink-500',
zhipu: 'bg-gradient-to-r from-indigo-400 to-indigo-500',
deepseek: 'bg-gradient-to-r from-teal-400 to-teal-500',
minimax: 'bg-gradient-to-r from-rose-400 to-rose-500',
composite: 'bg-gradient-to-r from-slate-500 to-cyan-500',
}
const ACCENT_BAR_DEFAULT = 'bg-gradient-to-r from-primary-400 to-primary-500'
@@ -110,6 +117,7 @@ const TEXT: Record<Platform, string> = {
kimi: 'text-pink-600 dark:text-pink-400',
zhipu: 'text-indigo-600 dark:text-indigo-400',
deepseek: 'text-teal-600 dark:text-teal-400',
minimax: 'text-rose-600 dark:text-rose-400',
composite: 'text-cyan-700 dark:text-cyan-300',
}
const TEXT_DEFAULT = 'text-primary-600 dark:text-primary-400'
@@ -124,6 +132,7 @@ const ICON: Record<Platform, string> = {
kimi: 'text-pink-500 dark:text-pink-400',
zhipu: 'text-indigo-500 dark:text-indigo-400',
deepseek: 'text-teal-500 dark:text-teal-400',
minimax: 'text-rose-500 dark:text-rose-400',
composite: 'text-cyan-600 dark:text-cyan-300',
}
const ICON_DEFAULT = 'text-primary-500 dark:text-primary-400'
@@ -138,6 +147,7 @@ const BUTTON: Record<Platform, string> = {
kimi: 'bg-pink-500 text-white hover:bg-pink-600 active:bg-pink-700 dark:bg-pink-500/80 dark:hover:bg-pink-500',
zhipu: 'bg-indigo-500 text-white hover:bg-indigo-600 active:bg-indigo-700 dark:bg-indigo-500/80 dark:hover:bg-indigo-500',
deepseek: 'bg-teal-500 text-white hover:bg-teal-600 active:bg-teal-700 dark:bg-teal-500/80 dark:hover:bg-teal-500',
minimax: 'bg-rose-500 text-white hover:bg-rose-600 active:bg-rose-700 dark:bg-rose-500/80 dark:hover:bg-rose-500',
composite: 'bg-cyan-700 text-white hover:bg-cyan-800 active:bg-cyan-900 dark:bg-cyan-600 dark:hover:bg-cyan-500',
}
const BUTTON_DEFAULT = 'bg-primary-500 text-white hover:bg-primary-600 dark:bg-primary-600 dark:hover:bg-primary-500'
@@ -152,6 +162,7 @@ const DISCOUNT: Record<Platform, string> = {
kimi: 'bg-pink-100 text-pink-700 dark:bg-pink-900/40 dark:text-pink-300',
zhipu: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300',
deepseek: 'bg-teal-100 text-teal-700 dark:bg-teal-900/40 dark:text-teal-300',
minimax: 'bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300',
composite: 'bg-cyan-100 text-cyan-800 dark:bg-cyan-900/40 dark:text-cyan-300',
}
const DISCOUNT_DEFAULT = 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
@@ -166,6 +177,7 @@ const GRADIENT: Record<Platform, string> = {
kimi: 'from-pink-500 to-pink-600',
zhipu: 'from-indigo-500 to-indigo-600',
deepseek: 'from-teal-500 to-teal-600',
minimax: 'from-rose-500 to-rose-600',
composite: 'from-slate-600 to-cyan-600',
}
const GRADIENT_DEFAULT = 'from-primary-500 to-primary-600'
@@ -180,6 +192,7 @@ const GRADIENT_TEXT: Record<Platform, string> = {
kimi: 'text-pink-100',
zhipu: 'text-indigo-100',
deepseek: 'text-teal-100',
minimax: 'text-rose-100',
composite: 'text-cyan-100',
}
const GRADIENT_TEXT_DEFAULT = 'text-primary-100'
@@ -193,6 +206,7 @@ const GRADIENT_SUBTEXT: Record<Platform, string> = {
kimi: 'text-pink-200',
zhipu: 'text-indigo-200',
deepseek: 'text-teal-200',
minimax: 'text-rose-200',
composite: 'text-cyan-200',
}
const GRADIENT_SUBTEXT_DEFAULT = 'text-primary-200'
@@ -209,6 +223,7 @@ function isPlatform(p: string): p is Platform {
p === 'kimi' ||
p === 'zhipu' ||
p === 'deepseek' ||
p === 'minimax' ||
p === 'composite'
)
}
@@ -275,6 +290,7 @@ export function platformLabel(p: string): string {
case 'kimi': return 'Kimi'
case 'zhipu': return 'Zhipu GLM'
case 'deepseek': return 'DeepSeek'
case 'minimax': return 'MiniMax'
case 'composite': return 'Composite'
default: return p || 'API'
}
+2 -2
View File
@@ -763,9 +763,9 @@ const form = reactive({
let abortController: AbortController | null = null
// ── Platform config ──
const platformOrder: GroupPlatform[] = ['anthropic', 'openai', 'gemini', 'antigravity', 'grok', 'kimi', 'zhipu', 'deepseek']
const platformOrder: GroupPlatform[] = ['anthropic', 'openai', 'gemini', 'antigravity', 'grok', 'kimi', 'zhipu', 'deepseek', 'minimax']
// Composite pricing/mapping may target every concrete schedulable provider.
const compositePlatforms: GroupPlatform[] = ['anthropic', 'openai', 'gemini', 'antigravity', 'grok', 'kimi', 'zhipu', 'deepseek']
const compositePlatforms: GroupPlatform[] = ['anthropic', 'openai', 'gemini', 'antigravity', 'grok', 'kimi', 'zhipu', 'deepseek', 'minimax']
// ── Helpers ──
function formatDate(value: string): string {
+4
View File
@@ -154,6 +154,8 @@
? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400'
: value === 'deepseek'
? 'bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-400'
: value === 'minimax'
? 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400'
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
]"
>
@@ -3766,6 +3768,8 @@
? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400'
: group.platform === 'deepseek'
? 'bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-400'
: group.platform === 'minimax'
? 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400'
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
]"
>
+8
View File
@@ -1786,6 +1786,14 @@ const qualityTargetLabel = (target: string) => {
return 'Gemini'
case 'grok':
return 'Grok'
case 'kimi':
return 'Kimi'
case 'zhipu':
return 'Zhipu GLM'
case 'deepseek':
return 'DeepSeek'
case 'minimax':
return 'MiniMax'
default:
return target
}
@@ -4,7 +4,7 @@ import { CONCRETE_PLATFORM_OPTIONS } from '@/constants/platforms'
describe('GroupsView Composite route options', () => {
it('offers Kimi, Zhipu GLM, and DeepSeek as route targets', () => {
expect(CONCRETE_PLATFORM_OPTIONS.map((option) => option.value)).toEqual(
expect.arrayContaining(['kimi', 'zhipu', 'deepseek'])
expect.arrayContaining(['kimi', 'zhipu', 'deepseek', 'minimax'])
)
})
})

Some files were not shown because too many files have changed in this diff Show More