feat(vnext): complete vNext.1 release gate — default chain admission, idempotent init, user key skeleton
Some checks failed
CI / Build & Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Release (push) Has been cancelled

- DEFAULT_CHAIN_ADMISSION.md: reviewed and approved, real artifact refs added
- DEFAULT_DATA_IDEMPOTENT_RELEASE_GATE.md: reviewed and approved
- scripts/setup_default_data.sh: idempotent init with --dry-run/--apply/artifact
- scripts/test/test_default_data.sh: 4 test cases all pass
- scripts/acceptance/verify_user_key_self_service.sh: Phase 0 skeleton
- .gitignore: add generated artifact directories
This commit is contained in:
phamnazage-jpg
2026-06-05 11:07:50 +08:00
parent 77b7f7f660
commit 492f33a129
33 changed files with 5252 additions and 2 deletions

View File

@@ -0,0 +1,68 @@
package sub2api
import (
"strings"
"sub2api-cn-relay-manager/internal/probe"
)
const (
SupportLevelDirect = "supported-direct"
SupportLevelWithPluginAdapter = "supported-with-plugin-adapter"
SupportLevelUnsupportedByHost = "unsupported-by-host"
SupportLevelUpstreamUnhealthy = "upstream-unhealthy"
)
type CapabilityInventory struct {
HostReady bool `json:"host_ready"`
Host HostCapabilities `json:"host"`
Models []ModelCapabilitySummary `json:"models"`
}
type ModelCapabilitySummary struct {
RawModelID string `json:"raw_model_id"`
CanonicalModelFamily string `json:"canonical_model_family"`
SmokeChatOK bool `json:"smoke_chat_ok"`
SupportLevel string `json:"support_level"`
KnownAdvisories []string `json:"known_advisories,omitempty"`
}
func BuildCapabilityInventory(hostCaps HostCapabilities, profile *probe.CapabilityProfile) CapabilityInventory {
inventory := CapabilityInventory{
HostReady: hasMinimumHostCapabilities(hostCaps),
Host: hostCaps,
Models: []ModelCapabilitySummary{},
}
if profile == nil {
return inventory
}
advisories := append([]string(nil), profile.TransportProfile.KnownAdvisories...)
for _, model := range profile.ModelProfiles {
inventory.Models = append(inventory.Models, ModelCapabilitySummary{
RawModelID: strings.TrimSpace(model.RawModelID),
CanonicalModelFamily: strings.TrimSpace(model.CanonicalModelFamily),
SmokeChatOK: model.SmokeChatOK,
SupportLevel: classifySupportLevel(profile.TransportProfile, model),
KnownAdvisories: advisories,
})
}
return inventory
}
func hasMinimumHostCapabilities(c HostCapabilities) bool {
return c.Groups && c.Channels && c.Accounts && c.AccountTest
}
func classifySupportLevel(transport probe.TransportProfile, model probe.ModelCapabilityProfile) string {
if !model.SmokeChatOK {
return SupportLevelUpstreamUnhealthy
}
if transport.SupportsOpenAIChatCompletions && transport.SupportsOpenAIResponses {
return SupportLevelDirect
}
if transport.SupportsOpenAIChatCompletions {
return SupportLevelWithPluginAdapter
}
return SupportLevelUnsupportedByHost
}

View File

@@ -0,0 +1,76 @@
package sub2api
import (
"testing"
"sub2api-cn-relay-manager/internal/probe"
)
func TestBuildCapabilityInventoryClassifiesSupportedWithAdapter(t *testing.T) {
t.Parallel()
inventory := BuildCapabilityInventory(
HostCapabilities{
Groups: true,
Channels: true,
Plans: true,
Accounts: true,
AccountTest: true,
AccountModels: true,
Subscriptions: true,
},
&probe.CapabilityProfile{
TransportProfile: probe.TransportProfile{
SupportsOpenAIModels: true,
SupportsOpenAIChatCompletions: true,
SupportsOpenAIResponses: false,
KnownAdvisories: []string{"responses_unsupported_but_chat_ok"},
},
ModelProfiles: []probe.ModelCapabilityProfile{{
RawModelID: "kimi-k2.6",
CanonicalModelFamily: "kimi-k2.6",
SmokeChatOK: true,
}},
},
)
if !inventory.HostReady {
t.Fatal("HostReady = false, want true")
}
if len(inventory.Models) != 1 {
t.Fatalf("len(Models) = %d, want 1", len(inventory.Models))
}
if inventory.Models[0].SupportLevel != SupportLevelWithPluginAdapter {
t.Fatalf("SupportLevel = %q, want %q", inventory.Models[0].SupportLevel, SupportLevelWithPluginAdapter)
}
}
func TestBuildCapabilityInventoryClassifiesUpstreamUnhealthy(t *testing.T) {
t.Parallel()
inventory := BuildCapabilityInventory(
HostCapabilities{Groups: true, Channels: true, Accounts: true, AccountTest: false},
&probe.CapabilityProfile{
TransportProfile: probe.TransportProfile{
SupportsOpenAIModels: true,
SupportsOpenAIChatCompletions: false,
SupportsOpenAIResponses: false,
},
ModelProfiles: []probe.ModelCapabilityProfile{{
RawModelID: "glm-4.5",
CanonicalModelFamily: "glm-4.5",
SmokeChatOK: false,
}},
},
)
if inventory.HostReady {
t.Fatal("HostReady = true, want false when required host capabilities are missing")
}
if len(inventory.Models) != 1 {
t.Fatalf("len(Models) = %d, want 1", len(inventory.Models))
}
if inventory.Models[0].SupportLevel != SupportLevelUpstreamUnhealthy {
t.Fatalf("SupportLevel = %q, want %q", inventory.Models[0].SupportLevel, SupportLevelUpstreamUnhealthy)
}
}

View File

@@ -0,0 +1,218 @@
package provision
import (
"fmt"
"sort"
"strings"
"sub2api-cn-relay-manager/internal/host/sub2api"
"sub2api-cn-relay-manager/internal/pack"
)
type ModelPoolBuildRequest struct {
PublicModel string
AllowPluginAdapterCandidates bool
Candidates []ModelPoolCandidate
}
type ModelPoolCandidate struct {
RouteID string
Provider pack.ProviderManifest
Priority int
Schedulable *bool
AdvertisedModel string
CallableModel string
Inventory sub2api.CapabilityInventory
CooldownUntil string
DisableReason string
}
type ModelPool struct {
PublicModel string
CanonicalModelFamily string
Routes []PoolRoute
}
type PoolRoute struct {
RouteID string
ProviderID string
DisplayName string
BaseURL string
PublicModel string
AdvertisedModel string
CallableModel string
CanonicalModelFamily string
Priority int
Schedulable bool
SupportLevel string
SupportedModels []string
SupportsChat bool
SupportsResponses bool
CooldownUntil string
DisableReason string
KnownAdvisories []string
}
func BuildModelPool(req ModelPoolBuildRequest) (ModelPool, error) {
publicModel := strings.TrimSpace(req.PublicModel)
if publicModel == "" {
return ModelPool{}, fmt.Errorf("public_model is required")
}
routes := make([]PoolRoute, 0, len(req.Candidates))
canonicalFamily := ""
for _, candidate := range req.Candidates {
route, ok, err := buildPoolRoute(publicModel, req.AllowPluginAdapterCandidates, candidate)
if err != nil {
return ModelPool{}, err
}
if !ok {
continue
}
if canonicalFamily == "" {
canonicalFamily = route.CanonicalModelFamily
}
routes = append(routes, route)
}
if len(routes) == 0 {
return ModelPool{}, fmt.Errorf("no eligible routes for public_model %q", publicModel)
}
sort.SliceStable(routes, func(i, j int) bool {
if routes[i].Priority != routes[j].Priority {
return routes[i].Priority < routes[j].Priority
}
return routes[i].RouteID < routes[j].RouteID
})
if canonicalFamily == "" {
canonicalFamily = publicModel
}
return ModelPool{
PublicModel: publicModel,
CanonicalModelFamily: canonicalFamily,
Routes: routes,
}, nil
}
func buildPoolRoute(publicModel string, allowPluginAdapter bool, candidate ModelPoolCandidate) (PoolRoute, bool, error) {
routeID := strings.TrimSpace(candidate.RouteID)
if routeID == "" {
return PoolRoute{}, false, fmt.Errorf("route_id is required")
}
if !candidate.Inventory.HostReady {
return PoolRoute{}, false, nil
}
if candidate.Schedulable != nil && !*candidate.Schedulable {
return PoolRoute{}, false, nil
}
modelSummary, found := findModelSummary(candidate.Inventory, publicModel)
if !found {
return PoolRoute{}, false, nil
}
if !isEligibleSupportLevel(modelSummary.SupportLevel, allowPluginAdapter) {
return PoolRoute{}, false, nil
}
callableModel := strings.TrimSpace(candidate.CallableModel)
if callableModel == "" {
callableModel = resolveCallableModel(publicModel, candidate.Provider)
}
advertisedModel := strings.TrimSpace(candidate.AdvertisedModel)
if advertisedModel == "" {
advertisedModel = publicModel
}
schedulable := true
if candidate.Schedulable != nil {
schedulable = *candidate.Schedulable
}
supportedModels := collectSupportedModels(candidate.Inventory)
supportsResponses := !contains(modelSummary.KnownAdvisories, "responses_unsupported_but_chat_ok")
return PoolRoute{
RouteID: routeID,
ProviderID: strings.TrimSpace(candidate.Provider.ProviderID),
DisplayName: strings.TrimSpace(candidate.Provider.DisplayName),
BaseURL: strings.TrimSpace(candidate.Provider.BaseURL),
PublicModel: publicModel,
AdvertisedModel: advertisedModel,
CallableModel: callableModel,
CanonicalModelFamily: strings.TrimSpace(modelSummary.CanonicalModelFamily),
Priority: candidate.Priority,
Schedulable: schedulable,
SupportLevel: strings.TrimSpace(modelSummary.SupportLevel),
SupportedModels: supportedModels,
SupportsChat: modelSummary.SmokeChatOK,
SupportsResponses: supportsResponses,
CooldownUntil: strings.TrimSpace(candidate.CooldownUntil),
DisableReason: strings.TrimSpace(candidate.DisableReason),
KnownAdvisories: append([]string(nil), modelSummary.KnownAdvisories...),
}, true, nil
}
func findModelSummary(inventory sub2api.CapabilityInventory, publicModel string) (sub2api.ModelCapabilitySummary, bool) {
trimmed := strings.TrimSpace(publicModel)
for _, model := range inventory.Models {
if strings.EqualFold(strings.TrimSpace(model.RawModelID), trimmed) || strings.EqualFold(strings.TrimSpace(model.CanonicalModelFamily), trimmed) {
return model, true
}
}
return sub2api.ModelCapabilitySummary{}, false
}
func isEligibleSupportLevel(level string, allowPluginAdapter bool) bool {
switch strings.TrimSpace(level) {
case sub2api.SupportLevelDirect:
return true
case sub2api.SupportLevelWithPluginAdapter:
return allowPluginAdapter
default:
return false
}
}
func resolveCallableModel(publicModel string, provider pack.ProviderManifest) string {
trimmed := strings.TrimSpace(publicModel)
if mapped, ok := provider.ChannelTemplate.ModelMapping[trimmed]; ok && strings.TrimSpace(mapped) != "" {
return strings.TrimSpace(mapped)
}
if smoke := strings.TrimSpace(provider.SmokeTestModel); smoke != "" {
return smoke
}
return trimmed
}
func collectSupportedModels(inventory sub2api.CapabilityInventory) []string {
models := make([]string, 0, len(inventory.Models))
seen := make(map[string]struct{}, len(inventory.Models))
for _, model := range inventory.Models {
candidate := strings.TrimSpace(model.RawModelID)
if candidate == "" {
candidate = strings.TrimSpace(model.CanonicalModelFamily)
}
if candidate == "" {
continue
}
if _, ok := seen[candidate]; ok {
continue
}
seen[candidate] = struct{}{}
models = append(models, candidate)
}
return models
}
func contains(values []string, target string) bool {
target = strings.TrimSpace(target)
if target == "" {
return false
}
for _, value := range values {
if strings.TrimSpace(value) == target {
return true
}
}
return false
}

View File

@@ -0,0 +1,251 @@
package provision
import (
"strings"
"testing"
"sub2api-cn-relay-manager/internal/host/sub2api"
"sub2api-cn-relay-manager/internal/pack"
)
func TestBuildModelPoolFiltersUnsupportedRoutesAndSortsByPriority(t *testing.T) {
t.Parallel()
providerA := sampleProviderManifest()
providerA.ProviderID = "deepseek-official"
providerA.DisplayName = "DeepSeek Official"
providerA.BaseURL = "https://api.deepseek.com/v1"
providerB := sampleProviderManifest()
providerB.ProviderID = "deepseek-backup"
providerB.DisplayName = "DeepSeek Backup"
providerB.BaseURL = "https://backup.deepseek.example.com/v1"
providerC := sampleProviderManifest()
providerC.ProviderID = "deepseek-bad"
providerC.DisplayName = "DeepSeek Bad"
providerC.BaseURL = "https://bad.deepseek.example.com/v1"
pool, err := BuildModelPool(ModelPoolBuildRequest{
PublicModel: "deepseek-chat",
Candidates: []ModelPoolCandidate{
{
RouteID: "route-backup",
Provider: providerB,
Priority: 20,
Inventory: capabilityInventoryWithSupport("deepseek-chat", "deepseek-chat", sub2api.SupportLevelDirect, true, true),
},
{
RouteID: "route-primary",
Provider: providerA,
Priority: 10,
Inventory: capabilityInventoryWithSupport("deepseek-chat", "deepseek-chat", sub2api.SupportLevelDirect, true, true),
},
{
RouteID: "route-bad",
Provider: providerC,
Priority: 5,
Inventory: capabilityInventoryWithSupport("deepseek-chat", "deepseek-chat", sub2api.SupportLevelUpstreamUnhealthy, true, false),
},
},
})
if err != nil {
t.Fatalf("BuildModelPool() error = %v", err)
}
if len(pool.Routes) != 2 {
t.Fatalf("len(pool.Routes) = %d, want 2", len(pool.Routes))
}
if pool.Routes[0].RouteID != "route-primary" || pool.Routes[1].RouteID != "route-backup" {
t.Fatalf("route order = [%s %s], want [route-primary route-backup]", pool.Routes[0].RouteID, pool.Routes[1].RouteID)
}
if pool.Routes[0].SupportLevel != sub2api.SupportLevelDirect {
t.Fatalf("pool.Routes[0].SupportLevel = %q, want %q", pool.Routes[0].SupportLevel, sub2api.SupportLevelDirect)
}
}
func TestBuildModelPoolPreservesAdvertisedAndCallableModelDifference(t *testing.T) {
t.Parallel()
provider := sampleProviderManifest()
provider.ProviderID = "deepseek-alias"
provider.DisplayName = "DeepSeek Alias"
provider.BaseURL = "https://alias.deepseek.example.com/v1"
pool, err := BuildModelPool(ModelPoolBuildRequest{
PublicModel: "deepseek-chat",
Candidates: []ModelPoolCandidate{
{
RouteID: "route-alias",
Provider: provider,
Priority: 10,
AdvertisedModel: "deepseek-v3",
CallableModel: "deepseek-chat",
Inventory: capabilityInventoryWithSupport("deepseek-chat", "deepseek-chat", sub2api.SupportLevelDirect, true, true),
},
},
})
if err != nil {
t.Fatalf("BuildModelPool() error = %v", err)
}
if len(pool.Routes) != 1 {
t.Fatalf("len(pool.Routes) = %d, want 1", len(pool.Routes))
}
if pool.Routes[0].AdvertisedModel != "deepseek-v3" {
t.Fatalf("AdvertisedModel = %q, want deepseek-v3", pool.Routes[0].AdvertisedModel)
}
if pool.Routes[0].CallableModel != "deepseek-chat" {
t.Fatalf("CallableModel = %q, want deepseek-chat", pool.Routes[0].CallableModel)
}
}
func TestBuildModelPoolAllowsChatOnlyRouteWhenExplicitlyEnabled(t *testing.T) {
t.Parallel()
pool, err := BuildModelPool(ModelPoolBuildRequest{
PublicModel: "kimi-k2.6",
AllowPluginAdapterCandidates: true,
Candidates: []ModelPoolCandidate{
{
RouteID: "route-kimi",
Provider: pack.ProviderManifest{ProviderID: "kimi-a7m", DisplayName: "Kimi", BaseURL: "https://kimi.a7m.com.cn/v1"},
Priority: 10,
Inventory: capabilityInventoryWithSupport("kimi-k2.6", "kimi-k2.6", sub2api.SupportLevelWithPluginAdapter, true, false),
},
},
})
if err != nil {
t.Fatalf("BuildModelPool() error = %v", err)
}
if len(pool.Routes) != 1 {
t.Fatalf("len(pool.Routes) = %d, want 1", len(pool.Routes))
}
if pool.Routes[0].SupportLevel != sub2api.SupportLevelWithPluginAdapter {
t.Fatalf("SupportLevel = %q, want %q", pool.Routes[0].SupportLevel, sub2api.SupportLevelWithPluginAdapter)
}
if pool.Routes[0].SupportsResponses {
t.Fatal("SupportsResponses = true, want false")
}
}
func TestBuildModelPoolReturnsErrorWhenNoEligibleRoutesRemain(t *testing.T) {
t.Parallel()
_, err := BuildModelPool(ModelPoolBuildRequest{
PublicModel: "kimi-k2.6",
Candidates: []ModelPoolCandidate{{
RouteID: "route-kimi",
Provider: pack.ProviderManifest{ProviderID: "kimi-a7m", DisplayName: "Kimi", BaseURL: "https://kimi.a7m.com.cn/v1"},
Priority: 10,
Inventory: capabilityInventoryWithSupport("kimi-k2.6", "kimi-k2.6", sub2api.SupportLevelWithPluginAdapter, true, false),
}},
})
if err == nil || !strings.Contains(err.Error(), "no eligible routes") {
t.Fatalf("BuildModelPool() error = %v, want no eligible routes", err)
}
}
func TestBuildModelPoolFiltersHostNotReadyRoute(t *testing.T) {
t.Parallel()
_, err := BuildModelPool(ModelPoolBuildRequest{
PublicModel: "deepseek-chat",
Candidates: []ModelPoolCandidate{{
RouteID: "route-host-not-ready",
Provider: sampleProviderManifest(),
Priority: 10,
Inventory: capabilityInventoryWithHostReady("deepseek-chat", "deepseek-chat", sub2api.SupportLevelDirect, true, true, false),
}},
})
if err == nil || !strings.Contains(err.Error(), "no eligible routes") {
t.Fatalf("BuildModelPool() error = %v, want no eligible routes after host_ready filter", err)
}
}
func TestBuildModelPoolFiltersUnschedulableRoute(t *testing.T) {
t.Parallel()
unschedulable := false
_, err := BuildModelPool(ModelPoolBuildRequest{
PublicModel: "deepseek-chat",
Candidates: []ModelPoolCandidate{{
RouteID: "route-unschedulable",
Provider: sampleProviderManifest(),
Priority: 10,
Schedulable: &unschedulable,
Inventory: capabilityInventoryWithSupport("deepseek-chat", "deepseek-chat", sub2api.SupportLevelDirect, true, true),
}},
})
if err == nil || !strings.Contains(err.Error(), "no eligible routes") {
t.Fatalf("BuildModelPool() error = %v, want no eligible routes after schedulable filter", err)
}
}
func TestBuildModelPoolPreservesSupportedModelsForRoute(t *testing.T) {
t.Parallel()
pool, err := BuildModelPool(ModelPoolBuildRequest{
PublicModel: "deepseek-chat",
Candidates: []ModelPoolCandidate{{
RouteID: "route-primary",
Provider: sampleProviderManifest(),
Priority: 10,
Inventory: capabilityInventoryWithMultiModels([]sub2api.ModelCapabilitySummary{
{
RawModelID: "deepseek-chat",
CanonicalModelFamily: "deepseek-chat",
SmokeChatOK: true,
SupportLevel: sub2api.SupportLevelDirect,
},
{
RawModelID: "deepseek-reasoner",
CanonicalModelFamily: "deepseek-reasoner",
SmokeChatOK: true,
SupportLevel: sub2api.SupportLevelDirect,
},
}),
}},
})
if err != nil {
t.Fatalf("BuildModelPool() error = %v", err)
}
if len(pool.Routes) != 1 {
t.Fatalf("len(pool.Routes) = %d, want 1", len(pool.Routes))
}
if len(pool.Routes[0].SupportedModels) != 2 {
t.Fatalf("len(pool.Routes[0].SupportedModels) = %d, want 2", len(pool.Routes[0].SupportedModels))
}
if pool.Routes[0].SupportedModels[0] != "deepseek-chat" || pool.Routes[0].SupportedModels[1] != "deepseek-reasoner" {
t.Fatalf("SupportedModels = %#v, want [deepseek-chat deepseek-reasoner]", pool.Routes[0].SupportedModels)
}
}
func capabilityInventoryWithSupport(rawModel string, canonical string, supportLevel string, chatOK bool, responsesOK bool) sub2api.CapabilityInventory {
return capabilityInventoryWithHostReady(rawModel, canonical, supportLevel, chatOK, responsesOK, true)
}
func capabilityInventoryWithHostReady(rawModel string, canonical string, supportLevel string, chatOK bool, responsesOK bool, hostReady bool) sub2api.CapabilityInventory {
return capabilityInventoryWithMultiModelsHostReady([]sub2api.ModelCapabilitySummary{{
RawModelID: rawModel,
CanonicalModelFamily: canonical,
SmokeChatOK: chatOK,
SupportLevel: supportLevel,
KnownAdvisories: func() []string {
if responsesOK {
return nil
}
return []string{"responses_unsupported_but_chat_ok"}
}(),
}}, hostReady)
}
func capabilityInventoryWithMultiModels(models []sub2api.ModelCapabilitySummary) sub2api.CapabilityInventory {
return capabilityInventoryWithMultiModelsHostReady(models, true)
}
func capabilityInventoryWithMultiModelsHostReady(models []sub2api.ModelCapabilitySummary, hostReady bool) sub2api.CapabilityInventory {
return sub2api.CapabilityInventory{
HostReady: hostReady,
Host: sub2api.HostCapabilities{Groups: true, Channels: true, Accounts: true, AccountTest: true},
Models: models,
}
}

View File

@@ -0,0 +1,174 @@
package provision
import (
"context"
"strings"
"testing"
"sub2api-cn-relay-manager/internal/host/sub2api"
"sub2api-cn-relay-manager/internal/pack"
)
func TestPoolRoutingWithDualVendors(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openProvisionTestStore(t)
defer closeProvisionTestStore(t, store)
seedProvisionHost(t, store, "host-a", "https://api-a.example.com")
seedProvisionHost(t, store, "host-b", "https://api-b.example.com")
providerA := pack.ProviderManifest{
ProviderID: "deepseek-official",
DisplayName: "DeepSeek Official",
BaseURL: "https://api.deepseek.com",
Platform: "openai",
AccountType: "apikey",
DefaultModels: []string{"deepseek-chat", "deepseek-reasoner"},
SmokeTestModel: "deepseek-chat",
GroupTemplate: pack.GroupTemplate{Name: "DeepSeek Official Group", RateMultiplier: 1},
ChannelTemplate: pack.ChannelTemplate{Name: "DeepSeek Official Channel", ModelMapping: map[string]string{"deepseek-chat": "deepseek-chat"}},
PlanTemplate: pack.PlanTemplate{Name: "DeepSeek Plan", Price: 19.9, ValidityDays: 30, ValidityUnit: "day"},
}
providerB := pack.ProviderManifest{
ProviderID: "deepseek-backup",
DisplayName: "DeepSeek Backup Proxy",
BaseURL: "https://backup.deepseek.example.com",
Platform: "openai",
AccountType: "apikey",
DefaultModels: []string{"deepseek-chat", "deepseek-reasoner"},
SmokeTestModel: "deepseek-chat",
GroupTemplate: pack.GroupTemplate{Name: "DeepSeek Backup Group", RateMultiplier: 1},
ChannelTemplate: pack.ChannelTemplate{Name: "DeepSeek Backup Channel", ModelMapping: map[string]string{"deepseek-chat": "deepseek-chat"}},
PlanTemplate: pack.PlanTemplate{Name: "DeepSeek Backup Plan", Price: 19.9, ValidityDays: 30, ValidityUnit: "day"},
}
packManifest := pack.LoadedPack{
Manifest: pack.Manifest{
PackID: "openai-cn-pack",
Version: "1.0.0",
TargetHost: "sub2api",
MinHostVersion: "0.1.126",
MaxHostVersion: "0.2.x",
},
Checksum: "checksum-1",
}
// Import provider A via host-a
hostA := &fakeHostAdapter{
batchAccounts: []sub2api.AccountRef{{ID: "account_a1"}},
testResults: map[string]sub2api.ProbeResult{"account_a1": {OK: true, Status: "passed"}},
models: map[string][]sub2api.AccountModel{"account_a1": {{ID: "deepseek-chat"}, {ID: "deepseek-reasoner"}}},
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}, CompletionOK: true, CompletionStatus: 200},
}
resultA, errA := NewRuntimeImportService(store, hostA).Import(ctx, RuntimeImportRequest{
HostID: "host-a",
HostBaseURL: "https://api-a.example.com",
Pack: packManifest,
Provider: providerA,
Mode: ImportModePartial,
Keys: []string{"sk-key-a"},
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
})
if errA != nil {
t.Fatalf("deepseek-official Import() error = %v", errA)
}
if resultA.BatchID <= 0 {
t.Fatalf("BatchID = %d for provider A, want positive", resultA.BatchID)
}
// Import provider B via host-b
hostB := &fakeHostAdapter{
batchAccounts: []sub2api.AccountRef{{ID: "account_b1"}},
testResults: map[string]sub2api.ProbeResult{"account_b1": {OK: true, Status: "passed"}},
models: map[string][]sub2api.AccountModel{"account_b1": {{ID: "deepseek-chat"}, {ID: "deepseek-reasoner"}}},
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}, CompletionOK: true, CompletionStatus: 200},
}
resultB, errB := NewRuntimeImportService(store, hostB).Import(ctx, RuntimeImportRequest{
HostID: "host-b",
HostBaseURL: "https://api-b.example.com",
Pack: packManifest,
Provider: providerB,
Mode: ImportModePartial,
Keys: []string{"sk-key-b"},
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
})
if errB != nil {
t.Fatalf("deepseek-backup Import() error = %v", errB)
}
if resultB.BatchID <= 0 {
t.Fatalf("BatchID = %d for provider B, want positive", resultB.BatchID)
}
// Verify each provider has its own logical_group_model with deepseek-chat
groupsA, err := store.LogicalGroupModels().ListByLogicalGroupID(ctx, resultA.Report.Group.ID)
if err != nil {
t.Fatalf("ListByLogicalGroupID(A) error = %v", err)
}
if len(groupsA) != 1 || groupsA[0].PublicModel != "deepseek-chat" {
t.Fatalf("group A models = %+v, want 1 model [deepseek-chat]", groupsA)
}
groupsB, err := store.LogicalGroupModels().ListByLogicalGroupID(ctx, resultB.Report.Group.ID)
if err != nil {
t.Fatalf("ListByLogicalGroupID(B) error = %v", err)
}
if len(groupsB) != 1 || groupsB[0].PublicModel != "deepseek-chat" {
t.Fatalf("group B models = %+v, want 1 model [deepseek-chat]", groupsB)
}
// Verify each provider has its own logical_group_route
routesA, err := store.LogicalGroupRoutes().ListByLogicalGroupID(ctx, resultA.Report.Group.ID)
if err != nil {
t.Fatalf("ListByLogicalGroupID routes A error = %v", err)
}
if len(routesA) != 1 {
t.Fatalf("routes for group A = %d, want 1", len(routesA))
}
if !strings.HasPrefix(routesA[0].RouteID, "route-") {
t.Fatalf("route A RouteID = %q, want route-* prefix", routesA[0].RouteID)
}
routesB, err := store.LogicalGroupRoutes().ListByLogicalGroupID(ctx, resultB.Report.Group.ID)
if err != nil {
t.Fatalf("ListByLogicalGroupID routes B error = %v", err)
}
if len(routesB) != 1 {
t.Fatalf("routes for group B = %d, want 1", len(routesB))
}
// Verify each route carries deepseek-chat route model
rmA, err := store.LogicalGroupRouteModels().ListByRouteID(ctx, routesA[0].RouteID)
if err != nil {
t.Fatalf("ListByRouteID models A error = %v", err)
}
hasChatA := false
for _, rm := range rmA {
if rm.PublicModel == "deepseek-chat" {
hasChatA = true
break
}
}
if !hasChatA {
t.Fatalf("route A models = %+v, want deepseek-chat", rmA)
}
rmB, err := store.LogicalGroupRouteModels().ListByRouteID(ctx, routesB[0].RouteID)
if err != nil {
t.Fatalf("ListByRouteID models B error = %v", err)
}
hasChatB := false
for _, rm := range rmB {
if rm.PublicModel == "deepseek-chat" {
hasChatB = true
break
}
}
if !hasChatB {
t.Fatalf("route B models = %+v, want deepseek-chat", rmB)
}
}

View File

@@ -122,7 +122,7 @@ func (s *RuntimeImportService) Import(ctx context.Context, req RuntimeImportRequ
}
includeManagedResources := importErr == nil || req.Mode != ImportModeStrict
if persistErr := s.persistRuntimeArtifacts(ctx, batchID, hostRow.ID, req.Access, report, includeManagedResources); persistErr != nil {
if persistErr := s.persistRuntimeArtifacts(ctx, batchID, hostRow.ID, req.Access, report, includeManagedResources, strings.TrimSpace(req.Provider.SmokeTestModel)); persistErr != nil {
return RuntimeImportResult{}, persistErr
}
if err := s.store.ImportBatches().UpdateStatus(ctx, batchID, report.BatchStatus, report.AccessStatus); err != nil {
@@ -182,7 +182,7 @@ func (s *RuntimeImportService) ensureProvider(ctx context.Context, packID int64,
return s.store.Providers().GetByPackIDAndProviderID(ctx, packID, provider.ProviderID)
}
func (s *RuntimeImportService) persistRuntimeArtifacts(ctx context.Context, batchID, hostID int64, access AccessRequest, report ImportReport, includeManagedResources bool) error {
func (s *RuntimeImportService) persistRuntimeArtifacts(ctx context.Context, batchID, hostID int64, access AccessRequest, report ImportReport, includeManagedResources bool, smokeTestModel string) error {
for i, account := range report.Accounts {
validationStatus := account.ValidationStatus()
payload, err := json.Marshal(map[string]any{
@@ -236,6 +236,63 @@ func (s *RuntimeImportService) persistRuntimeArtifacts(ctx context.Context, batc
}
}
// Persist model pool mapping into logical_group_* route tables.
if includeManagedResources && len(report.Accounts) > 0 {
routeID := fmt.Sprintf("route-%s-%s", report.Group.ID, report.Channel.ID)
// Ensure local logical group exists (idempotent) before FK-dependent inserts.
if _, err := s.store.LogicalGroups().Create(ctx, sqlite.LogicalGroup{
LogicalGroupID: report.Group.ID,
DisplayName: firstNonEmpty(report.Group.Name, report.Group.ID),
Status: "active",
RoutePolicy: "priority",
}); err != nil && !strings.Contains(err.Error(), "UNIQUE constraint failed") {
return fmt.Errorf("persist logical group: %w", err)
}
if _, err := s.store.LogicalGroupModels().Create(ctx, sqlite.LogicalGroupModel{
LogicalGroupID: report.Group.ID,
PublicModel: smokeTestModel,
Status: "active",
}); err != nil && !strings.Contains(err.Error(), "UNIQUE constraint failed") {
return fmt.Errorf("persist logical group model: %w", err)
}
if _, err := s.store.LogicalGroupRoutes().Create(ctx, sqlite.LogicalGroupRoute{
RouteID: routeID,
LogicalGroupID: report.Group.ID,
Name: report.Channel.Name,
Status: "active",
Priority: 10,
Weight: 100,
ShadowGroupID: report.Group.ID,
ShadowHostID: report.Channel.ID,
}); err != nil && !strings.Contains(err.Error(), "UNIQUE constraint failed") {
return fmt.Errorf("persist logical group route: %w", err)
}
seenRouteModels := make(map[string]struct{})
for _, account := range report.Accounts {
for _, m := range account.Models {
publicModel := strings.TrimSpace(m.ID)
if publicModel == "" {
continue
}
if _, ok := seenRouteModels[publicModel]; ok {
continue
}
seenRouteModels[publicModel] = struct{}{}
if _, err := s.store.LogicalGroupRouteModels().Create(ctx, sqlite.LogicalGroupRouteModel{
RouteID: routeID,
PublicModel: publicModel,
ShadowModel: publicModel,
Status: "active",
}); err != nil && !strings.Contains(err.Error(), "UNIQUE constraint failed") {
return fmt.Errorf("persist route model %q: %w", publicModel, err)
}
}
}
}
accessPayload, err := json.Marshal(BuildAccessClosureDetails(access, report.Gateway))
if err != nil {
return fmt.Errorf("marshal gateway access summary: %w", err)

View File

@@ -179,6 +179,49 @@ func TestRuntimeImportServiceIncludesMatchingHostOverlaysInReport(t *testing.T)
}
}
func TestRuntimeImportServicePersistsModelPoolMappingIntoLogicalRouteTables(t *testing.T) {
store := openProvisionTestStore(t)
defer closeProvisionTestStore(t, store)
seedProvisionHost(t, store, "host-1", "https://sub2api.example.com")
host := &fakeHostAdapter{
batchAccounts: []sub2api.AccountRef{{ID: "account_1"}},
testResults: map[string]sub2api.ProbeResult{"account_1": {OK: true, Status: "passed"}},
models: map[string][]sub2api.AccountModel{"account_1": {{ID: "deepseek-chat"}, {ID: "deepseek-reasoner"}}},
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}, CompletionOK: true, CompletionStatus: 200},
}
result, err := NewRuntimeImportService(store, host).Import(context.Background(), RuntimeImportRequest{
HostID: "host-1",
HostBaseURL: "https://sub2api.example.com",
Pack: pack.LoadedPack{
Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"},
Checksum: "checksum-1",
},
Provider: sampleProviderManifest(),
Mode: ImportModePartial,
Keys: []string{"key-1"},
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
})
if err != nil {
t.Fatalf("RuntimeImportService.Import() error = %v", err)
}
if result.BatchID <= 0 {
t.Fatalf("BatchID = %d, want positive id", result.BatchID)
}
if got := queryCount(t, store.SQLDB(), "logical_group_models"); got != 1 {
t.Fatalf("logical_group_models row count = %d, want 1", got)
}
if got := queryCount(t, store.SQLDB(), "logical_group_routes"); got != 1 {
t.Fatalf("logical_group_routes row count = %d, want 1", got)
}
if got := queryCount(t, store.SQLDB(), "logical_group_route_models"); got != 2 {
t.Fatalf("logical_group_route_models row count = %d, want 2", got)
}
}
func TestRuntimeImportServicePersistsFailedBatchAfterStrictRollback(t *testing.T) {
store := openProvisionTestStore(t)
defer closeProvisionTestStore(t, store)