feat(catalog): 统一模型调用身份并梳理生命周期
新增官方调用名、供应商真实名、显示名和兼容别名的独立契约,调整路由、目录聚合、管理端与 OpenAPI。 增加 Gemini、Qwen、DeepSeek、Claude 和 MiniMax 生命周期迁移、别名观测及引用保护,并补充数据库与聚合测试。
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
-- Gemini models that Google has retired or superseded stay callable only through
|
||||
-- the compatibility window; they are no longer eligible for new catalog cards.
|
||||
UPDATE base_model_catalog
|
||||
SET status = 'deprecated',
|
||||
metadata = metadata || jsonb_build_object(
|
||||
'officialModelId', invocation_name,
|
||||
'officialDisplayName', COALESCE(NULLIF(display_name, ''), invocation_name),
|
||||
'lifecycleStage', 'deprecated',
|
||||
'replacementModel', CASE
|
||||
WHEN invocation_name = 'gemini-2.0-flash-exp-image-generation' THEN 'gemini-3.1-flash-image'
|
||||
WHEN invocation_name = 'gemini-3-pro-preview' THEN 'gemini-3.1-pro-preview'
|
||||
WHEN invocation_name = 'gemini-3.1-flash-lite-preview' THEN 'gemini-3.5-flash-lite'
|
||||
WHEN invocation_name = 'gemini-3-pro-image-preview' THEN 'gemini-3-pro-image'
|
||||
WHEN invocation_name = 'gemini-3.1-flash-image-preview' THEN 'gemini-3.1-flash-image'
|
||||
WHEN invocation_name = 'gemini-3-flash-preview' THEN 'gemini-3.6-flash'
|
||||
ELSE metadata->>'replacementModel'
|
||||
END,
|
||||
'retireAt', '2026-07-22',
|
||||
'sourceUrl', 'https://ai.google.dev/gemini-api/docs/deprecations',
|
||||
'verifiedAt', '2026-07-22'
|
||||
),
|
||||
updated_at = now()
|
||||
WHERE catalog_type = 'system'
|
||||
AND customized_at IS NULL
|
||||
AND invocation_name IN (
|
||||
'gemini-2.0-flash-exp-image-generation',
|
||||
'gemini-3-pro-preview',
|
||||
'gemini-3.1-flash-lite-preview',
|
||||
'gemini-3-pro-image-preview',
|
||||
'gemini-3.1-flash-image-preview',
|
||||
'gemini-3-flash-preview'
|
||||
);
|
||||
|
||||
WITH targets(model_id, display_name, template_model, fallback_type, lifecycle_stage) AS (
|
||||
VALUES
|
||||
('gemini-3.1-pro-preview', 'Gemini 3.1 Pro Preview', 'gemini-3-pro-preview', 'text_generate', 'preview'),
|
||||
('gemini-3.5-flash-lite', 'Gemini 3.5 Flash-Lite', 'gemini-3.1-flash-lite-preview', 'text_generate', 'stable'),
|
||||
('gemini-3-pro-image', 'Gemini 3 Pro Image', 'gemini-3-pro-image-preview', 'image_generate', 'stable'),
|
||||
('gemini-3.1-flash-image', 'Gemini 3.1 Flash Image', 'gemini-3.1-flash-image-preview', 'image_generate', 'stable'),
|
||||
('gemini-3.6-flash', 'Gemini 3.6 Flash', 'gemini-3-flash-preview', 'text_generate', 'stable')
|
||||
)
|
||||
INSERT INTO base_model_catalog (
|
||||
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, pricing_version, status
|
||||
)
|
||||
SELECT provider.id,
|
||||
'gemini',
|
||||
'gemini:' || target.model_id,
|
||||
target.model_id,
|
||||
target.model_id,
|
||||
COALESCE(template.model_type, jsonb_build_array(target.fallback_type)),
|
||||
target.display_name,
|
||||
COALESCE(template.capabilities, '{}'::jsonb),
|
||||
COALESCE(template.base_billing_config, '{}'::jsonb),
|
||||
COALESCE(template.default_rate_limit_policy, '{}'::jsonb),
|
||||
template.pricing_rule_set_id,
|
||||
template.runtime_policy_set_id,
|
||||
COALESCE(template.runtime_policy_override, '{}'::jsonb),
|
||||
jsonb_build_object(
|
||||
'officialModelId', target.model_id,
|
||||
'officialDisplayName', target.display_name,
|
||||
'lifecycleStage', target.lifecycle_stage,
|
||||
'sourceUrl', 'https://ai.google.dev/gemini-api/docs/deprecations',
|
||||
'verifiedAt', '2026-07-22',
|
||||
'bindingStatus', 'unverified'
|
||||
),
|
||||
'system',
|
||||
COALESCE(template.pricing_version, 1),
|
||||
'active'
|
||||
FROM targets target
|
||||
LEFT JOIN model_catalog_providers provider ON provider.provider_key = 'gemini' OR provider.provider_code = 'gemini'
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT source.model_type, source.capabilities, source.base_billing_config,
|
||||
source.default_rate_limit_policy, source.pricing_rule_set_id,
|
||||
source.runtime_policy_set_id, source.runtime_policy_override, source.pricing_version
|
||||
FROM base_model_catalog source
|
||||
WHERE source.invocation_name = target.template_model
|
||||
ORDER BY CASE WHEN source.provider_key = 'gemini' THEN 0 ELSE 1 END, source.updated_at DESC
|
||||
LIMIT 1
|
||||
) template ON true
|
||||
ON CONFLICT (canonical_model_key) DO UPDATE
|
||||
SET invocation_name = CASE WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.invocation_name ELSE base_model_catalog.invocation_name END,
|
||||
provider_model_name = CASE WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.provider_model_name ELSE base_model_catalog.provider_model_name END,
|
||||
display_name = CASE WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.display_name ELSE base_model_catalog.display_name END,
|
||||
metadata = CASE WHEN base_model_catalog.customized_at IS NULL THEN base_model_catalog.metadata || EXCLUDED.metadata ELSE base_model_catalog.metadata END,
|
||||
status = CASE WHEN base_model_catalog.customized_at IS NULL THEN 'active' ELSE base_model_catalog.status END,
|
||||
updated_at = now();
|
||||
|
||||
-- Alibaba Cloud announced the following Qwen IDs for retirement by 2026-10-10.
|
||||
UPDATE base_model_catalog
|
||||
SET status = 'deprecated',
|
||||
metadata = metadata || jsonb_build_object(
|
||||
'officialModelId', invocation_name,
|
||||
'officialDisplayName', COALESCE(NULLIF(display_name, ''), invocation_name),
|
||||
'lifecycleStage', 'deprecated',
|
||||
'replacementModel', CASE
|
||||
WHEN invocation_name = 'qwen3-max' THEN 'qwen3.7-max'
|
||||
WHEN invocation_name = 'qwen-turbo' THEN 'qwen3.7-plus'
|
||||
WHEN invocation_name IN ('qwen-vl-max', 'qwen-vl-plus') THEN 'qwen3.6-flash'
|
||||
WHEN invocation_name = 'qwen3-235b-a22b' THEN 'qwen3.7-plus'
|
||||
ELSE metadata->>'replacementModel'
|
||||
END,
|
||||
'retireAt', '2026-10-10',
|
||||
'sourceUrl', 'https://help.aliyun.com/en/model-studio/model-depreciation',
|
||||
'verifiedAt', '2026-07-22'
|
||||
),
|
||||
updated_at = now()
|
||||
WHERE catalog_type = 'system'
|
||||
AND customized_at IS NULL
|
||||
AND invocation_name IN ('qwen3-max', 'qwen-turbo', 'qwen-vl-max', 'qwen-vl-plus', 'qwen3-235b-a22b');
|
||||
|
||||
WITH targets(model_id, display_name, template_model, fallback_type) AS (
|
||||
VALUES
|
||||
('qwen3.7-max', 'Qwen3.7-Max', 'qwen3-max', 'text_generate'),
|
||||
('qwen3.7-plus', 'Qwen3.7-Plus', 'qwen3-235b-a22b', 'text_generate'),
|
||||
('qwen3.6-flash', 'Qwen3.6-Flash', 'qwen-vl-plus', 'text_generate')
|
||||
)
|
||||
INSERT INTO base_model_catalog (
|
||||
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, pricing_version, status
|
||||
)
|
||||
SELECT provider.id,
|
||||
'aliyun',
|
||||
'aliyun:' || target.model_id,
|
||||
target.model_id,
|
||||
target.model_id,
|
||||
COALESCE(template.model_type, jsonb_build_array(target.fallback_type)),
|
||||
target.display_name,
|
||||
COALESCE(template.capabilities, '{}'::jsonb),
|
||||
COALESCE(template.base_billing_config, '{}'::jsonb),
|
||||
COALESCE(template.default_rate_limit_policy, '{}'::jsonb),
|
||||
template.pricing_rule_set_id,
|
||||
template.runtime_policy_set_id,
|
||||
COALESCE(template.runtime_policy_override, '{}'::jsonb),
|
||||
jsonb_build_object(
|
||||
'officialModelId', target.model_id,
|
||||
'officialDisplayName', target.display_name,
|
||||
'lifecycleStage', 'stable',
|
||||
'sourceUrl', 'https://help.aliyun.com/en/model-studio/model-depreciation',
|
||||
'verifiedAt', '2026-07-22',
|
||||
'bindingStatus', 'unverified'
|
||||
),
|
||||
'system',
|
||||
COALESCE(template.pricing_version, 1),
|
||||
'active'
|
||||
FROM targets target
|
||||
LEFT JOIN model_catalog_providers provider ON provider.provider_key = 'aliyun' OR provider.provider_code = 'aliyun'
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT source.model_type, source.capabilities, source.base_billing_config,
|
||||
source.default_rate_limit_policy, source.pricing_rule_set_id,
|
||||
source.runtime_policy_set_id, source.runtime_policy_override, source.pricing_version
|
||||
FROM base_model_catalog source
|
||||
WHERE source.invocation_name = target.template_model
|
||||
ORDER BY CASE WHEN source.provider_key = 'aliyun' THEN 0 ELSE 1 END, source.updated_at DESC
|
||||
LIMIT 1
|
||||
) template ON true
|
||||
ON CONFLICT (canonical_model_key) DO UPDATE
|
||||
SET invocation_name = CASE WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.invocation_name ELSE base_model_catalog.invocation_name END,
|
||||
provider_model_name = CASE WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.provider_model_name ELSE base_model_catalog.provider_model_name END,
|
||||
display_name = CASE WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.display_name ELSE base_model_catalog.display_name END,
|
||||
metadata = CASE WHEN base_model_catalog.customized_at IS NULL THEN base_model_catalog.metadata || EXCLUDED.metadata ELSE base_model_catalog.metadata END,
|
||||
status = CASE WHEN base_model_catalog.customized_at IS NULL THEN 'active' ELSE base_model_catalog.status END,
|
||||
updated_at = now();
|
||||
|
||||
-- DeepSeek direct API has two official public IDs. Aggregator-only V4 names are
|
||||
-- kept but explicitly classified as provider identities.
|
||||
UPDATE base_model_catalog
|
||||
SET metadata = metadata || jsonb_build_object(
|
||||
'officialModelId', invocation_name,
|
||||
'officialDisplayName', COALESCE(NULLIF(display_name, ''), invocation_name),
|
||||
'lifecycleStage', 'stable',
|
||||
'sourceUrl', 'https://api-docs.deepseek.com/quick_start/pricing-details-usd',
|
||||
'verifiedAt', '2026-07-22',
|
||||
'identityAuthority', 'official'
|
||||
),
|
||||
updated_at = now()
|
||||
WHERE invocation_name IN ('deepseek-chat', 'deepseek-reasoner');
|
||||
|
||||
UPDATE base_model_catalog
|
||||
SET metadata = metadata || jsonb_build_object(
|
||||
'lifecycleStage', 'provider',
|
||||
'verifiedAt', '2026-07-22',
|
||||
'identityAuthority', 'provider'
|
||||
),
|
||||
updated_at = now()
|
||||
WHERE lower(invocation_name) LIKE 'deepseek-v4-%';
|
||||
|
||||
-- OpenRouter keeps its namespaced upstream ID, while callers use Anthropic's
|
||||
-- official model ID.
|
||||
WITH claude_mapping(provider_model_name, invocation_name, display_name) AS (
|
||||
VALUES
|
||||
('anthropic/claude-opus-4.5', 'claude-opus-4-5', 'Claude Opus 4.5'),
|
||||
('anthropic/claude-opus-4.6', 'claude-opus-4-6', 'Claude Opus 4.6'),
|
||||
('anthropic/claude-sonnet-4.5', 'claude-sonnet-4-5', 'Claude Sonnet 4.5'),
|
||||
('anthropic/claude-sonnet-4.6', 'claude-sonnet-4-6', 'Claude Sonnet 4.6'),
|
||||
('anthropic/claude-haiku-4.5', 'claude-haiku-4-5', 'Claude Haiku 4.5')
|
||||
), affected AS (
|
||||
SELECT base.id, base.invocation_name AS old_invocation_name, mapping.invocation_name, mapping.display_name
|
||||
FROM base_model_catalog base
|
||||
JOIN claude_mapping mapping ON mapping.provider_model_name = base.provider_model_name
|
||||
WHERE base.catalog_type = 'system' AND base.customized_at IS NULL
|
||||
), old_aliases AS (
|
||||
INSERT INTO model_compatibility_aliases (base_model_id, alias, model_type, expires_at)
|
||||
SELECT affected.id, affected.old_invocation_name, model_type.value, now() + interval '14 days'
|
||||
FROM affected
|
||||
JOIN base_model_catalog base ON base.id = affected.id
|
||||
CROSS JOIN LATERAL jsonb_array_elements_text(base.model_type) AS model_type(value)
|
||||
WHERE affected.old_invocation_name <> affected.invocation_name
|
||||
ON CONFLICT (base_model_id, alias, model_type) DO NOTHING
|
||||
)
|
||||
UPDATE base_model_catalog base
|
||||
SET invocation_name = affected.invocation_name,
|
||||
display_name = affected.display_name,
|
||||
metadata = base.metadata || jsonb_build_object(
|
||||
'officialModelId', affected.invocation_name,
|
||||
'officialDisplayName', affected.display_name,
|
||||
'lifecycleStage', 'stable',
|
||||
'sourceUrl', 'https://platform.claude.com/docs/en/about-claude/models/model-ids-and-versions',
|
||||
'verifiedAt', '2026-07-22',
|
||||
'identityAuthority', 'official'
|
||||
),
|
||||
updated_at = now()
|
||||
FROM affected
|
||||
WHERE base.id = affected.id;
|
||||
|
||||
WITH ranked_targets AS (
|
||||
SELECT platform_model.id,
|
||||
base.invocation_name,
|
||||
COALESCE(NULLIF(base.display_name, ''), base.invocation_name) AS display_name,
|
||||
row_number() OVER (
|
||||
PARTITION BY platform_model.platform_id, base.invocation_name
|
||||
ORDER BY CASE WHEN platform_model.model_name = base.invocation_name THEN 0 ELSE 1 END,
|
||||
platform_model.created_at ASC,
|
||||
platform_model.id ASC
|
||||
) AS identity_rank
|
||||
FROM platform_models platform_model
|
||||
JOIN base_model_catalog base ON base.id = platform_model.base_model_id
|
||||
)
|
||||
UPDATE platform_models platform_model
|
||||
SET model_name = target.invocation_name,
|
||||
model_alias = target.invocation_name,
|
||||
display_name = target.display_name,
|
||||
updated_at = now()
|
||||
FROM ranked_targets target
|
||||
WHERE target.id = platform_model.id
|
||||
AND target.identity_rank = 1
|
||||
AND platform_model.model_name <> target.invocation_name
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM platform_models conflict
|
||||
WHERE conflict.platform_id = platform_model.platform_id
|
||||
AND conflict.id <> platform_model.id
|
||||
AND conflict.model_name = target.invocation_name
|
||||
);
|
||||
|
||||
-- MiniMax keeps current main and fallback families; older text/speech previews
|
||||
-- remain present only for compatibility and reference-safe migration.
|
||||
UPDATE base_model_catalog
|
||||
SET status = 'deprecated',
|
||||
metadata = metadata || jsonb_build_object(
|
||||
'lifecycleStage', 'deprecated',
|
||||
'replacementModel', CASE
|
||||
WHEN lower(invocation_name) LIKE '%speech-2.5%' THEN 'speech-2.8'
|
||||
ELSE 'minimax-m2.5'
|
||||
END,
|
||||
'sourceUrl', 'https://platform.minimaxi.com/docs/api-reference/api-overview',
|
||||
'verifiedAt', '2026-07-22'
|
||||
),
|
||||
updated_at = now()
|
||||
WHERE catalog_type = 'system'
|
||||
AND customized_at IS NULL
|
||||
AND (
|
||||
lower(invocation_name) IN ('minimax-m2', 'minimax-m2.1', 'minimax-m2-highspeed', 'minimax-m2.1-highspeed')
|
||||
OR lower(invocation_name) LIKE '%speech-2.5%preview%'
|
||||
);
|
||||
|
||||
-- Consolidate duplicate qwen-max rows conservatively. A row is hidden only
|
||||
-- after every safe platform binding has moved to the selected keeper.
|
||||
WITH ranked AS (
|
||||
SELECT base.id,
|
||||
first_value(base.id) OVER (
|
||||
PARTITION BY base.provider_key, base.invocation_name
|
||||
ORDER BY (SELECT count(*) FROM platform_models model WHERE model.base_model_id = base.id) DESC,
|
||||
CASE WHEN base.customized_at IS NULL THEN 0 ELSE 1 END,
|
||||
base.created_at ASC,
|
||||
base.id ASC
|
||||
) AS keeper_id
|
||||
FROM base_model_catalog base
|
||||
WHERE lower(base.invocation_name) = 'qwen-max'
|
||||
), rebound AS (
|
||||
UPDATE platform_models model
|
||||
SET base_model_id = ranked.keeper_id,
|
||||
updated_at = now()
|
||||
FROM ranked
|
||||
WHERE model.base_model_id = ranked.id
|
||||
AND ranked.id <> ranked.keeper_id
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM platform_models keeper_model
|
||||
WHERE keeper_model.platform_id = model.platform_id
|
||||
AND keeper_model.base_model_id = ranked.keeper_id
|
||||
)
|
||||
RETURNING model.id
|
||||
)
|
||||
UPDATE base_model_catalog base
|
||||
SET status = 'hidden',
|
||||
metadata = metadata || jsonb_build_object(
|
||||
'lifecycleStage', 'duplicate',
|
||||
'cleanupCandidate', true,
|
||||
'verifiedAt', '2026-07-22'
|
||||
),
|
||||
updated_at = now()
|
||||
FROM ranked
|
||||
WHERE base.id = ranked.id
|
||||
AND ranked.id <> ranked.keeper_id
|
||||
AND base.catalog_type = 'system'
|
||||
AND base.customized_at IS NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM platform_models model WHERE model.base_model_id = base.id);
|
||||
|
||||
-- Release B may physically delete only candidates which remain unreferenced
|
||||
-- after the observation window. Custom and manually modified rows are excluded.
|
||||
UPDATE base_model_catalog base
|
||||
SET metadata = metadata || jsonb_build_object(
|
||||
'cleanupCandidate', true,
|
||||
'cleanupEligibleAfter', '2026-08-05'
|
||||
),
|
||||
updated_at = now()
|
||||
WHERE base.status IN ('deprecated', 'hidden')
|
||||
AND base.catalog_type = 'system'
|
||||
AND base.customized_at IS NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM platform_models model WHERE model.base_model_id = base.id);
|
||||
Reference in New Issue
Block a user