diff --git a/apps/web/src/lib/ai-gateway/providers/gateway-models-cache.test.ts b/apps/web/src/lib/ai-gateway/providers/gateway-models-cache.test.ts index 1cb9c9af78..b3f15e8d06 100644 --- a/apps/web/src/lib/ai-gateway/providers/gateway-models-cache.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/gateway-models-cache.test.ts @@ -76,6 +76,34 @@ describe('isValidOpenRouterModelId', () => { mockLimit.mockReset(); }); + it('accepts retained legacy aliases without consulting the database', async () => { + const { isValidOpenRouterModelId } = await loadValidator(); + + await expect(isValidOpenRouterModelId('gpt-4o')).resolves.toBe(true); + expect(mockLimit).not.toHaveBeenCalled(); + }); + + it.each(['openai/gpt-4o-mini-transcribe', 'openai/gpt-4o-transcribe'])( + 'rejects removed transcription alias %s', + async modelId => { + const { isValidOpenRouterModelId } = await loadValidator(); + mockLimit.mockResolvedValue([ + { models: { 'openai/gpt-4o': storedModel({ id: 'openai/gpt-4o' }) } }, + ]); + + await expect(isValidOpenRouterModelId(modelId)).resolves.toBe(false); + } + ); + + it('rejects other legacy aliases that are not retained', async () => { + const { isValidOpenRouterModelId } = await loadValidator(); + mockLimit.mockResolvedValue([ + { models: { 'openai/gpt-4o': storedModel({ id: 'openai/gpt-4o' }) } }, + ]); + + await expect(isValidOpenRouterModelId('gpt-4o-2024-08-06')).resolves.toBe(false); + }); + it('accepts ids present in the database catalog', async () => { const { isValidOpenRouterModelId } = await loadValidator(); mockLimit.mockResolvedValue([ diff --git a/apps/web/src/lib/ai-gateway/providers/gateway-models-cache.ts b/apps/web/src/lib/ai-gateway/providers/gateway-models-cache.ts index e75fcd8053..e1b231261f 100644 --- a/apps/web/src/lib/ai-gateway/providers/gateway-models-cache.ts +++ b/apps/web/src/lib/ai-gateway/providers/gateway-models-cache.ts @@ -93,7 +93,45 @@ export const getOpenRouterModelsFromDatabase = createLanguageModelIdsFetcher( getOpenRouterModelsMetadataFromDatabase ); +// Undocumented aliases that remain in active use but are absent from OpenRouter's model catalog. +const legacyOpenRouterAliases: ReadonlySet = new Set([ + 'anthropic/claude-haiku-4-5', + 'anthropic/claude-sonnet-4-5', + 'anthropic/claude-sonnet-4-6', + 'anthropic/claude-sonnet-5-20260630', + 'claude-opus-5', + 'claude-sonnet-4', + 'claude-sonnet-4.5', + 'claude-sonnet-5', + 'deepseek-v4-flash', + 'deepseek-v4-flash-0731', + 'deepseek-v4-pro', + 'gemini-2.5-flash-lite', + 'glm-5.1', + 'glm-5.2', + 'gpt-4.1-mini', + 'gpt-4o', + 'gpt-4o-mini', + 'gpt-5.2', + 'gpt-5.2-codex', + 'gpt-5.4', + 'gpt-5.4-mini', + 'gpt-5.5', + 'gpt-5.6-luna', + 'gpt-5.6-sol', + 'gpt-5.6-terra', + 'kimi-k3', + 'mimo-v2.5', + 'minimax-m2.5', + 'minimax-m3', + 'minimax/minimax-m2.5-20260211', + 'step-3.5-flash', +]); + export async function isValidOpenRouterModelId(modelId: string): Promise { + if (legacyOpenRouterAliases.has(modelId)) { + return true; + } const openRouterModelIds = await getOpenRouterModelsFromDatabase(); if (openRouterModelIds.size === 0) { warnExceptInTest( diff --git a/apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.test.ts b/apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.test.ts index 8d8a701790..3e1c7eb270 100644 --- a/apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.test.ts @@ -88,21 +88,14 @@ describe('mapModelIdToVercel', () => { ['mistralai/mistral-large-2512', 'mistral/mistral-large-3'], ['mistralai/mistral-medium-3-5', 'mistral/mistral-medium-3.5'], ['mistralai/mistral-small-2603', 'mistral/mistral-small'], - ['mistralai/pixtral-large-2411', 'mistral/pixtral-large'], ['qwen/qwen3-14b', 'alibaba/qwen-3-14b'], ['qwen/qwen3-235b-a22b', 'alibaba/qwen-3-235b'], ['qwen/qwen3-30b-a3b', 'alibaba/qwen-3-30b'], ['qwen/qwen3-32b', 'alibaba/qwen-3-32b'], - ['qwen3.8-max', 'alibaba/qwen3.8-max'], ['anthropic/claude-haiku-4-5', 'anthropic/claude-haiku-4.5'], - ['anthropic/claude-opus-4-6', 'anthropic/claude-opus-4.6'], ['anthropic/claude-sonnet-4-5', 'anthropic/claude-sonnet-4.5'], ['anthropic/claude-sonnet-4-6', 'anthropic/claude-sonnet-4.6'], ['anthropic/claude-sonnet-5-20260630', 'anthropic/claude-sonnet-5'], - ['claude-fable-5', 'anthropic/claude-fable-5'], - ['claude-haiku-4.5', 'anthropic/claude-haiku-4.5'], - ['claude-haiku-4-5-20251001', 'anthropic/claude-haiku-4.5'], - ['claude-opus-4-8', 'anthropic/claude-opus-4.8'], ['claude-opus-5', 'anthropic/claude-opus-5'], ['claude-sonnet-4', 'anthropic/claude-sonnet-4'], ['claude-sonnet-4.5', 'anthropic/claude-sonnet-4.5'], @@ -110,54 +103,36 @@ describe('mapModelIdToVercel', () => { ['deepseek-v4-flash', 'deepseek/deepseek-v4-flash'], ['deepseek-v4-flash-0731', 'deepseek/deepseek-v4-flash-0731'], ['deepseek-v4-pro', 'deepseek/deepseek-v4-pro'], - ['gemini-2.5-flash', 'google/gemini-2.5-flash'], ['gemini-2.5-flash-lite', 'google/gemini-2.5-flash-lite'], - ['gemini-2.5-pro', 'google/gemini-2.5-pro'], - ['gemini-3-pro-image', 'google/gemini-3-pro-image'], - ['gemma-4-26b-a4b-it', 'google/gemma-4-26b-a4b-it'], - ['gemma-4-31b-it', 'google/gemma-4-31b-it'], ['minimax-m2.5', 'minimax/minimax-m2.5'], ['minimax-m3', 'minimax/minimax-m3'], ['minimax/minimax-m2.5-20260211', 'minimax/minimax-m2.5'], - ['kimi-k2.6', 'moonshotai/kimi-k2.6'], ['kimi-k3', 'moonshotai/kimi-k3'], - ['gpt-3.5-turbo', 'openai/gpt-3.5-turbo'], - ['gpt-4.1', 'openai/gpt-4.1'], ['gpt-4.1-mini', 'openai/gpt-4.1-mini'], ['gpt-4o', 'openai/gpt-4o'], - ['gpt-4o-2024-08-06', 'openai/gpt-4o'], ['gpt-4o-mini', 'openai/gpt-4o-mini'], - ['gpt-4o-mini-2024-07-18', 'openai/gpt-4o-mini'], - ['gpt-5.1-codex', 'openai/gpt-5.1-codex'], - ['gpt-5.1-codex-max', 'openai/gpt-5.1-codex-max'], - ['gpt-5.1-codex-mini', 'openai/gpt-5.1-codex-mini'], ['gpt-5.2', 'openai/gpt-5.2'], ['gpt-5.2-codex', 'openai/gpt-5.2-codex'], - ['gpt-5.2-pro', 'openai/gpt-5.2-pro'], - ['gpt-5.3-codex', 'openai/gpt-5.3-codex'], ['gpt-5.4', 'openai/gpt-5.4'], ['gpt-5.4-mini', 'openai/gpt-5.4-mini'], - ['gpt-5.4-nano', 'openai/gpt-5.4-nano'], - ['gpt-5.4-pro', 'openai/gpt-5.4-pro'], ['gpt-5.5', 'openai/gpt-5.5'], ['gpt-5.6-luna', 'openai/gpt-5.6-luna'], ['gpt-5.6-sol', 'openai/gpt-5.6-sol'], ['gpt-5.6-terra', 'openai/gpt-5.6-terra'], - ['gpt-5-nano', 'openai/gpt-5-nano'], - ['gpt-5-pro', 'openai/gpt-5-pro'], - ['gpt-oss-20b', 'openai/gpt-oss-20b'], - ['gpt-oss-safeguard-20b', 'openai/gpt-oss-safeguard-20b'], - ['o3-pro', 'openai/o3-pro'], - ['laguna-s-2.1', 'poolside/laguna-s-2.1'], ['step-3.5-flash', 'stepfun/step-3.5-flash'], - ['hy3', 'tencent/hy3'], ['mimo-v2.5', 'xiaomi/mimo-v2.5'], - ['glm-4.7-flash', 'zai/glm-4.7-flash'], ['glm-5.1', 'zai/glm-5.1'], ['glm-5.2', 'zai/glm-5.2'], ])('maps %s to %s', (input, expected) => { expect(mapModelIdToVercel(input)).toBe(expected); }); + + it.each([ + ['gpt-4o-2024-08-06', 'gpt-4o-2024-08-06'], + ['claude-fable-5', 'claude-fable-5'], + ])('does not retain a mapping for %s', (input, expected) => { + expect(mapModelIdToVercel(input)).toBe(expected); + }); }); describe('first-party inference provider inference', () => { diff --git a/apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.ts b/apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.ts index 31c6de55a1..320b6701bf 100644 --- a/apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.ts +++ b/apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.ts @@ -45,7 +45,6 @@ const vercelModelIdMapping: Record = { [GROK_LATEST_MODEL_ALIAS]: GROK_CURRENT_VERCEL_MODEL_ID, [GLM_LATEST_MODEL_ALIAS]: GLM_CURRENT_VERCEL_MODEL_ID, [DEEPSEEK_V4_FLASH_LATEST_MODEL_ALIAS]: 'deepseek/deepseek-v4-flash-0731', - 'inclusionai/ling-3.0-flash:free': 'inclusionai/ling-3.0-flash-free', 'mistralai/codestral-2508': 'mistral/codestral', 'mistralai/devstral-2512': 'mistral/devstral-2', 'mistralai/mistral-embed-2312': 'mistral/mistral-embed', @@ -56,21 +55,14 @@ const vercelModelIdMapping: Record = { 'mistralai/mistral-large-2512': 'mistral/mistral-large-3', 'mistralai/mistral-medium-3-5': 'mistral/mistral-medium-3.5', 'mistralai/mistral-small-2603': 'mistral/mistral-small', - 'mistralai/pixtral-large-2411': 'mistral/pixtral-large', 'qwen/qwen3-14b': 'alibaba/qwen-3-14b', 'qwen/qwen3-235b-a22b': 'alibaba/qwen-3-235b', 'qwen/qwen3-30b-a3b': 'alibaba/qwen-3-30b', 'qwen/qwen3-32b': 'alibaba/qwen-3-32b', - 'qwen3.8-max': 'alibaba/qwen3.8-max', 'anthropic/claude-haiku-4-5': 'anthropic/claude-haiku-4.5', - 'anthropic/claude-opus-4-6': 'anthropic/claude-opus-4.6', 'anthropic/claude-sonnet-4-5': 'anthropic/claude-sonnet-4.5', 'anthropic/claude-sonnet-4-6': 'anthropic/claude-sonnet-4.6', 'anthropic/claude-sonnet-5-20260630': 'anthropic/claude-sonnet-5', - 'claude-fable-5': 'anthropic/claude-fable-5', - 'claude-haiku-4.5': 'anthropic/claude-haiku-4.5', - 'claude-haiku-4-5-20251001': 'anthropic/claude-haiku-4.5', - 'claude-opus-4-8': 'anthropic/claude-opus-4.8', 'claude-opus-5': 'anthropic/claude-opus-5', 'claude-sonnet-4': 'anthropic/claude-sonnet-4', 'claude-sonnet-4.5': 'anthropic/claude-sonnet-4.5', @@ -78,49 +70,24 @@ const vercelModelIdMapping: Record = { 'deepseek-v4-flash': 'deepseek/deepseek-v4-flash', 'deepseek-v4-flash-0731': 'deepseek/deepseek-v4-flash-0731', 'deepseek-v4-pro': 'deepseek/deepseek-v4-pro', - 'gemini-2.5-flash': 'google/gemini-2.5-flash', 'gemini-2.5-flash-lite': 'google/gemini-2.5-flash-lite', - 'gemini-2.5-pro': 'google/gemini-2.5-pro', - 'gemini-3-pro-image': 'google/gemini-3-pro-image', - 'gemma-4-26b-a4b-it': 'google/gemma-4-26b-a4b-it', - 'gemma-4-31b-it': 'google/gemma-4-31b-it', 'minimax-m2.5': 'minimax/minimax-m2.5', 'minimax-m3': 'minimax/minimax-m3', 'minimax/minimax-m2.5-20260211': 'minimax/minimax-m2.5', - 'kimi-k2.6': 'moonshotai/kimi-k2.6', 'kimi-k3': 'moonshotai/kimi-k3', - 'gpt-3.5-turbo': 'openai/gpt-3.5-turbo', - 'gpt-4.1': 'openai/gpt-4.1', 'gpt-4.1-mini': 'openai/gpt-4.1-mini', 'gpt-4o': 'openai/gpt-4o', - 'gpt-4o-2024-08-06': 'openai/gpt-4o', 'gpt-4o-mini': 'openai/gpt-4o-mini', - 'gpt-4o-mini-2024-07-18': 'openai/gpt-4o-mini', - 'gpt-5.1-codex': 'openai/gpt-5.1-codex', - 'gpt-5.1-codex-max': 'openai/gpt-5.1-codex-max', - 'gpt-5.1-codex-mini': 'openai/gpt-5.1-codex-mini', 'gpt-5.2': 'openai/gpt-5.2', 'gpt-5.2-codex': 'openai/gpt-5.2-codex', - 'gpt-5.2-pro': 'openai/gpt-5.2-pro', - 'gpt-5.3-codex': 'openai/gpt-5.3-codex', 'gpt-5.4': 'openai/gpt-5.4', 'gpt-5.4-mini': 'openai/gpt-5.4-mini', - 'gpt-5.4-nano': 'openai/gpt-5.4-nano', - 'gpt-5.4-pro': 'openai/gpt-5.4-pro', 'gpt-5.5': 'openai/gpt-5.5', 'gpt-5.6-luna': 'openai/gpt-5.6-luna', 'gpt-5.6-sol': 'openai/gpt-5.6-sol', 'gpt-5.6-terra': 'openai/gpt-5.6-terra', - 'gpt-5-nano': 'openai/gpt-5-nano', - 'gpt-5-pro': 'openai/gpt-5-pro', - 'gpt-oss-20b': 'openai/gpt-oss-20b', - 'gpt-oss-safeguard-20b': 'openai/gpt-oss-safeguard-20b', - 'o3-pro': 'openai/o3-pro', - 'laguna-s-2.1': 'poolside/laguna-s-2.1', 'step-3.5-flash': 'stepfun/step-3.5-flash', - hy3: 'tencent/hy3', 'mimo-v2.5': 'xiaomi/mimo-v2.5', - 'glm-4.7-flash': 'zai/glm-4.7-flash', 'glm-5.1': 'zai/glm-5.1', 'glm-5.2': 'zai/glm-5.2', };