mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
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:
@@ -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)
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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'))")
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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。 */
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: '保存中...',
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
]"
|
||||
>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user