diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.test.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.test.ts index 24830cc826..6493ca878c 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.test.ts @@ -129,4 +129,19 @@ describe('parseModelsDevProviderModels', () => { }, ]); }); + + test('excludes models missing from the provider model list', () => { + const models = parseModelsDevProviderModels( + { + models: { + available: { id: 'available', limit: { context: 128_000 } }, + removed: { id: 'removed', limit: { context: 64_000 } }, + }, + }, + new Set(['available', 'provider-only']) + ); + + expect(models.map(model => model.id)).toEqual(['available']); + expect(models[0].context_length).toBe(128_000); + }); }); diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.ts index dfceb8f5fd..c6236bd7d7 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.ts @@ -114,13 +114,17 @@ export function parseOpenAICompatibleProviderModels(entry: unknown): RawModel[] })); } -export function parseModelsDevProviderModels(entry: unknown): RawModel[] { +export function parseModelsDevProviderModels( + entry: unknown, + availableModelIds?: ReadonlySet +): RawModel[] { const provider = ModelsDevProviderSchema.parse(entry); return Object.values(provider.models) .filter( model => model.status !== 'deprecated' && - (!model.modalities?.output || model.modalities.output.includes('text')) + (!model.modalities?.output || model.modalities.output.includes('text')) && + (!availableModelIds || availableModelIds.has(model.id)) ) .map(model => ({ id: model.id, @@ -134,7 +138,8 @@ export function parseModelsDevProviderModels(entry: unknown): RawModel[] { function modelsDevFetcher( providerId: DirectUserByokInferenceProviderId, - catalogKey: string + catalogKey: string, + availableModelsUrl?: string ): ProviderFetcher { return { providerId, @@ -144,7 +149,21 @@ function modelsDevFetcher( if (!entry) { throw new Error(`models.dev catalog missing ${catalogKey} entry`); } - return parseModelsDevProviderModels(entry); + if (!availableModelsUrl) { + return parseModelsDevProviderModels(entry); + } + const response = await fetch(availableModelsUrl); + if (!response.ok) { + throw new Error( + `Failed to fetch ${providerId} available models: ${response.status} ${response.statusText}` + ); + } + const availableModelIds = new Set( + OpenAICompatibleModelsResponseSchema.parse(await response.json()).data.map( + model => model.id + ) + ); + return parseModelsDevProviderModels(entry, availableModelIds); }, }; } @@ -202,8 +221,8 @@ const FETCHERS: ReadonlyArray = [ }), modelsDevFetcher('alibaba-token-plan', 'alibaba-token-plan'), modelsDevFetcher('zai-coding', 'zai-coding-plan'), - modelsDevFetcher('ollama-cloud', 'ollama-cloud'), - modelsDevFetcher('opencode-go', 'opencode-go'), + modelsDevFetcher('ollama-cloud', 'ollama-cloud', 'https://ollama.com/v1/models'), + modelsDevFetcher('opencode-go', 'opencode-go', 'https://opencode.ai/zen/go/v1/models'), modelsDevFetcher('xiaomi-token-plan-ams', 'xiaomi-token-plan-ams'), modelsDevFetcher('xiaomi-token-plan-sgp', 'xiaomi-token-plan-sgp'), ];