diff --git a/src/models.ts b/src/models.ts index 954d8d7..a913f8e 100644 --- a/src/models.ts +++ b/src/models.ts @@ -97,7 +97,7 @@ export type ConfigModel = { function buildLookupMap(modelsDevData: ModelsDevData) { const byFullId = new Map(); - const allowedProviders = new Set(["openai", "anthropic"]); + const allowedProviders = new Set(["openai", "anthropic", "deepseek"]); for (const [provider, providerData] of Object.entries(modelsDevData)) { if (!allowedProviders.has(provider) || !providerData?.models) continue; @@ -156,8 +156,8 @@ export async function fetchHubModels(): Promise { return res.json() as Promise; } -function checkIsAnthropic(provider: string): boolean { - return provider === "anthropic"; +function usesAnthropicApi(provider: string): boolean { + return provider === "anthropic" || provider === "deepseek"; } export function buildConfigModels( @@ -171,7 +171,7 @@ export function buildConfigModels( for (const [provider, providerData] of Object.entries(hubData.providers)) { if (!providerData?.models) continue; - const anthropic = checkIsAnthropic(provider); + const anthropic = usesAnthropicApi(provider); 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 e4a582b..a73444d 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -65,6 +65,43 @@ const MODELS_DEV_DATA = { }, }, }, + deepseek: { + models: { + "deepseek-v4-pro": { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + limit: { context: 128000, output: 32000 }, + attachment: true, + 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 }, + }, + "deepseek-v4-flash": { + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", + limit: { context: 128000, output: 32000 }, + attachment: true, + 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, + }, + }, + }, + }, }; function hubResponse() { @@ -80,6 +117,12 @@ function hubResponse() { "claude-sonnet-4-20250514": { display_name: "Claude Sonnet 4" }, }, }, + deepseek: { + models: { + "deepseek-v4-pro": { display_name: "DeepSeek V4 Pro" }, + "deepseek-v4-flash": { display_name: "DeepSeek V4 Flash" }, + }, + }, }, }; } @@ -169,6 +212,48 @@ describe("config hook", () => { "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", }, }); + + expect(models["deepseek-v4-pro"]).toEqual({ + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + provider: { + api: "https://hub.coreinfra.ai/claude/api/v1", + npm: "@ai-sdk/anthropic", + }, + attachment: true, + 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 }, + interleaved: { field: "reasoning_content" }, + headers: { + "anthropic-beta": + "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", + }, + }); + + expect(models["deepseek-v4-flash"]).toEqual({ + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", + provider: { + api: "https://hub.coreinfra.ai/claude/api/v1", + npm: "@ai-sdk/anthropic", + }, + attachment: true, + 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 }, + interleaved: { field: "reasoning_content" }, + headers: { + "anthropic-beta": + "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", + }, + }); }); it("uses defaults when models.dev has no matching model", async () => { diff --git a/test/models.test.ts b/test/models.test.ts index a585771..b44deee 100644 --- a/test/models.test.ts +++ b/test/models.test.ts @@ -52,6 +52,43 @@ const MODELS_DEV_FIXTURE = { }, }, }, + deepseek: { + models: { + "deepseek-v4-pro": { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + limit: { context: 128000, output: 32000 }, + attachment: true, + 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 }, + }, + "deepseek-v4-flash": { + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", + limit: { context: 128000, output: 32000 }, + attachment: true, + 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, + }, + }, + }, + }, }; const HUB_FIXTURE = { @@ -66,6 +103,12 @@ const HUB_FIXTURE = { "claude-sonnet-4-20250514": { display_name: "Claude Sonnet 4" }, }, }, + deepseek: { + models: { + "deepseek-v4-pro": { display_name: "DeepSeek V4 Pro" }, + "deepseek-v4-flash": { display_name: "DeepSeek V4 Flash" }, + }, + }, }, }; @@ -77,7 +120,7 @@ describe("buildConfigModels", () => { ); expect(warnings).toEqual([]); - expect(Object.keys(models)).toHaveLength(2); + expect(Object.keys(models)).toHaveLength(4); const gpt = models["gpt-5.4-nano"]; expect(gpt).toEqual({ @@ -125,6 +168,56 @@ describe("buildConfigModels", () => { "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", }, }); + + const dsPro = models["deepseek-v4-pro"]; + expect(dsPro).toEqual({ + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + provider: { + api: "https://hub.coreinfra.ai/claude/api/v1", + npm: "@ai-sdk/anthropic", + }, + attachment: true, + 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 }, + interleaved: { field: "reasoning_content" }, + headers: { + "anthropic-beta": + "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", + }, + }); + + const dsFlash = models["deepseek-v4-flash"]; + expect(dsFlash).toEqual({ + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", + provider: { + api: "https://hub.coreinfra.ai/claude/api/v1", + npm: "@ai-sdk/anthropic", + }, + attachment: true, + 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 }, + interleaved: { field: "reasoning_content" }, + headers: { + "anthropic-beta": + "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", + }, + }); }); it("uses defaults and emits warning when model not in models.dev", () => { @@ -197,7 +290,7 @@ describe("buildConfigModels", () => { expect(models["shared-id"].limit.context).toBe(100000); }); - it("does not resolve from non-openai/anthropic providers in models.dev", () => { + it("does not resolve from non-allowed providers in models.dev", () => { const modelsDevData = { "provider-a": { models: {