mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-28 00:07:33 +00:00
fix(composite): support CN providers
Extend composite routing, pricing, migrations, and admin options for Kimi, GLM, and DeepSeek.
This commit is contained in:
@@ -234,7 +234,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"`
|
||||
TargetPlatform string `json:"target_platform" binding:"required,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek"`
|
||||
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"`
|
||||
|
||||
@@ -71,13 +71,11 @@ func TestGroupPlatformBinding_RejectsInvalidPlatforms(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 守住 composite 路由目标不放行 CN:CN 平台不可作为 composite 路由目标
|
||||
// (DetectModelPlatform/isConcreteRequestPlatform 均无 CN 分支,放行即打开半实现路径)。
|
||||
func TestCompositeRouteTargetPlatform_StillExcludesCNProviders(t *testing.T) {
|
||||
func TestCompositeRouteTargetPlatform_AllowsCNProviders(t *testing.T) {
|
||||
for _, platform := range []string{"kimi", "zhipu", "deepseek"} {
|
||||
var req CompositeRouteRequest
|
||||
body := fmt.Sprintf(`{"public_model":"m","target_platform":%q}`, platform)
|
||||
require.Error(t, bindGroupPlatformJSON(t, &req, body),
|
||||
"composite target_platform %q 应保持被拒", platform)
|
||||
require.NoError(t, bindGroupPlatformJSON(t, &req, body))
|
||||
require.Equal(t, platform, req.TargetPlatform)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,19 +22,41 @@ func TestCompositeTargetPlatformAllowedResolvesKnownAllowedModel(t *testing.T) {
|
||||
require.Equal(t, service.PlatformOpenAI, platform)
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleTextTargetAllowsCompositeGrokModel(t *testing.T) {
|
||||
func TestOpenAICompatibleTextTargetAllowsCompositeProviders(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
for _, path := range []string{"/v1/messages", "/v1/chat/completions"} {
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest("POST", path, nil)
|
||||
apiKey := &service.APIKey{Group: &service.Group{Platform: service.PlatformComposite}}
|
||||
|
||||
require.True(t, openAICompatibleTextTargetAllowed(c, apiKey, "grok-4.3"), "path=%s", path)
|
||||
platform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context())
|
||||
require.True(t, ok, "path=%s", path)
|
||||
require.Equal(t, service.PlatformGrok, platform, "path=%s", path)
|
||||
providers := []struct {
|
||||
model string
|
||||
platform string
|
||||
}{
|
||||
{model: "grok-4.3", platform: service.PlatformGrok},
|
||||
{model: "kimi-k2-thinking", platform: service.PlatformKimi},
|
||||
{model: "glm-5.2", platform: service.PlatformZhipu},
|
||||
{model: "deepseek-v3.2", platform: service.PlatformDeepseek},
|
||||
}
|
||||
for _, path := range []string{"/v1/messages", "/v1/chat/completions", "/v1/responses"} {
|
||||
for _, provider := range providers {
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest("POST", path, nil)
|
||||
apiKey := &service.APIKey{Group: &service.Group{Platform: service.PlatformComposite}}
|
||||
|
||||
require.True(t, openAICompatibleTextTargetAllowed(c, apiKey, provider.model), "path=%s model=%s", path, provider.model)
|
||||
platform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context())
|
||||
require.True(t, ok, "path=%s model=%s", path, provider.model)
|
||||
require.Equal(t, provider.platform, platform, "path=%s model=%s", path, provider.model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponsesWebSocketCompositePlatformGuardAllowsOpenAICompatibleProviders(t *testing.T) {
|
||||
for _, platform := range []string{
|
||||
service.PlatformOpenAI, service.PlatformGrok, service.PlatformKimi,
|
||||
service.PlatformZhipu, service.PlatformDeepseek,
|
||||
} {
|
||||
require.True(t, isOpenAICompatibleTextPlatform(platform), "platform=%s", platform)
|
||||
}
|
||||
require.False(t, isOpenAICompatibleTextPlatform(service.PlatformAnthropic))
|
||||
require.False(t, isOpenAICompatibleTextPlatform(service.PlatformGemini))
|
||||
}
|
||||
|
||||
func TestCompositeTargetPlatformAllowedRejectsWrongOrUnknownModel(t *testing.T) {
|
||||
|
||||
@@ -1147,7 +1147,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} {
|
||||
for _, platform := range []string{service.PlatformAnthropic, service.PlatformGemini, service.PlatformOpenAI, service.PlatformAntigravity, service.PlatformGrok, service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek} {
|
||||
platformModels := h.gatewayService.GetAvailableModels(ctx, groupID, platform)
|
||||
if len(platformModels) == 0 {
|
||||
if _, ok := schedulablePlatforms[platform]; ok {
|
||||
@@ -1369,10 +1369,12 @@ func defaultModelIDsForPlatform(platform string) []string {
|
||||
return mergeModelIDs(ids, nil)
|
||||
case service.PlatformGrok:
|
||||
return xai.DefaultModelIDs()
|
||||
case service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek:
|
||||
return nil
|
||||
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} {
|
||||
for _, concretePlatform := range []string{service.PlatformAnthropic, service.PlatformGemini, service.PlatformOpenAI, service.PlatformAntigravity, service.PlatformGrok, service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek} {
|
||||
for _, id := range defaultModelIDsForPlatform(concretePlatform) {
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
|
||||
@@ -321,6 +321,27 @@ func TestGatewayModels_CompositeCustomModelsListFiltersAcrossConcretePlatforms(t
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 4,
|
||||
Platform: service.PlatformKimi,
|
||||
Credentials: map[string]any{
|
||||
"model_mapping": map[string]any{"kimi-custom": "kimi-upstream"},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 5,
|
||||
Platform: service.PlatformZhipu,
|
||||
Credentials: map[string]any{
|
||||
"model_mapping": map[string]any{"glm-custom": "glm-upstream"},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 6,
|
||||
Platform: service.PlatformDeepseek,
|
||||
Credentials: map[string]any{
|
||||
"model_mapping": map[string]any{"deepseek-custom": "deepseek-upstream"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -335,7 +356,7 @@ func TestGatewayModels_CompositeCustomModelsListFiltersAcrossConcretePlatforms(t
|
||||
Platform: service.PlatformComposite,
|
||||
ModelsListConfig: service.GroupModelsListConfig{
|
||||
Enabled: true,
|
||||
Models: []string{"gemini-2.5-flash", "missing-model", "ag-custom-model", "gpt-5.5"},
|
||||
Models: []string{"gemini-2.5-flash", "missing-model", "ag-custom-model", "gpt-5.5", "kimi-custom", "glm-custom", "deepseek-custom"},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -346,7 +367,7 @@ func TestGatewayModels_CompositeCustomModelsListFiltersAcrossConcretePlatforms(t
|
||||
|
||||
var got gatewayModelsResponseForTest
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
|
||||
require.Equal(t, []string{"gemini-2.5-flash", "ag-custom-model", "gpt-5.5"}, modelIDsForTest(got.Data))
|
||||
require.Equal(t, []string{"gemini-2.5-flash", "ag-custom-model", "gpt-5.5", "kimi-custom", "glm-custom", "deepseek-custom"}, modelIDsForTest(got.Data))
|
||||
}
|
||||
|
||||
func TestGatewayModels_CompositeUnmappedAccountsFallbackToLinkedPlatformsOnly(t *testing.T) {
|
||||
|
||||
@@ -213,6 +213,16 @@ func openAICompatibleTextTargetAllowed(c *gin.Context, apiKey *service.APIKey, m
|
||||
service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek)
|
||||
}
|
||||
|
||||
func isOpenAICompatibleTextPlatform(platform string) bool {
|
||||
switch platform {
|
||||
case service.PlatformOpenAI, service.PlatformGrok,
|
||||
service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// NewOpenAIGatewayHandler creates a new OpenAIGatewayHandler
|
||||
func NewOpenAIGatewayHandler(
|
||||
gatewayService *service.OpenAIGatewayService,
|
||||
@@ -333,7 +343,7 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
|
||||
}
|
||||
reqModel := modelResult.String()
|
||||
ensureCompositeTargetPlatform(c, apiKey, reqModel)
|
||||
if !compositeTargetPlatformAllowed(c, apiKey, reqModel, service.PlatformOpenAI, service.PlatformGrok) {
|
||||
if !openAICompatibleTextTargetAllowed(c, apiKey, reqModel) {
|
||||
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Model is not supported by this OpenAI-compatible endpoint for composite groups")
|
||||
return
|
||||
}
|
||||
@@ -1750,7 +1760,7 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
|
||||
ctx = c.Request.Context()
|
||||
if apiKey.Group != nil && apiKey.Group.Platform == service.PlatformComposite {
|
||||
platform, ok := service.ResolvedTargetPlatformFromContext(ctx)
|
||||
if !ok || (platform != service.PlatformOpenAI && platform != service.PlatformGrok) {
|
||||
if !ok || !isOpenAICompatibleTextPlatform(platform) {
|
||||
closeOpenAIClientWS(wsConn, coderws.StatusPolicyViolation, "Responses WebSocket API only supports OpenAI-compatible models for composite groups")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -247,6 +247,8 @@ func defaultModelsListCandidateIDs(platform string) []string {
|
||||
return ids
|
||||
case PlatformGrok:
|
||||
return xai.DefaultModelIDs()
|
||||
case PlatformKimi, PlatformZhipu, PlatformDeepseek:
|
||||
return nil
|
||||
case PlatformComposite:
|
||||
return compositeDefaultModelsListCandidateIDs()
|
||||
default:
|
||||
@@ -267,7 +269,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} {
|
||||
for _, platform := range []string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek} {
|
||||
for _, id := range defaultModelsListCandidateIDs(platform) {
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
|
||||
@@ -174,6 +174,13 @@ func TestAdminService_CompositeModelsListCandidatesIncludeConcreteAccountMapping
|
||||
"model_mapping": map[string]any{"gemini-custom": "gemini-2.5-flash"},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Platform: PlatformKimi,
|
||||
Credentials: map[string]any{
|
||||
"model_mapping": map[string]any{"kimi-custom": "kimi-k2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
groupRepo := &groupRepoStubForAdmin{
|
||||
@@ -188,6 +195,7 @@ func TestAdminService_CompositeModelsListCandidatesIncludeConcreteAccountMapping
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, candidates, "gpt-custom")
|
||||
require.Contains(t, candidates, "gemini-custom")
|
||||
require.Contains(t, candidates, "kimi-custom")
|
||||
require.Contains(t, candidates, "gpt-5.5")
|
||||
require.Contains(t, candidates, "gemini-2.5-flash")
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
return []string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek}
|
||||
}
|
||||
return []string{groupPlatform}
|
||||
}
|
||||
|
||||
@@ -2041,6 +2041,9 @@ func TestIsPlatformPricingMatch(t *testing.T) {
|
||||
{"gemini does NOT match anthropic", PlatformGemini, PlatformAnthropic, false},
|
||||
{"composite matches openai pricing", PlatformComposite, PlatformOpenAI, true},
|
||||
{"composite matches gemini pricing", PlatformComposite, PlatformGemini, true},
|
||||
{"composite matches kimi pricing", PlatformComposite, PlatformKimi, true},
|
||||
{"composite matches zhipu pricing", PlatformComposite, PlatformZhipu, true},
|
||||
{"composite matches deepseek pricing", PlatformComposite, PlatformDeepseek, true},
|
||||
{"empty string matches nothing", "", PlatformAnthropic, false},
|
||||
{"empty string matches empty", "", "", true},
|
||||
}
|
||||
@@ -2066,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}},
|
||||
{"composite returns concrete platforms", PlatformComposite, []string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -106,6 +106,12 @@ func DetectModelPlatform(model string) (string, bool) {
|
||||
return PlatformGemini, true
|
||||
case "xai", "x-ai", "grok":
|
||||
return PlatformGrok, true
|
||||
case "kimi", "moonshot":
|
||||
return PlatformKimi, true
|
||||
case "zhipu", "glm", "bigmodel":
|
||||
return PlatformZhipu, true
|
||||
case "deepseek":
|
||||
return PlatformDeepseek, true
|
||||
}
|
||||
if rest != "" {
|
||||
normalized = strings.TrimPrefix(rest, "models/")
|
||||
@@ -133,6 +139,13 @@ func DetectModelPlatform(model string) (string, bool) {
|
||||
return PlatformGemini, true
|
||||
case normalized == "grok" || strings.HasPrefix(normalized, "grok-"):
|
||||
return PlatformGrok, true
|
||||
case strings.HasPrefix(normalized, "kimi-"),
|
||||
strings.HasPrefix(normalized, "moonshot-"):
|
||||
return PlatformKimi, true
|
||||
case strings.HasPrefix(normalized, "glm-"):
|
||||
return PlatformZhipu, true
|
||||
case strings.HasPrefix(normalized, "deepseek-"):
|
||||
return PlatformDeepseek, true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
@@ -179,7 +192,8 @@ func (s *GatewayService) resolveCompositeRouteDecision(ctx context.Context, grou
|
||||
|
||||
func isConcreteRequestPlatform(platform string) bool {
|
||||
switch platform {
|
||||
case PlatformAnthropic, PlatformOpenAI, PlatformGemini, PlatformAntigravity, PlatformGrok:
|
||||
case PlatformAnthropic, PlatformOpenAI, PlatformGemini, PlatformAntigravity, PlatformGrok,
|
||||
PlatformKimi, PlatformZhipu, PlatformDeepseek:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -25,6 +25,10 @@ func TestDetectModelPlatform(t *testing.T) {
|
||||
{name: "learnlm", model: "learnlm-2.0-flash-experimental", platform: PlatformGemini, ok: true},
|
||||
{name: "grok", model: "grok-4", platform: PlatformGrok, ok: true},
|
||||
{name: "xai prefix", model: "xai/grok-4", platform: PlatformGrok, ok: true},
|
||||
{name: "kimi", model: "kimi-k2-thinking", platform: PlatformKimi, ok: true},
|
||||
{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: "unknown", model: "llama-4-maverick", ok: false},
|
||||
}
|
||||
|
||||
@@ -59,7 +63,14 @@ func TestCompositeGroupSchedulerHasAllCanonicalPlatformBuckets(t *testing.T) {
|
||||
platforms = append(platforms, platform)
|
||||
}
|
||||
require.ElementsMatch(t,
|
||||
[]string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok},
|
||||
[]string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek},
|
||||
platforms,
|
||||
)
|
||||
}
|
||||
|
||||
func TestCompositeConcretePlatformsIncludeCNProviders(t *testing.T) {
|
||||
for _, platform := range []string{PlatformKimi, PlatformZhipu, PlatformDeepseek} {
|
||||
require.True(t, isConcreteRequestPlatform(platform))
|
||||
require.True(t, canCopyAccountsFromGroupPlatform(PlatformComposite, platform))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,21 @@ func TestSchedulerBulkAccountEventScopesOpenAIRebuildToFreshPlatform(t *testing.
|
||||
require.Empty(t, deleted)
|
||||
}
|
||||
|
||||
func TestSchedulerBulkAccountEventScopesCNRebuildToFreshPlatform(t *testing.T) {
|
||||
for _, platform := range []string{PlatformKimi, PlatformZhipu, PlatformDeepseek} {
|
||||
t.Run(platform, func(t *testing.T) {
|
||||
cache := newBulkEventSnapshotCache()
|
||||
repo := newBulkEventAccountRepo(&Account{ID: 1, Platform: platform, GroupIDs: []int64{12}})
|
||||
svc := newBulkEventTestService(cache, repo)
|
||||
|
||||
err := svc.handleBulkAccountEvent(context.Background(), bulkEventPayload([]int64{1}, []int64{11}), make(map[batchSeenKey]struct{}))
|
||||
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, schedulerBucketsForTest([]int64{11, 12}, platform), cache.capturedBuckets())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchedulerBulkAccountEventRebuildsOpenAIUngroupedBucket(t *testing.T) {
|
||||
cache := newBulkEventSnapshotCache()
|
||||
repo := newBulkEventAccountRepo(&Account{ID: 6, Platform: PlatformOpenAI})
|
||||
|
||||
@@ -325,8 +325,8 @@ func newGroupLifecycleTestService(cache SchedulerCache, accounts AccountReposito
|
||||
}
|
||||
|
||||
func expectedGroupLifecycleBuckets(groupID int64) []SchedulerBucket {
|
||||
platforms := []string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok}
|
||||
buckets := make([]SchedulerBucket, 0, 12)
|
||||
platforms := schedulerSnapshotPlatforms()
|
||||
buckets := make([]SchedulerBucket, 0, 18)
|
||||
for _, platform := range platforms {
|
||||
buckets = append(buckets,
|
||||
SchedulerBucket{GroupID: groupID, Platform: platform, Mode: SchedulerModeSingle},
|
||||
|
||||
@@ -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:
|
||||
case PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek:
|
||||
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() [5]string {
|
||||
return [5]string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok}
|
||||
func schedulerSnapshotPlatforms() [8]string {
|
||||
return [8]string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek}
|
||||
}
|
||||
|
||||
// 生命周期辅助函数有意排除 group0;full rebuild 构造 group0 canonical 集时必须显式调用 canonical helper。
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Allow Composite model routes to target the three concrete CN providers.
|
||||
ALTER TABLE composite_model_routes
|
||||
DROP CONSTRAINT IF EXISTS composite_model_routes_target_platform_check;
|
||||
|
||||
ALTER TABLE composite_model_routes
|
||||
ADD CONSTRAINT composite_model_routes_target_platform_check
|
||||
CHECK (target_platform IN ('anthropic', 'openai', 'gemini', 'antigravity', 'grok',
|
||||
'kimi', 'zhipu', 'deepseek'));
|
||||
@@ -0,0 +1,18 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCompositeRoutesCNProvidersMigration(t *testing.T) {
|
||||
content, err := FS.ReadFile("227_composite_routes_add_cn_providers.sql")
|
||||
require.NoError(t, err)
|
||||
|
||||
sql := strings.Join(strings.Fields(string(content)), " ")
|
||||
require.Contains(t, sql, "DROP CONSTRAINT IF EXISTS composite_model_routes_target_platform_check")
|
||||
require.Contains(t, sql,
|
||||
"CHECK (target_platform IN ('anthropic', 'openai', 'gemini', 'antigravity', 'grok', 'kimi', 'zhipu', 'deepseek'))")
|
||||
}
|
||||
Reference in New Issue
Block a user