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
+194 -49
View File
@@ -3,25 +3,36 @@ package store
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/jackc/pgx/v5"
)
const baseModelColumns = `
id::text, provider_key, canonical_model_key, provider_model_name, model_type, display_name,
id::text, provider_key, canonical_model_key, invocation_name, provider_model_name, model_type, display_name,
capabilities, base_billing_config, default_rate_limit_policy, COALESCE(pricing_rule_set_id::text, ''),
COALESCE(runtime_policy_set_id::text, ''), runtime_policy_override, metadata,
catalog_type, COALESCE(default_snapshot, '{}'::jsonb), COALESCE(customized_at::text, ''),
pricing_version, status, created_at, updated_at`
pricing_version, status, created_at, updated_at,
COALESCE((
SELECT jsonb_agg(DISTINCT compatibility_alias.alias ORDER BY compatibility_alias.alias)
FROM model_compatibility_aliases compatibility_alias
WHERE compatibility_alias.base_model_id = base_model_catalog.id
AND compatibility_alias.active = true
AND (compatibility_alias.expires_at IS NULL OR compatibility_alias.expires_at > now())
), '[]'::jsonb),
(SELECT count(*)::int FROM platform_models platform_model WHERE platform_model.base_model_id = base_model_catalog.id)`
type BaseModelInput struct {
ProviderKey string `json:"providerKey"`
CanonicalModelKey string `json:"canonicalModelKey"`
InvocationName string `json:"invocationName"`
ProviderModelName string `json:"providerModelName"`
ModelType StringList `json:"modelType"`
ModelAlias string `json:"modelAlias"`
DisplayName string `json:"displayName"`
LegacyAliases StringList `json:"legacyAliases"`
Capabilities map[string]any `json:"capabilities"`
BaseBillingConfig map[string]any `json:"baseBillingConfig"`
DefaultRateLimitPolicy map[string]any `json:"defaultRateLimitPolicy"`
@@ -86,25 +97,32 @@ func (s *Store) CreateBaseModel(ctx context.Context, input BaseModelInput) (Base
defaultSnapshot, _ := json.Marshal(emptyObjectIfNil(input.DefaultSnapshot))
modelType, _ := json.Marshal(input.ModelType)
return scanBaseModel(s.pool.QueryRow(ctx, `
tx, err := s.pool.Begin(ctx)
if err != nil {
return BaseModel{}, err
}
defer tx.Rollback(ctx)
item, err := scanBaseModel(tx.QueryRow(ctx, `
INSERT INTO base_model_catalog (
provider_id, provider_key, canonical_model_key, provider_model_name, model_type, display_name,
provider_id, provider_key, canonical_model_key, invocation_name, provider_model_name, model_type, display_name,
capabilities, base_billing_config, default_rate_limit_policy, pricing_rule_set_id, runtime_policy_set_id, runtime_policy_override,
metadata, catalog_type, default_snapshot, pricing_version, status
)
VALUES (
(SELECT id FROM model_catalog_providers WHERE provider_key = $1 OR provider_code = $1 LIMIT 1),
$1, $2, $3, $4::jsonb, $5, $6, $7, $8,
COALESCE(NULLIF($9, '')::uuid, (SELECT id FROM model_pricing_rule_sets WHERE rule_set_key = 'default-multimodal-v1' LIMIT 1)),
COALESCE(NULLIF($10, '')::uuid, (SELECT id FROM model_runtime_policy_sets WHERE policy_key = 'default-runtime-v1' LIMIT 1)),
$11, $12, NULLIF($13, ''), NULLIF($14::jsonb, '{}'::jsonb), $15, $16
$1, $2, $3, $4, $5::jsonb, $6, $7, $8, $9,
COALESCE(NULLIF($10, '')::uuid, (SELECT id FROM model_pricing_rule_sets WHERE rule_set_key = 'default-multimodal-v1' LIMIT 1)),
COALESCE(NULLIF($11, '')::uuid, (SELECT id FROM model_runtime_policy_sets WHERE policy_key = 'default-runtime-v1' LIMIT 1)),
$12, $13, NULLIF($14, ''), NULLIF($15::jsonb, '{}'::jsonb), $16, $17
)
RETURNING `+baseModelColumns,
input.ProviderKey,
input.CanonicalModelKey,
input.InvocationName,
input.ProviderModelName,
string(modelType),
input.ModelAlias,
input.DisplayName,
capabilities,
billingConfig,
rateLimitPolicy,
@@ -117,6 +135,17 @@ RETURNING `+baseModelColumns,
input.PricingVersion,
input.Status,
))
if err != nil {
return BaseModel{}, err
}
if err := replaceBaseModelAliases(ctx, tx, item.ID, input); err != nil {
return BaseModel{}, err
}
if err := tx.Commit(ctx); err != nil {
return BaseModel{}, err
}
item.LegacyAliases = input.LegacyAliases
return item, nil
}
func (s *Store) UpdateBaseModel(ctx context.Context, id string, input BaseModelInput) (BaseModel, error) {
@@ -129,35 +158,43 @@ func (s *Store) UpdateBaseModel(ctx context.Context, id string, input BaseModelI
defaultSnapshot, _ := json.Marshal(emptyObjectIfNil(input.DefaultSnapshot))
modelType, _ := json.Marshal(input.ModelType)
return scanBaseModel(s.pool.QueryRow(ctx, `
tx, err := s.pool.Begin(ctx)
if err != nil {
return BaseModel{}, err
}
defer tx.Rollback(ctx)
item, err := scanBaseModel(tx.QueryRow(ctx, `
UPDATE base_model_catalog
SET provider_id = (SELECT id FROM model_catalog_providers WHERE provider_key = $2 OR provider_code = $2 LIMIT 1),
provider_key = $2,
canonical_model_key = $3,
provider_model_name = $4,
model_type = $5::jsonb,
display_name = $6,
capabilities = $7,
base_billing_config = $8,
default_rate_limit_policy = $9,
pricing_rule_set_id = COALESCE(NULLIF($10, '')::uuid, (SELECT id FROM model_pricing_rule_sets WHERE rule_set_key = 'default-multimodal-v1' LIMIT 1)),
runtime_policy_set_id = COALESCE(NULLIF($11, '')::uuid, (SELECT id FROM model_runtime_policy_sets WHERE policy_key = 'default-runtime-v1' LIMIT 1)),
runtime_policy_override = $12,
metadata = $13,
catalog_type = NULLIF($14, ''),
default_snapshot = COALESCE(NULLIF($15::jsonb, '{}'::jsonb), default_snapshot),
customized_at = CASE WHEN NULLIF($14, '') = 'system' THEN now() ELSE NULL END,
pricing_version = $16,
status = $17,
invocation_name = $4,
provider_model_name = $5,
model_type = $6::jsonb,
display_name = $7,
capabilities = $8,
base_billing_config = $9,
default_rate_limit_policy = $10,
pricing_rule_set_id = COALESCE(NULLIF($11, '')::uuid, (SELECT id FROM model_pricing_rule_sets WHERE rule_set_key = 'default-multimodal-v1' LIMIT 1)),
runtime_policy_set_id = COALESCE(NULLIF($12, '')::uuid, (SELECT id FROM model_runtime_policy_sets WHERE policy_key = 'default-runtime-v1' LIMIT 1)),
runtime_policy_override = $13,
metadata = $14,
catalog_type = NULLIF($15, ''),
default_snapshot = COALESCE(NULLIF($16::jsonb, '{}'::jsonb), default_snapshot),
customized_at = CASE WHEN NULLIF($15, '') = 'system' THEN now() ELSE NULL END,
pricing_version = $17,
status = $18,
updated_at = now()
WHERE id = $1::uuid
RETURNING `+baseModelColumns,
id,
input.ProviderKey,
input.CanonicalModelKey,
input.InvocationName,
input.ProviderModelName,
string(modelType),
input.ModelAlias,
input.DisplayName,
capabilities,
billingConfig,
rateLimitPolicy,
@@ -170,6 +207,17 @@ RETURNING `+baseModelColumns,
input.PricingVersion,
input.Status,
))
if err != nil {
return BaseModel{}, err
}
if err := replaceBaseModelAliases(ctx, tx, item.ID, input); err != nil {
return BaseModel{}, err
}
if err := tx.Commit(ctx); err != nil {
return BaseModel{}, err
}
item.LegacyAliases = input.LegacyAliases
return item, nil
}
func (s *Store) ResetBaseModelToDefault(ctx context.Context, id string) (BaseModel, error) {
@@ -194,18 +242,19 @@ UPDATE base_model_catalog
SET provider_id = (SELECT id FROM model_catalog_providers WHERE provider_key = COALESCE($2::text, provider_key) OR provider_code = COALESCE($2::text, provider_key) LIMIT 1),
provider_key = COALESCE($2::text, provider_key),
canonical_model_key = COALESCE($3::text, canonical_model_key),
provider_model_name = COALESCE($4::text, provider_model_name),
model_type = COALESCE($5::jsonb, model_type),
display_name = COALESCE($6::text, display_name),
capabilities = COALESCE($7::jsonb, capabilities),
base_billing_config = COALESCE($8::jsonb, base_billing_config),
default_rate_limit_policy = COALESCE($9::jsonb, default_rate_limit_policy),
pricing_rule_set_id = COALESCE(NULLIF($10::text, '')::uuid, pricing_rule_set_id),
runtime_policy_set_id = COALESCE(NULLIF($11::text, '')::uuid, runtime_policy_set_id),
runtime_policy_override = COALESCE($12::jsonb, runtime_policy_override),
metadata = COALESCE($13::jsonb, metadata),
pricing_version = COALESCE($14::integer, pricing_version),
status = COALESCE($15::text, status),
invocation_name = COALESCE($4::text, invocation_name),
provider_model_name = COALESCE($5::text, provider_model_name),
model_type = COALESCE($6::jsonb, model_type),
display_name = COALESCE($7::text, display_name),
capabilities = COALESCE($8::jsonb, capabilities),
base_billing_config = COALESCE($9::jsonb, base_billing_config),
default_rate_limit_policy = COALESCE($10::jsonb, default_rate_limit_policy),
pricing_rule_set_id = COALESCE(NULLIF($11::text, '')::uuid, pricing_rule_set_id),
runtime_policy_set_id = COALESCE(NULLIF($12::text, '')::uuid, runtime_policy_set_id),
runtime_policy_override = COALESCE($13::jsonb, runtime_policy_override),
metadata = COALESCE($14::jsonb, metadata),
pricing_version = COALESCE($15::integer, pricing_version),
status = COALESCE($16::text, status),
customized_at = NULL,
updated_at = now()
WHERE id = $1::uuid
@@ -213,9 +262,10 @@ RETURNING `+baseModelColumns,
id,
stringFromSnapshot(snapshot, "providerKey"),
stringFromSnapshot(snapshot, "canonicalModelKey"),
stringFromSnapshot(snapshot, "invocationName", "modelAlias", "providerModelName"),
stringFromSnapshot(snapshot, "providerModelName"),
jsonStringListFromSnapshot(snapshot, "modelType"),
stringFromSnapshot(snapshot, "modelAlias", "displayName"),
stringFromSnapshot(snapshot, "displayName", "modelAlias", "providerModelName"),
jsonFromSnapshot(snapshot, "capabilities"),
jsonFromSnapshot(snapshot, "baseBillingConfig"),
jsonFromSnapshot(snapshot, "defaultRateLimitPolicy"),
@@ -240,13 +290,14 @@ SET provider_id = (
),
provider_key = COALESCE(NULLIF(default_snapshot->>'providerKey', ''), provider_key),
canonical_model_key = COALESCE(NULLIF(default_snapshot->>'canonicalModelKey', ''), canonical_model_key),
invocation_name = COALESCE(NULLIF(default_snapshot->>'invocationName', ''), NULLIF(default_snapshot->>'modelAlias', ''), invocation_name),
provider_model_name = COALESCE(NULLIF(default_snapshot->>'providerModelName', ''), provider_model_name),
model_type = COALESCE(NULLIF(CASE
WHEN jsonb_typeof(default_snapshot->'modelType') = 'array' THEN default_snapshot->'modelType'
WHEN COALESCE(default_snapshot->>'modelType', '') <> '' THEN jsonb_build_array(default_snapshot->>'modelType')
ELSE NULL
END, '[]'::jsonb), model_type),
display_name = COALESCE(NULLIF(COALESCE(default_snapshot->>'modelAlias', default_snapshot->>'displayName'), ''), display_name),
display_name = COALESCE(NULLIF(COALESCE(default_snapshot->>'displayName', default_snapshot->>'modelAlias'), ''), display_name),
capabilities = COALESCE(default_snapshot->'capabilities', capabilities),
base_billing_config = COALESCE(default_snapshot->'baseBillingConfig', base_billing_config),
default_rate_limit_policy = COALESCE(default_snapshot->'defaultRateLimitPolicy', default_rate_limit_policy),
@@ -269,11 +320,23 @@ RETURNING `+baseModelColumns)
}
func (s *Store) DeleteBaseModel(ctx context.Context, id string) error {
result, err := s.pool.Exec(ctx, `DELETE FROM base_model_catalog WHERE id = $1::uuid`, id)
result, err := s.pool.Exec(ctx, `
DELETE FROM base_model_catalog base_model
WHERE base_model.id = $1::uuid
AND NOT EXISTS (
SELECT 1 FROM platform_models platform_model WHERE platform_model.base_model_id = base_model.id
)`, id)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
var exists bool
if err := s.pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM base_model_catalog WHERE id = $1::uuid)`, id).Scan(&exists); err != nil {
return err
}
if exists {
return ErrBaseModelInUse
}
return pgx.ErrNoRows
}
return nil
@@ -294,7 +357,7 @@ func scanBaseModelRows(rows pgx.Rows) ([]BaseModel, error) {
func scanBaseModel(scanner baseModelScanner) (BaseModel, error) {
var item BaseModel
var modelType []byte
var modelAlias string
var legacyAliases []byte
var capabilities []byte
var billingConfig []byte
var rateLimitPolicy []byte
@@ -305,9 +368,10 @@ func scanBaseModel(scanner baseModelScanner) (BaseModel, error) {
&item.ID,
&item.ProviderKey,
&item.CanonicalModelKey,
&item.InvocationName,
&item.ProviderModelName,
&modelType,
&modelAlias,
&item.DisplayName,
&capabilities,
&billingConfig,
&rateLimitPolicy,
@@ -322,6 +386,8 @@ func scanBaseModel(scanner baseModelScanner) (BaseModel, error) {
&item.Status,
&item.CreatedAt,
&item.UpdatedAt,
&legacyAliases,
&item.ReferenceCount,
); err != nil {
return BaseModel{}, err
}
@@ -332,18 +398,20 @@ func scanBaseModel(scanner baseModelScanner) (BaseModel, error) {
item.Metadata = decodeObject(metadata)
item.DefaultSnapshot = decodeObject(defaultSnapshot)
item.ModelType = decodeStringArray(modelType)
item.ModelAlias = modelAlias
item.DisplayName = modelAlias
item.LegacyAliases = decodeStringArray(legacyAliases)
item.ModelAlias = item.InvocationName
return item, nil
}
func normalizeBaseModelInput(input BaseModelInput) BaseModelInput {
input.ProviderKey = strings.TrimSpace(input.ProviderKey)
input.CanonicalModelKey = strings.TrimSpace(input.CanonicalModelKey)
input.InvocationName = strings.TrimSpace(input.InvocationName)
input.ProviderModelName = strings.TrimSpace(input.ProviderModelName)
input.ModelType = uniqueStringList(input.ModelType)
input.ModelAlias = strings.TrimSpace(input.ModelAlias)
input.DisplayName = strings.TrimSpace(input.DisplayName)
input.LegacyAliases = normalizeCompatibilityAliases(input.LegacyAliases)
input.PricingRuleSetID = strings.TrimSpace(input.PricingRuleSetID)
input.RuntimePolicySetID = strings.TrimSpace(input.RuntimePolicySetID)
input.CatalogType = strings.TrimSpace(input.CatalogType)
@@ -351,12 +419,17 @@ func normalizeBaseModelInput(input BaseModelInput) BaseModelInput {
if input.CanonicalModelKey == "" && input.ProviderKey != "" && input.ProviderModelName != "" {
input.CanonicalModelKey = input.ProviderKey + ":" + input.ProviderModelName
}
if input.ModelAlias == "" {
input.ModelAlias = input.DisplayName
if input.InvocationName == "" {
input.InvocationName = input.ModelAlias
}
if input.ModelAlias == "" {
input.ModelAlias = input.ProviderModelName
if input.InvocationName == "" {
input.InvocationName = input.ProviderModelName
}
if input.DisplayName == "" {
input.DisplayName = input.InvocationName
}
input.ModelAlias = input.InvocationName
input.LegacyAliases = withoutStrings(input.LegacyAliases, input.InvocationName)
if len(input.ModelType) == 0 {
input.ModelType = StringList{"text_generate"}
}
@@ -372,6 +445,78 @@ func normalizeBaseModelInput(input BaseModelInput) BaseModelInput {
return input
}
func replaceBaseModelAliases(ctx context.Context, tx pgx.Tx, baseModelID string, input BaseModelInput) error {
for _, alias := range input.LegacyAliases {
for _, modelType := range input.ModelType {
var conflictingCanonicalKey string
if err := tx.QueryRow(ctx, `
SELECT COALESCE((
SELECT other.canonical_model_key
FROM base_model_catalog other
WHERE other.id <> $1::uuid
AND other.invocation_name <> $4::text
AND other.model_type ? $3::text
AND (
other.invocation_name = $2::text
OR EXISTS (
SELECT 1
FROM model_compatibility_aliases other_alias
WHERE other_alias.base_model_id = other.id
AND other_alias.alias = $2::text
AND other_alias.model_type = $3::text
AND other_alias.active = true
AND (other_alias.expires_at IS NULL OR other_alias.expires_at > now())
)
)
LIMIT 1
), '')`, baseModelID, alias, modelType, input.InvocationName).Scan(&conflictingCanonicalKey); err != nil {
return err
}
if conflictingCanonicalKey != "" {
return fmt.Errorf("%w: %q (%s) conflicts with %s", ErrModelAliasConflict, alias, modelType, conflictingCanonicalKey)
}
}
}
if _, err := tx.Exec(ctx, `DELETE FROM model_compatibility_aliases WHERE base_model_id = $1::uuid`, baseModelID); err != nil {
return err
}
for _, alias := range input.LegacyAliases {
for _, modelType := range input.ModelType {
if _, err := tx.Exec(ctx, `
INSERT INTO model_compatibility_aliases (base_model_id, alias, model_type, expires_at)
VALUES ($1::uuid, $2, $3, now() + interval '14 days')`, baseModelID, alias, modelType); err != nil {
return err
}
}
}
return nil
}
func normalizeCompatibilityAliases(values []string) StringList {
out := make(StringList, 0, len(values))
seen := map[string]bool{}
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" || seen[value] {
continue
}
seen[value] = true
out = append(out, value)
}
return out
}
func withoutStrings(values []string, excluded string) StringList {
out := make(StringList, 0, len(values))
for _, value := range values {
if value != excluded {
out = append(out, value)
}
}
return out
}
func stringFromSnapshot(snapshot map[string]any, keys ...string) any {
for _, key := range keys {
value, ok := snapshot[key]