fix(provision): reconcile channel pricing and hosted access
This commit is contained in:
@@ -157,6 +157,7 @@ func validateProviders(providers []ProviderManifest) error {
|
||||
seen := make(map[string]struct{}, len(providers))
|
||||
for _, provider := range providers {
|
||||
providerID := strings.TrimSpace(provider.ProviderID)
|
||||
missingDefaultModel := firstMissingDefaultModel(provider.DefaultModels, provider.ChannelTemplate.ModelMapping)
|
||||
switch {
|
||||
case providerID == "":
|
||||
return fmt.Errorf("provider manifest: provider_id is required")
|
||||
@@ -180,6 +181,10 @@ func validateProviders(providers []ProviderManifest) error {
|
||||
return fmt.Errorf("provider %q: channel_template.name is required", providerID)
|
||||
case len(provider.ChannelTemplate.ModelMapping) == 0:
|
||||
return fmt.Errorf("provider %q: channel_template.model_mapping must not be empty", providerID)
|
||||
case !containsProviderModel(provider.ChannelTemplate.ModelMapping, provider.SmokeTestModel):
|
||||
return fmt.Errorf("provider %q: channel_template.model_mapping must include smoke_test_model %q", providerID, provider.SmokeTestModel)
|
||||
case missingDefaultModel != "":
|
||||
return fmt.Errorf("provider %q: channel_template.model_mapping must cover default_models, missing %q", providerID, missingDefaultModel)
|
||||
case strings.TrimSpace(provider.PlanTemplate.Name) == "":
|
||||
return fmt.Errorf("provider %q: plan_template.name is required", providerID)
|
||||
case provider.PlanTemplate.ValidityDays <= 0:
|
||||
@@ -247,3 +252,29 @@ func contains(items []string, target string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsProviderModel(modelMapping map[string]string, target string) bool {
|
||||
trimmedTarget := strings.TrimSpace(target)
|
||||
if trimmedTarget == "" {
|
||||
return false
|
||||
}
|
||||
for sourceModel, mappedModel := range modelMapping {
|
||||
if strings.TrimSpace(sourceModel) == trimmedTarget || strings.TrimSpace(mappedModel) == trimmedTarget {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func firstMissingDefaultModel(defaultModels []string, modelMapping map[string]string) string {
|
||||
for _, model := range defaultModels {
|
||||
trimmedModel := strings.TrimSpace(model)
|
||||
if trimmedModel == "" {
|
||||
continue
|
||||
}
|
||||
if !containsProviderModel(modelMapping, trimmedModel) {
|
||||
return trimmedModel
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestLoadDirParsesAndValidatesPack(t *testing.T) {
|
||||
"default_models": ["deepseek-chat", "deepseek-reasoner"],
|
||||
"smoke_test_model": "deepseek-chat",
|
||||
"group_template": {"name": "DeepSeek 默认分组", "rate_multiplier": 1.0},
|
||||
"channel_template": {"name": "DeepSeek 默认渠道", "model_mapping": {"deepseek-chat": "deepseek-chat"}},
|
||||
"channel_template": {"name": "DeepSeek 默认渠道", "model_mapping": {"deepseek-chat": "deepseek-chat", "deepseek-reasoner": "deepseek-reasoner"}},
|
||||
"plan_template": {"name": "DeepSeek 默认套餐", "price": 19.9, "validity_days": 30, "validity_unit": "day"},
|
||||
"import": {"supports_multi_key": true, "supports_strict": true, "supports_partial": true}
|
||||
}`,
|
||||
@@ -82,6 +82,36 @@ func TestLoadDirRejectsInvalidProviderSchema(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDirRejectsSmokeTestModelMissingFromChannelMapping(t *testing.T) {
|
||||
packDir := createPackFixture(t, map[string]string{
|
||||
"pack.json": `{"pack_id":"openai-cn-pack","version":"1.0.0","vendor":"x","target_host":"sub2api","min_host_version":"0.1.126","max_host_version":"0.2.x","providers_dir":"providers","checksum_file":"checksums.txt"}`,
|
||||
"providers/deepseek.json": `{"provider_id":"deepseek","display_name":"DeepSeek","base_url":"https://api.deepseek.com","platform":"openai","account_type":"apikey","default_models":["deepseek-v4-pro","deepseek-v4-flash"],"smoke_test_model":"deepseek-v4-pro","group_template":{"name":"g","rate_multiplier":1},"channel_template":{"name":"c","model_mapping":{"deepseek-chat":"deepseek-chat","deepseek-reasoner":"deepseek-reasoner"}},"plan_template":{"name":"p","price":1,"validity_days":30,"validity_unit":"day"},"import":{"supports_multi_key":true,"supports_strict":true,"supports_partial":true}}`,
|
||||
})
|
||||
|
||||
_, err := LoadDir(packDir)
|
||||
if err == nil {
|
||||
t.Fatal("LoadDir() error = nil, want smoke_test_model channel mapping validation failure")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "channel_template.model_mapping") || !strings.Contains(err.Error(), "smoke_test_model") {
|
||||
t.Fatalf("LoadDir() error = %v, want smoke_test_model channel mapping detail", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDirRejectsDefaultModelsMissingFromChannelMapping(t *testing.T) {
|
||||
packDir := createPackFixture(t, map[string]string{
|
||||
"pack.json": `{"pack_id":"openai-cn-pack","version":"1.0.0","vendor":"x","target_host":"sub2api","min_host_version":"0.1.126","max_host_version":"0.2.x","providers_dir":"providers","checksum_file":"checksums.txt"}`,
|
||||
"providers/minimax.json": `{"provider_id":"minimax","display_name":"MiniMax","base_url":"https://api.minimax.example.com","platform":"openai","account_type":"apikey","default_models":["MiniMax-M2.5-highspeed","MiniMax-M2.7-highspeed"],"smoke_test_model":"MiniMax-M2.7-highspeed","group_template":{"name":"g","rate_multiplier":1},"channel_template":{"name":"c","model_mapping":{"MiniMax-M2.7-highspeed":"MiniMax-M2.7-highspeed"}},"plan_template":{"name":"p","price":1,"validity_days":30,"validity_unit":"day"},"import":{"supports_multi_key":true,"supports_strict":true,"supports_partial":true}}`,
|
||||
})
|
||||
|
||||
_, err := LoadDir(packDir)
|
||||
if err == nil {
|
||||
t.Fatal("LoadDir() error = nil, want default_models channel mapping validation failure")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "default_models") || !strings.Contains(err.Error(), "channel_template.model_mapping") {
|
||||
t.Fatalf("LoadDir() error = %v, want default_models channel mapping detail", err)
|
||||
}
|
||||
}
|
||||
|
||||
func createPackFixture(t *testing.T, files map[string]string) string {
|
||||
t.Helper()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user