-- 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);