mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
refactor(platform): 后端平台列表改由平台清单派生
新增 domain/platforms.go 作为具体平台的单一数据来源:每个平台一条登记, 写明展示名、所走网关(anthropic / openai / gemini / antigravity)、是否国产 单一厂商、定价目录 provider 名,顺序即展示顺序。 以下列表与判断改为查平台清单: - "全部具体平台":用户平台额度白名单、错误透传规则平台列表、组合路由目标 校验、上游计费探测资格、调度快照遍历、组合分组的定价 / 模型映射回退、 组合分组默认模型列表; - "OpenAI 网关族":IsOpenAICompatible、NormalizeOpenAICompatiblePlatform、 Responses 兼容路由判断、count_tokens 分发、组合分组的 OpenAI 文本目标; - 国产厂商判断、渠道定价同步的 provider 映射; - 多协议 API Key 供应商相关的请求头覆盖资格、凭证测试、OpenAI 基址回退改用 IsMultiProtocolAPIKeyProvider; - 分组平台与组合路由目标的请求校验改为自定义校验标签 group_platform / concrete_platform,拒绝规则不变(大小写、首尾空格、别名一律拒绝)。 组合分组的逐平台回退是先命中者生效,CompositePrecedencePlatformIDs 保持 原有顺序 anthropic、gemini、openai,其余按登记顺序追加。依赖具体实现的 能力(OAuth、渠道监控、调度阈值、余额 / 用量窗口、Codex 模型目录等)仍在 各自代码中显式列出。 等价性测试把改动前的每个列表与 switch 原样保留为基准,逐一比较派生结果 (含顺序),谓词覆盖全部平台、composite 与非法值 × 全部账号类型。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
105c955c88
commit
3b89cc08dd
+1
-1
@@ -19,6 +19,7 @@ require (
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/dgraph-io/ristretto v0.2.0
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-playground/validator/v10 v10.14.0
|
||||
github.com/go-webauthn/webauthn v0.17.4
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/google/uuid v1.6.0
|
||||
@@ -120,7 +121,6 @@ require (
|
||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-webauthn/x v0.2.6 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package domain
|
||||
|
||||
// 平台清单:平台的单一数据来源。"所有平台"类列表(分组平台、用户额度、组合
|
||||
// 路由目标、调度快照、定价匹配等)与"某网关族"类列表都从这里派生;新增平台
|
||||
// 登记一条即可在这些位置生效,无需逐处修改或新增数据库约束迁移。
|
||||
//
|
||||
// 依赖具体实现的能力(OAuth、渠道监控、余额 / Coding Plan 探测、用量窗口、
|
||||
// Codex 模型目录等)仍由各自的代码显式列出,不在此处声明。
|
||||
|
||||
// PlatformGateway 标识平台请求由哪个网关服务处理。
|
||||
type PlatformGateway string
|
||||
|
||||
const (
|
||||
// PlatformGatewayAnthropic 由 GatewayService 处理(Claude Messages 协议,以及 TypeSafe 的 /v1/systemone)。
|
||||
PlatformGatewayAnthropic PlatformGateway = "anthropic"
|
||||
// PlatformGatewayOpenAI 由 OpenAIGatewayService 处理(OpenAI、Grok 与多协议 API Key 供应商)。
|
||||
PlatformGatewayOpenAI PlatformGateway = "openai"
|
||||
// PlatformGatewayGemini 由 Gemini 兼容网关处理。
|
||||
PlatformGatewayGemini PlatformGateway = "gemini"
|
||||
// PlatformGatewayAntigravity 由 Antigravity 网关处理。
|
||||
PlatformGatewayAntigravity PlatformGateway = "antigravity"
|
||||
)
|
||||
|
||||
// PlatformSpec 描述一个具体(非 composite)平台。
|
||||
type PlatformSpec struct {
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Gateway PlatformGateway `json:"gateway"`
|
||||
// CNProvider 标识国产单一厂商(Kimi / 智谱 / DeepSeek / MiniMax),共享国产
|
||||
// 供应商的余额 / Coding Plan 等能力。
|
||||
CNProvider bool `json:"cn_provider"`
|
||||
// LiteLLMProvider 是定价目录中的 provider 名;空表示不支持从定价目录同步模型。
|
||||
LiteLLMProvider string `json:"litellm_provider,omitempty"`
|
||||
}
|
||||
|
||||
// platformList 的顺序即平台的展示顺序。
|
||||
var platformList = []PlatformSpec{
|
||||
{ID: PlatformAnthropic, DisplayName: "Anthropic", Gateway: PlatformGatewayAnthropic, LiteLLMProvider: "anthropic"},
|
||||
{ID: PlatformOpenAI, DisplayName: "OpenAI", Gateway: PlatformGatewayOpenAI, LiteLLMProvider: "openai"},
|
||||
{ID: PlatformGemini, DisplayName: "Gemini", Gateway: PlatformGatewayGemini, LiteLLMProvider: "gemini"},
|
||||
{ID: PlatformAntigravity, DisplayName: "Antigravity", Gateway: PlatformGatewayAntigravity, LiteLLMProvider: "anthropic"},
|
||||
{ID: PlatformGrok, DisplayName: "Grok", Gateway: PlatformGatewayOpenAI, LiteLLMProvider: "xai"},
|
||||
{ID: PlatformKimi, DisplayName: "Kimi", Gateway: PlatformGatewayOpenAI, CNProvider: true, LiteLLMProvider: "moonshot"},
|
||||
{ID: PlatformZhipu, DisplayName: "Zhipu GLM", Gateway: PlatformGatewayOpenAI, CNProvider: true, LiteLLMProvider: "zhipu"},
|
||||
{ID: PlatformDeepseek, DisplayName: "DeepSeek", Gateway: PlatformGatewayOpenAI, CNProvider: true, LiteLLMProvider: "deepseek"},
|
||||
{ID: PlatformMiniMax, DisplayName: "MiniMax", Gateway: PlatformGatewayOpenAI, CNProvider: true, LiteLLMProvider: "minimax"},
|
||||
{ID: PlatformOpenCodeGo, DisplayName: "OpenCode", Gateway: PlatformGatewayOpenAI, LiteLLMProvider: "opencode-go"},
|
||||
{ID: PlatformTypeSafe, DisplayName: "TypeSafe / Jev", Gateway: PlatformGatewayAnthropic, LiteLLMProvider: "typesafe"},
|
||||
}
|
||||
|
||||
var platformIndex = func() map[string]int {
|
||||
index := make(map[string]int, len(platformList))
|
||||
for i, spec := range platformList {
|
||||
index[spec.ID] = i
|
||||
}
|
||||
return index
|
||||
}()
|
||||
|
||||
// Platforms 返回全部具体平台(副本),按展示顺序。
|
||||
func Platforms() []PlatformSpec {
|
||||
out := make([]PlatformSpec, len(platformList))
|
||||
copy(out, platformList)
|
||||
return out
|
||||
}
|
||||
|
||||
// LookupPlatform 返回具体平台的登记信息;composite 与未知值返回 false。
|
||||
func LookupPlatform(id string) (PlatformSpec, bool) {
|
||||
i, ok := platformIndex[id]
|
||||
if !ok {
|
||||
return PlatformSpec{}, false
|
||||
}
|
||||
return platformList[i], true
|
||||
}
|
||||
|
||||
// IsConcretePlatform 报告 id 是否为已登记的具体平台(不含 composite)。
|
||||
func IsConcretePlatform(id string) bool {
|
||||
_, ok := platformIndex[id]
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsGroupPlatform 报告 id 是否可作为分组平台(具体平台或 composite)。
|
||||
func IsGroupPlatform(id string) bool {
|
||||
return id == PlatformComposite || IsConcretePlatform(id)
|
||||
}
|
||||
|
||||
// ConcretePlatformIDs 返回全部具体平台标识,按展示顺序。
|
||||
func ConcretePlatformIDs() []string {
|
||||
return PlatformIDsWhere(func(PlatformSpec) bool { return true })
|
||||
}
|
||||
|
||||
// PlatformIDsWhere 返回满足条件的具体平台标识,按展示顺序。
|
||||
func PlatformIDsWhere(match func(PlatformSpec) bool) []string {
|
||||
out := make([]string, 0, len(platformList))
|
||||
for _, spec := range platformList {
|
||||
if match(spec) {
|
||||
out = append(out, spec.ID)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// compositePrecedenceHead 是组合分组查找顺序的固定前缀(历史顺序:Gemini 先于 OpenAI)。
|
||||
var compositePrecedenceHead = []string{PlatformAnthropic, PlatformGemini, PlatformOpenAI}
|
||||
|
||||
// CompositePrecedencePlatformIDs 返回按平台依次回退时的顺序:组合分组在请求目标
|
||||
// 解析前逐平台查找定价 / 模型映射(先命中者生效)、拼接默认模型列表,以及调度快照
|
||||
// 遍历。沿用历史顺序 anthropic、gemini、openai,其余按展示顺序,新登记的平台追加在末尾。
|
||||
func CompositePrecedencePlatformIDs() []string {
|
||||
out := make([]string, 0, len(platformList))
|
||||
seen := make(map[string]struct{}, len(platformList))
|
||||
for _, id := range compositePrecedenceHead {
|
||||
if IsConcretePlatform(id) {
|
||||
out = append(out, id)
|
||||
seen[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, spec := range platformList {
|
||||
if _, ok := seen[spec.ID]; !ok {
|
||||
out = append(out, spec.ID)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// UsesOpenAIGateway 报告平台是否由 OpenAI 网关处理。
|
||||
func UsesOpenAIGateway(id string) bool {
|
||||
spec, ok := LookupPlatform(id)
|
||||
return ok && spec.Gateway == PlatformGatewayOpenAI
|
||||
}
|
||||
|
||||
// IsCNProviderPlatform 报告平台是否为国产单一厂商。
|
||||
func IsCNProviderPlatform(id string) bool {
|
||||
spec, ok := LookupPlatform(id)
|
||||
return ok && spec.CNProvider
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 重构前各处手写的平台列表,作为平台清单派生结果的等价基准。
|
||||
var (
|
||||
legacyDisplayOrder = []string{
|
||||
PlatformAnthropic, PlatformOpenAI, PlatformGemini, PlatformAntigravity, PlatformGrok,
|
||||
PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo, PlatformTypeSafe,
|
||||
}
|
||||
legacyCompositePrecedence = []string{
|
||||
PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok,
|
||||
PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo, PlatformTypeSafe,
|
||||
}
|
||||
legacyOpenAIGateway = []string{
|
||||
PlatformOpenAI, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo,
|
||||
}
|
||||
legacyCNProviders = []string{PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax}
|
||||
legacyLiteLLMByPlatf = map[string]string{
|
||||
PlatformAnthropic: "anthropic", PlatformOpenAI: "openai", PlatformGemini: "gemini",
|
||||
PlatformAntigravity: "anthropic", PlatformGrok: "xai", PlatformKimi: "moonshot",
|
||||
PlatformZhipu: "zhipu", PlatformDeepseek: "deepseek", PlatformMiniMax: "minimax",
|
||||
PlatformOpenCodeGo: "opencode-go", PlatformTypeSafe: "typesafe",
|
||||
}
|
||||
)
|
||||
|
||||
func TestPlatformListMatchesLegacyLists(t *testing.T) {
|
||||
require.Equal(t, legacyDisplayOrder, ConcretePlatformIDs())
|
||||
require.Equal(t, legacyCompositePrecedence, CompositePrecedencePlatformIDs())
|
||||
require.Equal(t, legacyOpenAIGateway, PlatformIDsWhere(func(spec PlatformSpec) bool {
|
||||
return spec.Gateway == PlatformGatewayOpenAI
|
||||
}))
|
||||
require.Equal(t, legacyCNProviders, PlatformIDsWhere(func(spec PlatformSpec) bool { return spec.CNProvider }))
|
||||
for platform, provider := range legacyLiteLLMByPlatf {
|
||||
spec, ok := LookupPlatform(platform)
|
||||
require.True(t, ok, platform)
|
||||
require.Equal(t, provider, spec.LiteLLMProvider, platform)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformListPredicates(t *testing.T) {
|
||||
for _, platform := range legacyDisplayOrder {
|
||||
require.True(t, IsConcretePlatform(platform), platform)
|
||||
require.True(t, IsGroupPlatform(platform), platform)
|
||||
spec, ok := LookupPlatform(platform)
|
||||
require.True(t, ok, platform)
|
||||
require.NotEmpty(t, spec.DisplayName, platform)
|
||||
require.NotEmpty(t, spec.Gateway, platform)
|
||||
}
|
||||
require.False(t, IsConcretePlatform(PlatformComposite))
|
||||
require.True(t, IsGroupPlatform(PlatformComposite))
|
||||
for _, invalid := range []string{"", "moonshot", "Kimi", "openai ", "glm", "bogus"} {
|
||||
require.False(t, IsConcretePlatform(invalid), invalid)
|
||||
require.False(t, IsGroupPlatform(invalid), invalid)
|
||||
require.False(t, UsesOpenAIGateway(invalid), invalid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformsReturnsCopy(t *testing.T) {
|
||||
platforms := Platforms()
|
||||
platforms[0].ID = "mutated"
|
||||
require.Equal(t, PlatformAnthropic, Platforms()[0].ID)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
@@ -631,20 +632,14 @@ func (h *ChannelHandler) GetModelDefaultPricing(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// platformToLiteLLMProvider maps a channel platform name to the corresponding
|
||||
// LiteLLM provider string used as the key in the pricing catalog.
|
||||
var platformToLiteLLMProvider = map[string]string{
|
||||
service.PlatformAnthropic: "anthropic",
|
||||
service.PlatformOpenAI: "openai",
|
||||
service.PlatformGemini: "gemini",
|
||||
service.PlatformAntigravity: "anthropic",
|
||||
service.PlatformGrok: "xai",
|
||||
service.PlatformKimi: "moonshot",
|
||||
service.PlatformZhipu: "zhipu",
|
||||
service.PlatformDeepseek: "deepseek",
|
||||
service.PlatformMiniMax: "minimax",
|
||||
service.PlatformOpenCodeGo: "opencode-go",
|
||||
service.PlatformTypeSafe: "typesafe",
|
||||
// platformLiteLLMProvider maps a channel platform name to the corresponding
|
||||
// LiteLLM provider string used as the key in the pricing catalog (platform list).
|
||||
func platformLiteLLMProvider(platform string) (string, bool) {
|
||||
spec, ok := domain.LookupPlatform(platform)
|
||||
if !ok || spec.LiteLLMProvider == "" {
|
||||
return "", false
|
||||
}
|
||||
return spec.LiteLLMProvider, true
|
||||
}
|
||||
|
||||
// SyncPricingModels 返回 LiteLLM 定价目录中指定平台的最新模型列表
|
||||
@@ -657,7 +652,7 @@ func (h *ChannelHandler) SyncPricingModels(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
provider, ok := platformToLiteLLMProvider[platform]
|
||||
provider, ok := platformLiteLLMProvider(platform)
|
||||
if !ok {
|
||||
response.ErrorFrom(c, infraerrors.BadRequest("UNSUPPORTED_PLATFORM",
|
||||
fmt.Sprintf("unsupported platform: %s", platform)).
|
||||
|
||||
@@ -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 minimax opencode_go typesafe composite"`
|
||||
Platform string `json:"platform" binding:"omitempty,group_platform"`
|
||||
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 minimax opencode_go typesafe composite"`
|
||||
Platform string `json:"platform" binding:"omitempty,group_platform"`
|
||||
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 minimax opencode_go typesafe"`
|
||||
TargetPlatform string `json:"target_platform" binding:"required,concrete_platform"`
|
||||
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"`
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// 平台相关的请求绑定校验标签,由平台清单(domain/platforms.go)驱动,替代在
|
||||
// binding 标签里手写 oneof 白名单(新增平台时易漏加,导致分组无法创建):
|
||||
// - group_platform:已登记的具体平台或 composite;
|
||||
// - concrete_platform:已登记的具体平台。
|
||||
func init() {
|
||||
engine, ok := binding.Validator.Engine().(*validator.Validate)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = engine.RegisterValidation("group_platform", func(fl validator.FieldLevel) bool {
|
||||
return domain.IsGroupPlatform(fl.Field().String())
|
||||
})
|
||||
_ = engine.RegisterValidation("concrete_platform", func(fl validator.FieldLevel) bool {
|
||||
return domain.IsConcretePlatform(fl.Field().String())
|
||||
})
|
||||
}
|
||||
@@ -1301,7 +1301,7 @@ func (h *GatewayHandler) compositeAvailableModels(ctx context.Context, groupID *
|
||||
seen := make(map[string]struct{})
|
||||
models := make([]string, 0)
|
||||
schedulablePlatforms := h.gatewayService.GetSchedulablePlatforms(ctx, groupID)
|
||||
for _, platform := range []string{service.PlatformAnthropic, service.PlatformGemini, service.PlatformOpenAI, service.PlatformAntigravity, service.PlatformGrok, service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax, service.PlatformOpenCodeGo, service.PlatformTypeSafe} {
|
||||
for _, platform := range domain.CompositePrecedencePlatformIDs() {
|
||||
if platform == service.PlatformTypeSafe && !includeSystemOne {
|
||||
continue
|
||||
}
|
||||
@@ -1503,10 +1503,13 @@ func defaultModelIDsForPlatform(platform string) []string {
|
||||
case service.PlatformComposite:
|
||||
ids := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
// TypeSafe is deliberately absent: jev-latest only works through
|
||||
// /v1/systemone, so the static fallback never advertises it to LLM
|
||||
// clients. compositeAvailableModels lists it when the group can serve it.
|
||||
for _, concretePlatform := range []string{service.PlatformAnthropic, service.PlatformGemini, service.PlatformOpenAI, service.PlatformAntigravity, service.PlatformGrok, service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax, service.PlatformOpenCodeGo} {
|
||||
for _, concretePlatform := range domain.CompositePrecedencePlatformIDs() {
|
||||
// TypeSafe is deliberately skipped: jev-latest only works through
|
||||
// /v1/systemone, so the static fallback never advertises it to LLM
|
||||
// clients. compositeAvailableModels lists it when the group can serve it.
|
||||
if concretePlatform == service.PlatformTypeSafe {
|
||||
continue
|
||||
}
|
||||
for _, id := range defaultModelIDsForPlatform(concretePlatform) {
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/ip"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||
@@ -330,9 +331,9 @@ 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.PlatformMiniMax, service.PlatformOpenCodeGo)
|
||||
domain.PlatformIDsWhere(func(spec domain.PlatformSpec) bool {
|
||||
return spec.Gateway == domain.PlatformGatewayOpenAI
|
||||
})...)
|
||||
}
|
||||
|
||||
// isResponsesWebSocketCompositePlatform 限定 composite 分组在 Responses WebSocket
|
||||
|
||||
@@ -49,21 +49,9 @@ const (
|
||||
PlatformTypeSafe = domain.PlatformTypeSafe
|
||||
)
|
||||
|
||||
// AllPlatforms 返回所有支持的平台列表
|
||||
// AllPlatforms 返回所有支持的平台列表(平台清单,按展示顺序)。
|
||||
func AllPlatforms() []string {
|
||||
return []string{
|
||||
PlatformAnthropic,
|
||||
PlatformOpenAI,
|
||||
PlatformGemini,
|
||||
PlatformAntigravity,
|
||||
PlatformGrok,
|
||||
PlatformKimi,
|
||||
PlatformZhipu,
|
||||
PlatformDeepseek,
|
||||
PlatformMiniMax,
|
||||
PlatformOpenCodeGo,
|
||||
PlatformTypeSafe,
|
||||
}
|
||||
return domain.ConcretePlatformIDs()
|
||||
}
|
||||
|
||||
// Validate 验证规则配置的有效性
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
"github.com/Wei-Shaw/sub2api/internal/handler"
|
||||
pkghttputil "github.com/Wei-Shaw/sub2api/internal/pkg/httputil"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/requestmodel"
|
||||
@@ -46,22 +47,15 @@ func RegisterGatewayRoutes(
|
||||
groupModelAllowlist := middleware.GroupModelAllowlist()
|
||||
|
||||
isOpenAIResponsesCompatibleGatewayPlatform := func(c *gin.Context) bool {
|
||||
switch getGroupPlatform(c) {
|
||||
case service.PlatformOpenAI, service.PlatformGrok,
|
||||
service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek,
|
||||
service.PlatformMiniMax, service.PlatformOpenCodeGo:
|
||||
// 国产 OpenAI 兼容供应商与 openai/grok 一样经 OpenAI 网关转发。
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
// openai、grok 与多协议 API Key 供应商经 OpenAI 网关转发(平台清单)。
|
||||
return domain.UsesOpenAIGateway(getGroupPlatform(c))
|
||||
}
|
||||
countTokensHandler := func(c *gin.Context) {
|
||||
switch getGroupPlatform(c) {
|
||||
case service.PlatformOpenAI, service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax, service.PlatformOpenCodeGo:
|
||||
h.OpenAIGateway.CountTokens(c)
|
||||
case service.PlatformGrok:
|
||||
switch platform := getGroupPlatform(c); {
|
||||
case platform == service.PlatformGrok:
|
||||
h.OpenAIGateway.GrokCountTokens(c)
|
||||
case domain.UsesOpenAIGateway(platform):
|
||||
h.OpenAIGateway.CountTokens(c)
|
||||
default:
|
||||
h.Gateway.CountTokens(c)
|
||||
}
|
||||
|
||||
@@ -303,7 +303,7 @@ func (a *Account) IsCNProvider() bool {
|
||||
// openai/grok 原生走 OpenAI 网关;国产供应商同为 OpenAI Chat Completions
|
||||
// 兼容上游,也经 OpenAI 网关转发。OpenCode 同样经 OpenAI 网关按模型分流。
|
||||
func (a *Account) IsOpenAICompatible() bool {
|
||||
return a != nil && (a.Platform == PlatformOpenAI || a.Platform == PlatformGrok || a.IsCNProvider() || a.IsOpenCodeGo())
|
||||
return a != nil && domain.UsesOpenAIGateway(a.Platform)
|
||||
}
|
||||
|
||||
func (a *Account) GeminiOAuthType() string {
|
||||
@@ -1384,7 +1384,7 @@ func (a *Account) IsOpenAIApiKey() bool {
|
||||
// 适用 openai、国产 OpenAI 兼容供应商(kimi/zhipu/deepseek)与 OpenCode Go;
|
||||
// grok 走 GetGrokBaseURL,此处对 grok 返回 "" 以保持原有行为。
|
||||
func (a *Account) GetOpenAIBaseURL() string {
|
||||
if !a.IsOpenAI() && !a.IsCNProvider() && !a.IsOpenCodeGo() {
|
||||
if !a.IsOpenAI() && !a.IsMultiProtocolAPIKey() {
|
||||
return ""
|
||||
}
|
||||
if a.IsMultiProtocolAPIKey() && a.IsAdaptiveAPIProtocol() {
|
||||
|
||||
@@ -77,10 +77,10 @@ func (a *Account) IsHeaderOverrideEligible() bool {
|
||||
if a == nil {
|
||||
return false
|
||||
}
|
||||
switch a.Platform {
|
||||
case PlatformAnthropic, PlatformOpenAI, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo:
|
||||
switch {
|
||||
case a.Platform == PlatformAnthropic, a.Platform == PlatformOpenAI, IsMultiProtocolAPIKeyProvider(a.Platform):
|
||||
return a.Type == AccountTypeAPIKey
|
||||
case PlatformGrok:
|
||||
case a.Platform == PlatformGrok:
|
||||
return a.Type == AccountTypeAPIKey || a.Type == AccountTypeOAuth
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -522,10 +522,12 @@ func (s *AccountService) TestCredentials(ctx context.Context, id int64) error {
|
||||
case PlatformTypeSafe:
|
||||
// TypeSafe credentials are API keys; inference failures drive health and cooldown state.
|
||||
return nil
|
||||
case PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo:
|
||||
// 国产 OpenAI 兼容供应商与 OpenCode:凭证为 API Key,实际可用性经余额/额度探测与转发路径验证。
|
||||
return nil
|
||||
default:
|
||||
if IsMultiProtocolAPIKeyProvider(account.Platform) {
|
||||
// 多协议 API Key 供应商(国产厂商与聚合平台):凭证为 API Key,实际可用性
|
||||
// 经余额/额度探测与转发路径验证。
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unsupported platform: %s", account.Platform)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
@@ -319,10 +320,13 @@ func defaultAllowImageGenerationForPlatform(platform string) bool {
|
||||
func compositeDefaultModelsListCandidateIDs() []string {
|
||||
seen := make(map[string]struct{})
|
||||
ids := make([]string, 0)
|
||||
// TypeSafe stays out of the static composite candidates (jev-latest only works
|
||||
// through /v1/systemone); groups with TypeSafe accounts still get it from the
|
||||
// account model mappings collected by GetGroupModelsListCandidates.
|
||||
for _, platform := range []string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo} {
|
||||
for _, platform := range domain.CompositePrecedencePlatformIDs() {
|
||||
// TypeSafe stays out of the static composite candidates (jev-latest only works
|
||||
// through /v1/systemone); groups with TypeSafe accounts still get it from the
|
||||
// account model mappings collected by GetGroupModelsListCandidates.
|
||||
if platform == PlatformTypeSafe {
|
||||
continue
|
||||
}
|
||||
for _, id := range defaultModelsListCandidateIDs(platform) {
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||
@@ -368,7 +369,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, PlatformMiniMax, PlatformOpenCodeGo, PlatformTypeSafe}
|
||||
return domain.CompositePrecedencePlatformIDs()
|
||||
}
|
||||
return []string{groupPlatform}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
|
||||
)
|
||||
|
||||
@@ -204,11 +205,5 @@ func (s *GatewayService) resolveCompositeRouteDecision(ctx context.Context, grou
|
||||
}
|
||||
|
||||
func isConcreteRequestPlatform(platform string) bool {
|
||||
switch platform {
|
||||
case PlatformAnthropic, PlatformOpenAI, PlatformGemini, PlatformAntigravity, PlatformGrok,
|
||||
PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo, PlatformTypeSafe:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return domain.IsConcretePlatform(platform)
|
||||
}
|
||||
|
||||
@@ -101,14 +101,10 @@ const (
|
||||
DefaultOpenCodeZenAnthropicBaseURL = "https://opencode.ai/zen"
|
||||
)
|
||||
|
||||
// IsCNProvider 报告 platform 是否为国产 OpenAI 兼容供应商(kimi/zhipu/deepseek/minimax)。
|
||||
// IsCNProvider 报告 platform 是否为国产 OpenAI 兼容供应商(kimi/zhipu/deepseek/minimax),
|
||||
// 以平台清单(domain/platforms.go)为准。
|
||||
func IsCNProvider(platform string) bool {
|
||||
switch platform {
|
||||
case PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return domain.IsCNProviderPlatform(platform)
|
||||
}
|
||||
|
||||
// IsOpenCodeGo 报告 platform 是否为 OpenCode Go 订阅网关。
|
||||
@@ -123,22 +119,9 @@ func IsMultiProtocolAPIKeyProvider(platform string) bool {
|
||||
return LookupProviderProfile(platform) != nil
|
||||
}
|
||||
|
||||
// AllowedQuotaPlatforms 是允许设置 user × platform quota 的平台列表(单一权威来源)。
|
||||
// ent/schema/user_platform_quota.go 的 Validate 函数独立维护(构建期约束),
|
||||
// 若新增平台需同步修改该 schema。
|
||||
var AllowedQuotaPlatforms = []string{
|
||||
PlatformAnthropic,
|
||||
PlatformOpenAI,
|
||||
PlatformGemini,
|
||||
PlatformAntigravity,
|
||||
PlatformGrok,
|
||||
PlatformKimi,
|
||||
PlatformZhipu,
|
||||
PlatformDeepseek,
|
||||
PlatformMiniMax,
|
||||
PlatformOpenCodeGo,
|
||||
PlatformTypeSafe,
|
||||
}
|
||||
// AllowedQuotaPlatforms 是允许设置 user × platform quota 的平台列表:全部已登记的
|
||||
// 具体平台(domain/platforms.go),ent/schema/user_platform_quota.go 的校验同源。
|
||||
var AllowedQuotaPlatforms = domain.ConcretePlatformIDs()
|
||||
|
||||
// AllowedSchedulingThresholdPlatforms 是允许设置账号自动停调阈值的平台列表。
|
||||
// openai/anthropic/grok 有原生用量窗口;kimi/zhipu/minimax 的 Coding Plan 同样暴露
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -285,18 +286,16 @@ func (s *OpenAIGatewayService) SelectAccountForTokenCount(
|
||||
)
|
||||
}
|
||||
|
||||
// NormalizeOpenAICompatiblePlatform 保留 grok 与国产 OpenAI 兼容供应商(kimi/zhipu/
|
||||
// deepseek)的原值,其他值一律归一为 openai。调度器据此对账号与请求做精确平台匹配:
|
||||
// NormalizeOpenAICompatiblePlatform 保留走 OpenAI 网关的平台(openai、grok 与多协议
|
||||
// API Key 供应商,见平台清单)的原值,其他值一律归一为 openai。调度器据此对账号与请求做精确平台匹配:
|
||||
// kimi 分组请求只命中 kimi 账号,语义与 openai/grok 一致。
|
||||
// (upstream 曾将本函数改为未导出 normalizeOpenAICompatiblePlatform,本分支的
|
||||
// handler 调度入口仍需导出,保持导出名。)
|
||||
func NormalizeOpenAICompatiblePlatform(platform string) string {
|
||||
switch platform {
|
||||
case PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo:
|
||||
if domain.UsesOpenAIGateway(platform) {
|
||||
return platform
|
||||
default:
|
||||
return PlatformOpenAI
|
||||
}
|
||||
return PlatformOpenAI
|
||||
}
|
||||
|
||||
// noAvailableOpenAISelectionError builds the standard "no account available" error
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 以下为重构前各处手写的平台列表 / switch,作为平台清单派生结果的等价基准。
|
||||
var (
|
||||
legacyAllPlatforms = []string{
|
||||
PlatformAnthropic, PlatformOpenAI, PlatformGemini, PlatformAntigravity, PlatformGrok,
|
||||
PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo, PlatformTypeSafe,
|
||||
}
|
||||
legacySchedulerSnapshotPlatforms = []string{
|
||||
PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok,
|
||||
PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo, PlatformTypeSafe,
|
||||
}
|
||||
legacyCompositeMatchingPlatforms = legacySchedulerSnapshotPlatforms
|
||||
legacyMultiProtocolProviders = []string{PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo}
|
||||
platformProbeValues = append(append([]string{}, legacyAllPlatforms...), PlatformComposite, "", "moonshot", "Kimi", "openai ", "glm", "bogus")
|
||||
platformProbeAccountTypes = []string{AccountTypeAPIKey, AccountTypeOAuth, AccountTypeSetupToken, AccountTypeUpstream, ""}
|
||||
)
|
||||
|
||||
func legacyIsCNProvider(platform string) bool {
|
||||
switch platform {
|
||||
case PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func legacyIsOpenAICompatible(platform string) bool {
|
||||
return platform == PlatformOpenAI || platform == PlatformGrok || legacyIsCNProvider(platform) || platform == PlatformOpenCodeGo
|
||||
}
|
||||
|
||||
func legacyNormalizeOpenAICompatiblePlatform(platform string) string {
|
||||
switch platform {
|
||||
case PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo:
|
||||
return platform
|
||||
}
|
||||
return PlatformOpenAI
|
||||
}
|
||||
|
||||
func legacyIsUpstreamBillingProbeIdentity(platform, accountType string) bool {
|
||||
if accountType != AccountTypeAPIKey {
|
||||
return false
|
||||
}
|
||||
switch platform {
|
||||
case PlatformOpenAI, PlatformAnthropic, PlatformGemini, PlatformAntigravity, PlatformGrok,
|
||||
PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo,
|
||||
PlatformTypeSafe:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func legacyIsHeaderOverrideEligible(platform, accountType string) bool {
|
||||
switch platform {
|
||||
case PlatformAnthropic, PlatformOpenAI, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo:
|
||||
return accountType == AccountTypeAPIKey
|
||||
case PlatformGrok:
|
||||
return accountType == AccountTypeAPIKey || accountType == AccountTypeOAuth
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func legacyIsConcreteRequestPlatform(platform string) bool {
|
||||
for _, p := range legacyAllPlatforms {
|
||||
if p == platform {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestPlatformListDerivedListsMatchLegacy(t *testing.T) {
|
||||
require.Equal(t, legacyAllPlatforms, AllowedQuotaPlatforms)
|
||||
require.Equal(t, legacySchedulerSnapshotPlatforms, schedulerSnapshotPlatforms())
|
||||
require.Equal(t, legacyCompositeMatchingPlatforms, matchingPlatforms(PlatformComposite))
|
||||
require.Equal(t, []string{PlatformKimi}, matchingPlatforms(PlatformKimi))
|
||||
|
||||
var multiProtocol []string
|
||||
for _, platform := range legacyAllPlatforms {
|
||||
if IsMultiProtocolAPIKeyProvider(platform) {
|
||||
multiProtocol = append(multiProtocol, platform)
|
||||
}
|
||||
}
|
||||
require.Equal(t, legacyMultiProtocolProviders, multiProtocol)
|
||||
}
|
||||
|
||||
func TestPlatformListDerivedPredicatesMatchLegacy(t *testing.T) {
|
||||
for _, platform := range platformProbeValues {
|
||||
require.Equal(t, legacyIsCNProvider(platform), IsCNProvider(platform), platform)
|
||||
require.Equal(t, legacyIsConcreteRequestPlatform(platform), isConcreteRequestPlatform(platform), platform)
|
||||
require.Equal(t, legacyNormalizeOpenAICompatiblePlatform(platform), NormalizeOpenAICompatiblePlatform(platform), platform)
|
||||
require.Equal(t, legacyIsOpenAICompatible(platform), (&Account{Platform: platform}).IsOpenAICompatible(), platform)
|
||||
for _, accountType := range platformProbeAccountTypes {
|
||||
require.Equal(t, legacyIsUpstreamBillingProbeIdentity(platform, accountType),
|
||||
IsUpstreamBillingProbeIdentity(platform, accountType), "%s/%s", platform, accountType)
|
||||
require.Equal(t, legacyIsHeaderOverrideEligible(platform, accountType),
|
||||
(&Account{Platform: platform, Type: accountType}).IsHeaderOverrideEligible(), "%s/%s", platform, accountType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 多协议 API Key 供应商经 OpenAI 网关转发(NormalizeOpenAICompatiblePlatform、
|
||||
// countTokens 路由等依赖此约束),ProviderProfile 登记的平台必须在平台清单中声明 openai 网关。
|
||||
func TestProviderProfilesUseOpenAIGateway(t *testing.T) {
|
||||
for platform := range providerProfiles {
|
||||
spec, ok := domain.LookupPlatform(platform)
|
||||
require.True(t, ok, platform)
|
||||
require.Equal(t, domain.PlatformGatewayOpenAI, spec.Gateway, platform)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||
)
|
||||
|
||||
@@ -608,10 +609,10 @@ func (s *SchedulerSnapshotService) handleBulkAccountEvent(ctx context.Context, p
|
||||
continue
|
||||
}
|
||||
accountGroupIDs := s.normalizeGroupIDs(account.GroupIDs)
|
||||
switch account.Platform {
|
||||
case PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo, PlatformTypeSafe:
|
||||
switch {
|
||||
case account.Platform != PlatformAntigravity && isConcreteRequestPlatform(account.Platform):
|
||||
addPlatformGroups(account.Platform, accountGroupIDs)
|
||||
case PlatformAntigravity:
|
||||
case account.Platform == PlatformAntigravity:
|
||||
// 批量更新可能刚关闭 mixed_scheduling,仍需清理两个兼容平台的旧快照。
|
||||
addPlatformGroups(PlatformAntigravity, accountGroupIDs)
|
||||
addPlatformGroups(PlatformAnthropic, accountGroupIDs)
|
||||
@@ -824,8 +825,9 @@ func (s *SchedulerSnapshotService) rebuildByAccount(ctx context.Context, account
|
||||
return s.rebuildBuckets(ctx, buckets, reason)
|
||||
}
|
||||
|
||||
func schedulerSnapshotPlatforms() [11]string {
|
||||
return [11]string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo, PlatformTypeSafe}
|
||||
// schedulerSnapshotPlatforms 返回需要维护调度快照的全部具体平台(平台清单)。
|
||||
func schedulerSnapshotPlatforms() []string {
|
||||
return domain.CompositePrecedencePlatformIDs()
|
||||
}
|
||||
|
||||
// 生命周期辅助函数有意排除 group0;full rebuild 构造 group0 canonical 集时必须显式调用 canonical helper。
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
@@ -1008,17 +1009,7 @@ func decodeUpstreamBillingProbeSnapshot(extra map[string]any) *UpstreamBillingPr
|
||||
// type=apikey by the admin form, so only pre-existing type=upstream rows
|
||||
// cannot turn the probe on.
|
||||
func IsUpstreamBillingProbeIdentity(platform, accountType string) bool {
|
||||
if accountType != AccountTypeAPIKey {
|
||||
return false
|
||||
}
|
||||
switch platform {
|
||||
case PlatformOpenAI, PlatformAnthropic, PlatformGemini, PlatformAntigravity, PlatformGrok,
|
||||
PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo,
|
||||
PlatformTypeSafe:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return accountType == AccountTypeAPIKey && domain.IsConcretePlatform(platform)
|
||||
}
|
||||
|
||||
func isUpstreamBillingProbeAccount(account *Account) bool {
|
||||
|
||||
Reference in New Issue
Block a user