From 9bb28900d761094eb869869341c61be80dcf1ef5 Mon Sep 17 00:00:00 2001 From: "@taltas" <6816042+taltas@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:13:31 +0000 Subject: [PATCH] feat(providers): add DeepSeek V4 Flash Vision Exp --- packages/types/src/providers/deepseek.ts | 18 ++++++ packages/types/src/providers/fireworks.ts | 14 +++++ src/api/providers/__tests__/deepseek.spec.ts | 60 +++++++++++++++++++ src/api/providers/__tests__/fireworks.spec.ts | 17 ++++++ src/api/providers/deepseek.ts | 3 +- .../fetchers/__tests__/deepseek.spec.ts | 1 + .../hooks/__tests__/useSelectedModel.spec.ts | 15 +++++ 7 files changed, 127 insertions(+), 1 deletion(-) diff --git a/packages/types/src/providers/deepseek.ts b/packages/types/src/providers/deepseek.ts index d9cf7ce755..7b3ffca4f2 100644 --- a/packages/types/src/providers/deepseek.ts +++ b/packages/types/src/providers/deepseek.ts @@ -40,6 +40,24 @@ export const deepSeekModels = { cacheReadsPrice: 0.044, description: `DeepSeek-V4-Pro-0813 is DeepSeek's strongest V4 model for reasoning, coding, long-context, and agentic workloads. 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-vision-exp": { + displayName: "DeepSeek V4 Flash Vision Exp", + maxTokens: 384_000, + contextWindow: 1_000_000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["disable", "low", "high", "max"], + preserveReasoning: true, + reasoningEffort: "high", + supportsTemperature: true, + defaultTemperature: 1.0, + inputPrice: 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 an experimental multimodal model for text and image understanding. It supports thinking and non-thinking modes, JSON output, tool calls, and image input through Chat Completions, Responses, and Anthropic-compatible APIs.`, + }, } as const satisfies Record // https://api-docs.deepseek.com/quick_start/parameter_settings diff --git a/packages/types/src/providers/fireworks.ts b/packages/types/src/providers/fireworks.ts index 993bf1ad8e..5417618ac6 100644 --- a/packages/types/src/providers/fireworks.ts +++ b/packages/types/src/providers/fireworks.ts @@ -17,6 +17,7 @@ export type FireworksModelId = | "accounts/fireworks/models/deepseek-v3p2" | "accounts/fireworks/models/deepseek-v4-pro" | "accounts/fireworks/models/deepseek-v4-pro-0813" + | "accounts/fireworks/models/deepseek-v4-flash-vision-exp" | "accounts/fireworks/models/glm-4p5" | "accounts/fireworks/models/glm-4p5-air" | "accounts/fireworks/models/glm-4p6" @@ -280,6 +281,19 @@ export const fireworksModels = { description: "DeepSeek V4 Pro 0813 is DeepSeek's production checkpoint for advanced reasoning, coding, and long-context agentic workloads.", }, + "accounts/fireworks/models/deepseek-v4-flash-vision-exp": { + displayName: "DeepSeek V4 Flash Vision Exp", + maxTokens: 384_000, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + supportsMaxTokens: true, + inputPrice: 0.22, + outputPrice: 0.66, + cacheReadsPrice: 0.007, + description: + "DeepSeek V4 Flash Vision Exp is an experimental multimodal model with text and image input, function calling, and long-context support.", + }, "accounts/fireworks/models/kimi-k2p7-code": { maxTokens: 16384, contextWindow: 262144, diff --git a/src/api/providers/__tests__/deepseek.spec.ts b/src/api/providers/__tests__/deepseek.spec.ts index dd2c3d4019..36e05c5a2a 100644 --- a/src/api/providers/__tests__/deepseek.spec.ts +++ b/src/api/providers/__tests__/deepseek.spec.ts @@ -249,6 +249,24 @@ describe("DeepSeekHandler", () => { expect((model.info as ModelInfo).reasoningEffort).toBe("high") }) + it("should return vision metadata for deepseek-v4-flash-vision-exp", () => { + const visionHandler = new DeepSeekHandler({ + ...mockOptions, + apiModelId: "deepseek-v4-flash-vision-exp", + }) + const model = visionHandler.getModel() + + expect(model.info).toMatchObject({ + maxTokens: 384_000, + contextWindow: 1_000_000, + supportsImages: true, + supportsPromptCache: true, + preserveReasoning: true, + reasoningEffort: "high", + defaultTemperature: 1.0, + }) + }) + it("should return provided model ID with default model info if model does not exist", () => { const handlerWithInvalidModel = new DeepSeekHandler({ ...mockOptions, @@ -324,6 +342,43 @@ describe("DeepSeekHandler", () => { expect(textChunks[0].text).toBe("Test response") }) + it("should send images and V4 thinking controls to deepseek-v4-flash-vision-exp", async () => { + const visionHandler = new DeepSeekHandler({ + ...mockOptions, + apiModelId: "deepseek-v4-flash-vision-exp", + }) + 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: "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" } }, + ]), + }) + }) + it("should include usage information", async () => { const chunks: any[] = await collectStream(handler.createMessage(systemPrompt, messages)) @@ -684,6 +739,11 @@ describe("DeepSeekHandler", () => { rawReasoningEffort: "max", mappedReasoningEffort: "max", }, + { + modelId: "deepseek-v4-flash-vision-exp", + rawReasoningEffort: "medium", + mappedReasoningEffort: "high", + }, ] for (const { modelId, rawReasoningEffort, mappedReasoningEffort } of mappings) { diff --git a/src/api/providers/__tests__/fireworks.spec.ts b/src/api/providers/__tests__/fireworks.spec.ts index bde144591d..de56c5a803 100644 --- a/src/api/providers/__tests__/fireworks.spec.ts +++ b/src/api/providers/__tests__/fireworks.spec.ts @@ -127,6 +127,13 @@ describe("FireworksHandler", () => { outputPrice: 3.96, cacheReadsPrice: 0.044, }, + { + modelId: "accounts/fireworks/models/deepseek-v4-flash-vision-exp" as const, + contextWindow: 1_048_576, + inputPrice: 0.22, + outputPrice: 0.66, + cacheReadsPrice: 0.007, + }, ])( "should expose newly added model $modelId", ({ modelId, contextWindow, inputPrice, outputPrice, cacheReadsPrice }) => { @@ -147,6 +154,16 @@ describe("FireworksHandler", () => { }, ) + it("should expose vision support for DeepSeek V4 Flash Vision Exp", () => { + const model = fireworksModels["accounts/fireworks/models/deepseek-v4-flash-vision-exp"] + + expect(model).toMatchObject({ + supportsImages: true, + supportsPromptCache: true, + supportsMaxTokens: true, + }) + }) + it("should return Kimi K2 Instruct model with correct configuration", () => { const testModelId: FireworksModelId = "accounts/fireworks/models/kimi-k2-instruct" const handlerWithModel = new FireworksHandler({ diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index 64782c9fb4..b3ca67287a 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -28,7 +28,7 @@ type DeepSeekChatCompletionParams = Omit deepSeekV4ThinkingModels.has(modelId) // Only known V4 models and the legacy reasoner alias support DeepSeek's @@ -51,6 +51,7 @@ export const normalizeDeepSeekReasoningEffort = ( switch (modelId) { case "deepseek-v4-flash": case "deepseek-v4-pro": + case "deepseek-v4-flash-vision-exp": switch (reasoningEffort) { case "low": return "low" diff --git a/src/api/providers/fetchers/__tests__/deepseek.spec.ts b/src/api/providers/fetchers/__tests__/deepseek.spec.ts index 55df933141..e44190a141 100644 --- a/src/api/providers/fetchers/__tests__/deepseek.spec.ts +++ b/src/api/providers/fetchers/__tests__/deepseek.spec.ts @@ -31,6 +31,7 @@ describe("getDeepSeekModels", () => { expect(globalThis.fetch).toHaveBeenCalledWith("http://127.0.0.1:43123/models", expect.any(Object)) 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("throws for 404 responses when fallback flag is not enabled", async () => { diff --git a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts index 33a9df16da..0931c8be62 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -220,6 +220,21 @@ describe("useSelectedModel", () => { }, ) + it("selects static vision metadata when the DeepSeek catalog is unavailable", () => { + const modelId = "deepseek-v4-flash-vision-exp" + mockUseRouterModels.mockReturnValue(createRouterModelsResult({ [providerIdentifiers.deepseek]: null })) + mockUseOpenRouterModelProviders.mockReturnValue(createOpenRouterModelProvidersResult({})) + + const { result } = renderHook( + () => useSelectedModel({ apiProvider: providerIdentifiers.deepseek, apiModelId: modelId }), + { wrapper: createWrapper() }, + ) + + expect(result.current.id).toBe(modelId) + expect(result.current.info).toEqual(deepSeekModels[modelId]) + expect(result.current.info?.supportsImages).toBe(true) + }) + it.each([providerIdentifiers.deepseek, providerIdentifiers.moonshot])( "falls back to static data when the %s router catalog is null", (provider) => {