feat(catalog): 统一模型调用身份并梳理生命周期

新增官方调用名、供应商真实名、显示名和兼容别名的独立契约,调整路由、目录聚合、管理端与 OpenAPI。

增加 Gemini、Qwen、DeepSeek、Claude 和 MiniMax 生命周期迁移、别名观测及引用保护,并补充数据库与聚合测试。
This commit is contained in:
2026-07-22 20:23:36 +08:00
parent e3f6c0fa3e
commit 8d86c4b3b3
20 changed files with 1282 additions and 284 deletions
@@ -131,13 +131,24 @@ func TestBuildModelCatalogKeepsDisplayNameSeparateFromCallAlias(t *testing.T) {
platforms := []store.Platform{
{ID: "platform-volces", Provider: "volces", Name: "火山引擎", Status: "enabled"},
}
baseModels := []store.BaseModel{
{
ID: "base-seedream-pro",
CanonicalModelKey: "seedream:seedream-5.0-pro",
InvocationName: "seedream-5.0-pro",
ProviderModelName: "doubao-seedream-5-0-pro-260628",
DisplayName: "Seedream 5.0 Pro",
Status: "active",
},
}
models[0].BaseModelID = "base-seedream-pro"
response := buildModelCatalog(models, platforms, nil, nil, nil, nil, nil)
response := buildModelCatalog(models, platforms, nil, nil, nil, nil, baseModels)
if len(response.Items) != 1 {
t.Fatalf("expected one Seedream Pro catalog item, got %+v", response.Items)
}
item := response.Items[0]
if item.Alias != "Seedream-5.0-Pro" || item.DisplayName != "Seedream 5.0 Pro" || item.ModelName != "Seedream-5.0-Pro" {
if item.Alias != "seedream-5.0-pro" || item.DisplayName != "Seedream 5.0 Pro" || item.ModelName != "seedream-5.0-pro" {
t.Fatalf("catalog should separate the call alias from the display name, got %+v", item)
}
if strings.Contains(item.Alias, " ") {
@@ -145,6 +156,46 @@ func TestBuildModelCatalogKeepsDisplayNameSeparateFromCallAlias(t *testing.T) {
}
}
func TestBuildModelCatalogAggregatesByInvocationNameNotDisplayName(t *testing.T) {
models := []store.PlatformModel{
{ID: "official-a", PlatformID: "platform-a", BaseModelID: "base-a", ModelName: "provider/a", ModelType: store.StringList{"text_generate"}, DisplayName: "同名营销标题", Enabled: true},
{ID: "official-b", PlatformID: "platform-b", BaseModelID: "base-b", ModelName: "provider/b", ModelType: store.StringList{"text_generate"}, DisplayName: "同名营销标题", Enabled: true},
{ID: "official-a-mirror", PlatformID: "platform-b", BaseModelID: "base-a-mirror", ModelName: "mirror/a", ModelType: store.StringList{"text_generate"}, DisplayName: "另一个展示标题", Enabled: true},
}
platforms := []store.Platform{
{ID: "platform-a", Provider: "provider-a", Status: "enabled"},
{ID: "platform-b", Provider: "provider-b", Status: "enabled"},
}
baseModels := []store.BaseModel{
{ID: "base-a", CanonicalModelKey: "provider-a:official-a", InvocationName: "official-a", DisplayName: "模型 A", Status: "active"},
{ID: "base-a-mirror", CanonicalModelKey: "provider-b:official-a", InvocationName: "official-a", DisplayName: "模型 A 镜像", Status: "active"},
{ID: "base-b", CanonicalModelKey: "provider-b:official-b", InvocationName: "official-b", DisplayName: "同名营销标题", Status: "active"},
}
response := buildModelCatalog(models, platforms, nil, nil, nil, nil, baseModels)
if response.Summary.ModelCount != 2 || response.Summary.SourceCount != 3 {
t.Fatalf("expected two official identities and three sources, got %+v", response.Summary)
}
for _, item := range response.Items {
if item.ModelName == "official-a" && item.SourceCount != 2 {
t.Fatalf("official-a mirrors should aggregate into one card: %+v", item)
}
}
}
func TestBuildModelCatalogExcludesDeprecatedBaseModels(t *testing.T) {
models := []store.PlatformModel{
{ID: "preview", PlatformID: "platform", BaseModelID: "base-preview", ModelName: "gemini-preview", ModelType: store.StringList{"text_generate"}, Enabled: true},
}
platforms := []store.Platform{{ID: "platform", Provider: "gemini", Status: "enabled"}}
baseModels := []store.BaseModel{{ID: "base-preview", InvocationName: "gemini-preview", Status: "deprecated"}}
response := buildModelCatalog(models, platforms, nil, nil, nil, nil, baseModels)
if response.Summary.ModelCount != 0 || response.Summary.SourceCount != 0 {
t.Fatalf("deprecated base models must not create new catalog cards: %+v", response.Summary)
}
}
func TestBuildModelCatalogUsesBaseModelProviderForProviderFilters(t *testing.T) {
models := []store.PlatformModel{
{