diff --git a/packages/types/src/__tests__/deepseek-v4-pro.test.ts b/packages/types/src/__tests__/deepseek-v4-pro.test.ts index 78a4befd6b..6fa40d28eb 100644 --- a/packages/types/src/__tests__/deepseek-v4-pro.test.ts +++ b/packages/types/src/__tests__/deepseek-v4-pro.test.ts @@ -10,12 +10,18 @@ describe("DeepSeek V4 Pro 0813 provider catalogs", () => { expect(model?.contextWindow).toBeGreaterThanOrEqual(1_000_000) }) - it("uses peak first-party pricing and unchanged OpenCode Go pricing", () => { + it("uses current peak first-party pricing and unchanged OpenCode Go pricing", () => { + expect(deepSeekModels["deepseek-flash"]).toMatchObject({ + supportsImages: true, + outputPrice: 1.2, + cacheWritesPrice: 0.3, + cacheReadsPrice: 0.006, + }) expect(deepSeekModels["deepseek-v4-flash"]).toMatchObject({ - supportsImages: false, - outputPrice: 1.32, - cacheWritesPrice: 0.44, - cacheReadsPrice: 0.014, + supportsImages: true, + outputPrice: 1.2, + cacheWritesPrice: 0.3, + cacheReadsPrice: 0.006, }) expect(deepSeekModels["deepseek-v4-pro"].supportsImages).toBe(false) expect(deepSeekModels["deepseek-v4-pro"]).toMatchObject({ @@ -42,6 +48,10 @@ describe("DeepSeek V4 Pro 0813 provider catalogs", () => { expect(model.supportsPromptCache).toBe(true) expect(model.contextWindow).toBeGreaterThanOrEqual(1_000_000) expect(model.supportsReasoningEffort).toEqual(["disable", "low", "high", "max"]) + expect(model).toMatchObject({ outputPrice: 1.2, cacheWritesPrice: 0.3, cacheReadsPrice: 0.006 }) + expect(model.description).toContain("Legacy model name") + expect(model).not.toHaveProperty("supportsTemperature") + expect(model).not.toHaveProperty("defaultTemperature") }) // Self-hosted providers retain separate IDs for the preview weights and 0813 checkpoint. diff --git a/packages/types/src/providers/deepseek.ts b/packages/types/src/providers/deepseek.ts index 3e42bbfeec..5cd2e0f21d 100644 --- a/packages/types/src/providers/deepseek.ts +++ b/packages/types/src/providers/deepseek.ts @@ -6,23 +6,38 @@ import type { ModelInfo } from "../model.js" // continuation within the same turn. See: https://api-docs.deepseek.com/guides/thinking_mode export type DeepSeekModelId = keyof typeof deepSeekModels -export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-v4-flash" +export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-flash" export const deepSeekModels = { + "deepseek-flash": { + maxTokens: 384_000, + contextWindow: 1_000_000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["disable", "low", "high", "max"], // Updated 2026-09-10 + preserveReasoning: true, + reasoningEffort: "high", + inputPrice: 0, // the inputs are priced as cache read/write, so `inputPrice` should be 0 + // Static estimates use peak rates; off-peak rates are 50% lower. Effective 2026-09-10. + outputPrice: 1.2, + cacheWritesPrice: 0.3, + cacheReadsPrice: 0.006, + description: `DeepSeek-V4.1-Flash is DeepSeek's fast multimodal model with image understanding. It supports thinking and non-thinking modes, JSON output, tool calls, chat prefix completion (beta), and FIM completion (beta) in non-thinking mode.`, + }, "deepseek-v4-flash": { maxTokens: 384_000, contextWindow: 1_000_000, - supportsImages: false, + supportsImages: true, supportsPromptCache: true, supportsReasoningEffort: ["disable", "low", "high", "max"], // Updated 2026-08-13 preserveReasoning: true, reasoningEffort: "high", inputPrice: 0, // the inputs are priced as cache read/write, so `inputPrice` should be 0 - // Static estimates use peak rates; off-peak rates are 50% lower. Effective 2026-08-16. - outputPrice: 1.32, - cacheWritesPrice: 0.44, - cacheReadsPrice: 0.014, - description: `DeepSeek-V4-Flash is DeepSeek's fast, cost-efficient V4 model. It supports thinking and non-thinking modes, JSON output, tool calls, chat prefix completion (beta), and FIM completion (beta) in non-thinking mode.`, + // This retired ID is billed as the current Flash model. + outputPrice: 1.2, + cacheWritesPrice: 0.3, + cacheReadsPrice: 0.006, + description: `Legacy model name routed to the latest DeepSeek Flash model, which supports image input. Use deepseek-flash for new configurations.`, }, "deepseek-v4-pro": { displayName: "DeepSeek V4 Pro 0813", @@ -49,14 +64,12 @@ export const deepSeekModels = { supportsReasoningEffort: ["disable", "low", "high", "max"], // Updated 2026-08-13 preserveReasoning: true, reasoningEffort: "high", - supportsTemperature: true, - defaultTemperature: 1.0, inputPrice: 0, // the inputs are priced as cache read/write, so `inputPrice` should be 0 - // Static estimates use peak rates; off-peak rates are 50% lower. - outputPrice: 1.32, - cacheWritesPrice: 0.44, - cacheReadsPrice: 0.014, - description: `DeepSeek-V4-Flash-Vision-Exp is DeepSeek's experimental multimodal V4 Flash model with image understanding. It supports thinking and non-thinking modes, JSON output, tool calls, chat prefix completion (beta), and image input through Chat Completions, Responses, and Anthropic-compatible APIs.`, + // This retired ID is billed as the current Flash model. + outputPrice: 1.2, + cacheWritesPrice: 0.3, + cacheReadsPrice: 0.006, + description: `Legacy model name routed to the latest DeepSeek Flash model, which supports image input. Use deepseek-flash for new configurations.`, }, } as const satisfies Record diff --git a/src/api/providers/__tests__/deepseek.spec.ts b/src/api/providers/__tests__/deepseek.spec.ts index 2f344d8405..4ab247b131 100644 --- a/src/api/providers/__tests__/deepseek.spec.ts +++ b/src/api/providers/__tests__/deepseek.spec.ts @@ -240,22 +240,22 @@ describe("DeepSeekHandler", () => { expect(model.info).toBeDefined() expect(model.info.maxTokens).toBe(384_000) expect(model.info.contextWindow).toBe(1_000_000) - expect(model.info.supportsImages).toBe(false) + expect(model.info.supportsImages).toBe(true) expect(model.info.supportsPromptCache).toBe(true) // Should be true now expect((model.info as ModelInfo).preserveReasoning).toBe(true) }) - it("should use deepseek-v4-flash as the default model ID for new configs", () => { + it("should use deepseek-flash as the default model ID for new configs", () => { const handlerWithoutModel = new DeepSeekHandler({ ...mockOptions, apiModelId: undefined, }) const model = handlerWithoutModel.getModel() expect(model.id).toBe(deepSeekDefaultModelId) - expect(model.id).toBe("deepseek-v4-flash") + expect(model.id).toBe("deepseek-flash") expect(model.info.maxTokens).toBe(384_000) expect(model.info.contextWindow).toBe(1_000_000) - expect(model.info.supportsImages).toBe(false) + expect(model.info.supportsImages).toBe(true) expect((model.info as ModelInfo).supportsReasoningEffort).toContain("max") }) @@ -290,7 +290,6 @@ describe("DeepSeekHandler", () => { supportsPromptCache: true, preserveReasoning: true, reasoningEffort: "high", - defaultTemperature: 1.0, }) }) @@ -369,41 +368,61 @@ describe("DeepSeekHandler", () => { expect(textChunks[0].text).toBe("Test response") }) - it("should send images and V4 thinking controls to deepseek-v4-flash-vision-exp", async () => { + it.each(["deepseek-flash", "deepseek-v4-flash", "deepseek-v4-flash-vision-exp"] as const)( + "should send images and thinking controls to %s", + async (modelId) => { + const visionHandler = new DeepSeekHandler({ + ...mockOptions, + apiModelId: modelId, + }) + const visionMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "Describe this image." }, + { + type: "image", + source: { type: "base64", media_type: "image/png", data: "image-data" }, + }, + ], + }, + ] + + await collectStream(visionHandler.createMessage(systemPrompt, visionMessages)) + + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs).toMatchObject({ + model: modelId, + thinking: { type: "enabled" }, + reasoning_effort: "high", + max_completion_tokens: 200_000, + }) + expect(callArgs.temperature).toBeUndefined() + expect(callArgs.messages).toContainEqual({ + role: "user", + content: expect.arrayContaining([ + { type: "text", text: expect.stringContaining("Describe this image.") }, + { type: "image_url", image_url: { url: "data:image/png;base64,image-data" } }, + ]), + }) + }, + ) + + it("should use the provider default temperature when reasoning is disabled for the vision alias", async () => { const visionHandler = new DeepSeekHandler({ ...mockOptions, apiModelId: "deepseek-v4-flash-vision-exp", + enableReasoningEffort: false, }) - const visionMessages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { type: "text", text: "Describe this image." }, - { - type: "image", - source: { type: "base64", media_type: "image/png", data: "image-data" }, - }, - ], - }, - ] - await collectStream(visionHandler.createMessage(systemPrompt, visionMessages)) + await collectStream(visionHandler.createMessage(systemPrompt, messages)) - const callArgs = mockCreate.mock.calls[0][0] - expect(callArgs).toMatchObject({ + expect(mockCreate.mock.calls[0][0]).toMatchObject({ model: "deepseek-v4-flash-vision-exp", - thinking: { type: "enabled" }, - reasoning_effort: "high", - max_completion_tokens: 200_000, - }) - expect(callArgs.temperature).toBeUndefined() - expect(callArgs.messages).toContainEqual({ - role: "user", - content: expect.arrayContaining([ - { type: "text", text: expect.stringContaining("Describe this image.") }, - { type: "image_url", image_url: { url: "data:image/png;base64,image-data" } }, - ]), + thinking: { type: "disabled" }, + temperature: 0, }) + expect(mockCreate.mock.calls[0][0].reasoning_effort).toBeUndefined() }) it("should include usage information", async () => { diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index c423c2b55c..149adb186b 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -28,7 +28,12 @@ type DeepSeekChatCompletionParams = Omit deepSeekV4ThinkingModels.has(modelId) // Only known V4 models and the legacy reasoner alias support DeepSeek's @@ -49,6 +54,7 @@ export const normalizeDeepSeekReasoningEffort = ( ): "low" | "high" | "max" | undefined => { // still check the modelId so non-supported models won't produce reasoning efforts switch (modelId) { + case "deepseek-flash": case "deepseek-v4-flash": case "deepseek-v4-pro": case "deepseek-v4-flash-vision-exp": diff --git a/src/api/providers/fetchers/__tests__/deepseek.spec.ts b/src/api/providers/fetchers/__tests__/deepseek.spec.ts index e44190a141..7856874329 100644 --- a/src/api/providers/fetchers/__tests__/deepseek.spec.ts +++ b/src/api/providers/fetchers/__tests__/deepseek.spec.ts @@ -29,11 +29,24 @@ describe("getDeepSeekModels", () => { const models = await getDeepSeekModels("http://127.0.0.1:43123/v1", "mock-key") expect(globalThis.fetch).toHaveBeenCalledWith("http://127.0.0.1:43123/models", expect.any(Object)) + expect(models["deepseek-flash"]).toEqual(deepSeekModels["deepseek-flash"]) expect(models["deepseek-v4-flash"]).toEqual(deepSeekModels["deepseek-v4-flash"]) expect(models["deepseek-v4-pro"]).toEqual(deepSeekModels["deepseek-v4-pro"]) expect(models["deepseek-v4-flash-vision-exp"]).toEqual(deepSeekModels["deepseek-v4-flash-vision-exp"]) }) + it("applies vision metadata to the canonical Flash model returned by DeepSeek", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ data: [{ id: "deepseek-flash" }] }), + }) as unknown as typeof fetch + + const models = await getDeepSeekModels(undefined, "test-key") + + expect(models["deepseek-flash"]).toEqual(deepSeekModels["deepseek-flash"]) + expect(models["deepseek-flash"].supportsImages).toBe(true) + }) + it("throws for 404 responses when fallback flag is not enabled", async () => { delete process.env.E2E_MOCK_MODEL_LIST_FALLBACK