diff --git a/src/models.ts b/src/models.ts index a913f8e..947a95f 100644 --- a/src/models.ts +++ b/src/models.ts @@ -31,6 +31,16 @@ const DEFAULT_COST = { cache_write: 0, }; +type Protocol = "openai" | "anthropic"; + +// Providers we explicitly know how to route. Models from any provider not +// listed here are skipped. Adding a provider is a one-line change here. +const PROVIDERS = new Map([ + ["openai", { protocol: "openai" }], + ["anthropic", { protocol: "anthropic" }], + ["deepseek", { protocol: "anthropic" }], +]); + type ModelDevEntry = { id?: string; name?: string; @@ -97,10 +107,9 @@ export type ConfigModel = { function buildLookupMap(modelsDevData: ModelsDevData) { const byFullId = new Map(); - const allowedProviders = new Set(["openai", "anthropic", "deepseek"]); for (const [provider, providerData] of Object.entries(modelsDevData)) { - if (!allowedProviders.has(provider) || !providerData?.models) continue; + if (!PROVIDERS.has(provider) || !providerData?.models) continue; for (const [modelId, entry] of Object.entries(providerData.models)) { byFullId.set(`${provider}/${modelId}`, entry); } @@ -156,10 +165,6 @@ export async function fetchHubModels(): Promise { return res.json() as Promise; } -function usesAnthropicApi(provider: string): boolean { - return provider === "anthropic" || provider === "deepseek"; -} - export function buildConfigModels( modelsDevData: ModelsDevData, hubData: HubResponse, @@ -169,9 +174,10 @@ export function buildConfigModels( const warnings: string[] = []; for (const [provider, providerData] of Object.entries(hubData.providers)) { - if (!providerData?.models) continue; + const known = PROVIDERS.get(provider); + if (!known || !providerData?.models) continue; - const anthropic = usesAnthropicApi(provider); + const anthropic = known.protocol === "anthropic"; for (const [modelId, hubModel] of Object.entries(providerData.models)) { const entry = resolveEntry(provider, modelId, byFullId); diff --git a/test/index.test.ts b/test/index.test.ts index a73444d..f3f3f49 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -31,11 +31,11 @@ const MODELS_DEV_DATA = { name: "GPT-5.4 Nano", limit: { context: 400000, output: 128000 }, attachment: true, - reasoning: false, - temperature: true, + reasoning: true, + temperature: false, tool_call: true, modalities: { - input: ["text", "image", "audio"], + input: ["text", "image"], output: ["text"], }, cost: { input: 0.2, output: 1.25, cache_read: 0.02, cache_write: 0 }, @@ -70,8 +70,8 @@ const MODELS_DEV_DATA = { "deepseek-v4-pro": { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", - limit: { context: 128000, output: 32000 }, - attachment: true, + limit: { context: 1000000, output: 384000 }, + attachment: false, reasoning: true, temperature: true, tool_call: true, @@ -84,8 +84,8 @@ const MODELS_DEV_DATA = { "deepseek-v4-flash": { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", - limit: { context: 128000, output: 32000 }, - attachment: true, + limit: { context: 1000000, output: 384000 }, + attachment: false, reasoning: true, temperature: true, tool_call: true, @@ -182,10 +182,10 @@ describe("config hook", () => { npm: "@ai-sdk/openai", }, attachment: true, - reasoning: false, - temperature: true, + reasoning: true, + temperature: false, tool_call: true, - modalities: { input: ["text", "image", "audio"], output: ["text"] }, + modalities: { input: ["text", "image"], output: ["text"] }, cost: { input: 0.2, output: 1.25, cache_read: 0.02, cache_write: 0 }, limit: { context: 400000, output: 128000 }, interleaved: true, @@ -220,13 +220,13 @@ describe("config hook", () => { api: "https://hub.coreinfra.ai/claude/api/v1", npm: "@ai-sdk/anthropic", }, - attachment: true, + attachment: false, reasoning: true, temperature: true, tool_call: true, modalities: { input: ["text"], output: ["text"] }, cost: { input: 1.5, output: 6, cache_read: 0.15, cache_write: 1.5 }, - limit: { context: 128000, output: 32000 }, + limit: { context: 1000000, output: 384000 }, interleaved: { field: "reasoning_content" }, headers: { "anthropic-beta": @@ -241,13 +241,13 @@ describe("config hook", () => { api: "https://hub.coreinfra.ai/claude/api/v1", npm: "@ai-sdk/anthropic", }, - attachment: true, + attachment: false, reasoning: true, temperature: true, tool_call: true, modalities: { input: ["text"], output: ["text"] }, cost: { input: 0.15, output: 0.6, cache_read: 0.015, cache_write: 0.15 }, - limit: { context: 128000, output: 32000 }, + limit: { context: 1000000, output: 384000 }, interleaved: { field: "reasoning_content" }, headers: { "anthropic-beta": diff --git a/test/models.test.ts b/test/models.test.ts index b44deee..d30a20e 100644 --- a/test/models.test.ts +++ b/test/models.test.ts @@ -18,11 +18,11 @@ const MODELS_DEV_FIXTURE = { name: "GPT-5.4 Nano", limit: { context: 400000, output: 128000 }, attachment: true, - reasoning: false, - temperature: true, + reasoning: true, + temperature: false, tool_call: true, modalities: { - input: ["text", "image", "audio", "video", "pdf"], + input: ["text", "image"], output: ["text"], }, cost: { input: 0.2, output: 1.25, cache_read: 0.02, cache_write: 0 }, @@ -57,8 +57,8 @@ const MODELS_DEV_FIXTURE = { "deepseek-v4-pro": { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", - limit: { context: 128000, output: 32000 }, - attachment: true, + limit: { context: 1000000, output: 384000 }, + attachment: false, reasoning: true, temperature: true, tool_call: true, @@ -71,8 +71,8 @@ const MODELS_DEV_FIXTURE = { "deepseek-v4-flash": { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", - limit: { context: 128000, output: 32000 }, - attachment: true, + limit: { context: 1000000, output: 384000 }, + attachment: false, reasoning: true, temperature: true, tool_call: true, @@ -131,11 +131,11 @@ describe("buildConfigModels", () => { npm: "@ai-sdk/openai", }, attachment: true, - reasoning: false, - temperature: true, + reasoning: true, + temperature: false, tool_call: true, modalities: { - input: ["text", "image", "audio", "video", "pdf"], + input: ["text", "image"], output: ["text"], }, cost: { input: 0.2, output: 1.25, cache_read: 0.02, cache_write: 0 }, @@ -177,7 +177,7 @@ describe("buildConfigModels", () => { api: "https://hub.coreinfra.ai/claude/api/v1", npm: "@ai-sdk/anthropic", }, - attachment: true, + attachment: false, reasoning: true, temperature: true, tool_call: true, @@ -186,7 +186,7 @@ describe("buildConfigModels", () => { output: ["text"], }, cost: { input: 1.5, output: 6, cache_read: 0.15, cache_write: 1.5 }, - limit: { context: 128000, output: 32000 }, + limit: { context: 1000000, output: 384000 }, interleaved: { field: "reasoning_content" }, headers: { "anthropic-beta": @@ -202,7 +202,7 @@ describe("buildConfigModels", () => { api: "https://hub.coreinfra.ai/claude/api/v1", npm: "@ai-sdk/anthropic", }, - attachment: true, + attachment: false, reasoning: true, temperature: true, tool_call: true, @@ -211,7 +211,7 @@ describe("buildConfigModels", () => { output: ["text"], }, cost: { input: 0.15, output: 0.6, cache_read: 0.015, cache_write: 0.15 }, - limit: { context: 128000, output: 32000 }, + limit: { context: 1000000, output: 384000 }, interleaved: { field: "reasoning_content" }, headers: { "anthropic-beta": @@ -290,36 +290,65 @@ describe("buildConfigModels", () => { expect(models["shared-id"].limit.context).toBe(100000); }); - it("does not resolve from non-allowed providers in models.dev", () => { - const modelsDevData = { - "provider-a": { - models: { - "some-model": { - id: "some-model", - name: "Should Not Match", - cost: { input: 99 }, + it("skips unknown hub providers silently", () => { + const hubData = { + providers: { + google: { + models: { + "gemini-3-pro": { display_name: "Gemini 3 Pro" }, + }, + }, + }, + }; + + const { models, warnings } = buildConfigModels(MODELS_DEV_FIXTURE, hubData); + + expect(models).toEqual({}); + expect(warnings).toEqual([]); + }); + + it("skips providers named like inherited object properties", () => { + const hubData = { + providers: { + toString: { + models: { + "evil-model": { display_name: "Evil Model" }, + }, + }, + constructor: { + models: { + "ctor-model": { display_name: "Ctor Model" }, }, }, }, }; + const { models, warnings } = buildConfigModels(MODELS_DEV_FIXTURE, hubData); + + expect(models).toEqual({}); + expect(warnings).toEqual([]); + }); + + it("registers only known providers when hub mixes known and unknown", () => { const hubData = { providers: { - "provider-a": { + openai: { + models: { + "gpt-5.4-nano": { display_name: "GPT-5.4 Nano" }, + }, + }, + google: { models: { - "some-model": { display_name: "Hub Model" }, + "gemini-3-pro": { display_name: "Gemini 3 Pro" }, }, }, }, }; - const { models, warnings } = buildConfigModels(modelsDevData, hubData); + const { models, warnings } = buildConfigModels(MODELS_DEV_FIXTURE, hubData); - expect(warnings).toEqual([ - "provider-a/some-model not found in models.dev — using defaults", - ]); - expect(models["some-model"].name).toBe("Hub Model"); - expect(models["some-model"].cost.input).toBe(0); + expect(Object.keys(models)).toEqual(["gpt-5.4-nano"]); + expect(warnings).toEqual([]); }); it("uses hub display_name as fallback when models.dev entry has no name", () => {