mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-06 00:48:36 +00: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:
1 parent
909b8c7ef4
commit
19382f275e
101 files changed
+704
-151
No files matched your search
@@ -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)
|
||||
if err != nil {
|
||||
log.Printf("[CNBalance] list %s accounts failed: %v", PlatformZhipu, err)
|
||||
} else {
|
||||
collect(PlatformZhipu, accounts)
|
||||
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", 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},
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user