Skip to content

Commit 500152b

Browse files
zoomote[bot]roomoteedelauna
authored
[Fix] DeepSeek Flash cannot read attached images (#1618)
* fix(deepseek): enable images for current Flash models * test(deepseek): cover vision alias defaults --------- Co-authored-by: Roomote <[email protected]> Co-authored-by: edelauna <[email protected]>
1 parent 9973630 commit 500152b

5 files changed

Lines changed: 113 additions & 52 deletions

File tree

‎packages/types/src/__tests__/deepseek-v4-pro.test.ts‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@ describe("DeepSeek V4 Pro 0813 provider catalogs", () => {
1010
expect(model?.contextWindow).toBeGreaterThanOrEqual(1_000_000)
1111
})
1212

13-
it("uses peak first-party pricing and unchanged OpenCode Go pricing", () => {
13+
it("uses current peak first-party pricing and unchanged OpenCode Go pricing", () => {
14+
expect(deepSeekModels["deepseek-flash"]).toMatchObject({
15+
supportsImages: true,
16+
outputPrice: 1.2,
17+
cacheWritesPrice: 0.3,
18+
cacheReadsPrice: 0.006,
19+
})
1420
expect(deepSeekModels["deepseek-v4-flash"]).toMatchObject({
15-
supportsImages: false,
16-
outputPrice: 1.32,
17-
cacheWritesPrice: 0.44,
18-
cacheReadsPrice: 0.014,
21+
supportsImages: true,
22+
outputPrice: 1.2,
23+
cacheWritesPrice: 0.3,
24+
cacheReadsPrice: 0.006,
1925
})
2026
expect(deepSeekModels["deepseek-v4-pro"].supportsImages).toBe(false)
2127
expect(deepSeekModels["deepseek-v4-pro"]).toMatchObject({
@@ -42,6 +48,10 @@ describe("DeepSeek V4 Pro 0813 provider catalogs", () => {
4248
expect(model.supportsPromptCache).toBe(true)
4349
expect(model.contextWindow).toBeGreaterThanOrEqual(1_000_000)
4450
expect(model.supportsReasoningEffort).toEqual(["disable", "low", "high", "max"])
51+
expect(model).toMatchObject({ outputPrice: 1.2, cacheWritesPrice: 0.3, cacheReadsPrice: 0.006 })
52+
expect(model.description).toContain("Legacy model name")
53+
expect(model).not.toHaveProperty("supportsTemperature")
54+
expect(model).not.toHaveProperty("defaultTemperature")
4555
})
4656

4757
// Self-hosted providers retain separate IDs for the preview weights and 0813 checkpoint.

‎packages/types/src/providers/deepseek.ts‎

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,38 @@ import type { ModelInfo } from "../model.js"
66
// continuation within the same turn. See: https://api-docs.deepseek.com/guides/thinking_mode
77
export type DeepSeekModelId = keyof typeof deepSeekModels
88

9-
export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-v4-flash"
9+
export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-flash"
1010

1111
export const deepSeekModels = {
12+
"deepseek-flash": {
13+
maxTokens: 384_000,
14+
contextWindow: 1_000_000,
15+
supportsImages: true,
16+
supportsPromptCache: true,
17+
supportsReasoningEffort: ["disable", "low", "high", "max"], // Updated 2026-09-10
18+
preserveReasoning: true,
19+
reasoningEffort: "high",
20+
inputPrice: 0, // the inputs are priced as cache read/write, so `inputPrice` should be 0
21+
// Static estimates use peak rates; off-peak rates are 50% lower. Effective 2026-09-10.
22+
outputPrice: 1.2,
23+
cacheWritesPrice: 0.3,
24+
cacheReadsPrice: 0.006,
25+
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.`,
26+
},
1227
"deepseek-v4-flash": {
1328
maxTokens: 384_000,
1429
contextWindow: 1_000_000,
15-
supportsImages: false,
30+
supportsImages: true,
1631
supportsPromptCache: true,
1732
supportsReasoningEffort: ["disable", "low", "high", "max"], // Updated 2026-08-13
1833
preserveReasoning: true,
1934
reasoningEffort: "high",
2035
inputPrice: 0, // the inputs are priced as cache read/write, so `inputPrice` should be 0
21-
// Static estimates use peak rates; off-peak rates are 50% lower. Effective 2026-08-16.
22-
outputPrice: 1.32,
23-
cacheWritesPrice: 0.44,
24-
cacheReadsPrice: 0.014,
25-
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.`,
36+
// This retired ID is billed as the current Flash model.
37+
outputPrice: 1.2,
38+
cacheWritesPrice: 0.3,
39+
cacheReadsPrice: 0.006,
40+
description: `Legacy model name routed to the latest DeepSeek Flash model, which supports image input. Use deepseek-flash for new configurations.`,
2641
},
2742
"deepseek-v4-pro": {
2843
displayName: "DeepSeek V4 Pro 0813",
@@ -49,14 +64,12 @@ export const deepSeekModels = {
4964
supportsReasoningEffort: ["disable", "low", "high", "max"], // Updated 2026-08-13
5065
preserveReasoning: true,
5166
reasoningEffort: "high",
52-
supportsTemperature: true,
53-
defaultTemperature: 1.0,
5467
inputPrice: 0, // the inputs are priced as cache read/write, so `inputPrice` should be 0
55-
// Static estimates use peak rates; off-peak rates are 50% lower.
56-
outputPrice: 1.32,
57-
cacheWritesPrice: 0.44,
58-
cacheReadsPrice: 0.014,
59-
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.`,
68+
// This retired ID is billed as the current Flash model.
69+
outputPrice: 1.2,
70+
cacheWritesPrice: 0.3,
71+
cacheReadsPrice: 0.006,
72+
description: `Legacy model name routed to the latest DeepSeek Flash model, which supports image input. Use deepseek-flash for new configurations.`,
6073
},
6174
} as const satisfies Record<string, ModelInfo>
6275

‎src/api/providers/__tests__/deepseek.spec.ts‎

Lines changed: 51 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -240,22 +240,22 @@ describe("DeepSeekHandler", () => {
240240
expect(model.info).toBeDefined()
241241
expect(model.info.maxTokens).toBe(384_000)
242242
expect(model.info.contextWindow).toBe(1_000_000)
243-
expect(model.info.supportsImages).toBe(false)
243+
expect(model.info.supportsImages).toBe(true)
244244
expect(model.info.supportsPromptCache).toBe(true) // Should be true now
245245
expect((model.info as ModelInfo).preserveReasoning).toBe(true)
246246
})
247247

248-
it("should use deepseek-v4-flash as the default model ID for new configs", () => {
248+
it("should use deepseek-flash as the default model ID for new configs", () => {
249249
const handlerWithoutModel = new DeepSeekHandler({
250250
...mockOptions,
251251
apiModelId: undefined,
252252
})
253253
const model = handlerWithoutModel.getModel()
254254
expect(model.id).toBe(deepSeekDefaultModelId)
255-
expect(model.id).toBe("deepseek-v4-flash")
255+
expect(model.id).toBe("deepseek-flash")
256256
expect(model.info.maxTokens).toBe(384_000)
257257
expect(model.info.contextWindow).toBe(1_000_000)
258-
expect(model.info.supportsImages).toBe(false)
258+
expect(model.info.supportsImages).toBe(true)
259259
expect((model.info as ModelInfo).supportsReasoningEffort).toContain("max")
260260
})
261261

@@ -290,7 +290,6 @@ describe("DeepSeekHandler", () => {
290290
supportsPromptCache: true,
291291
preserveReasoning: true,
292292
reasoningEffort: "high",
293-
defaultTemperature: 1.0,
294293
})
295294
})
296295

@@ -369,41 +368,61 @@ describe("DeepSeekHandler", () => {
369368
expect(textChunks[0].text).toBe("Test response")
370369
})
371370

372-
it("should send images and V4 thinking controls to deepseek-v4-flash-vision-exp", async () => {
371+
it.each(["deepseek-flash", "deepseek-v4-flash", "deepseek-v4-flash-vision-exp"] as const)(
372+
"should send images and thinking controls to %s",
373+
async (modelId) => {
374+
const visionHandler = new DeepSeekHandler({
375+
...mockOptions,
376+
apiModelId: modelId,
377+
})
378+
const visionMessages: Anthropic.Messages.MessageParam[] = [
379+
{
380+
role: "user",
381+
content: [
382+
{ type: "text", text: "Describe this image." },
383+
{
384+
type: "image",
385+
source: { type: "base64", media_type: "image/png", data: "image-data" },
386+
},
387+
],
388+
},
389+
]
390+
391+
await collectStream(visionHandler.createMessage(systemPrompt, visionMessages))
392+
393+
const callArgs = mockCreate.mock.calls[0][0]
394+
expect(callArgs).toMatchObject({
395+
model: modelId,
396+
thinking: { type: "enabled" },
397+
reasoning_effort: "high",
398+
max_completion_tokens: 200_000,
399+
})
400+
expect(callArgs.temperature).toBeUndefined()
401+
expect(callArgs.messages).toContainEqual({
402+
role: "user",
403+
content: expect.arrayContaining([
404+
{ type: "text", text: expect.stringContaining("Describe this image.") },
405+
{ type: "image_url", image_url: { url: "data:image/png;base64,image-data" } },
406+
]),
407+
})
408+
},
409+
)
410+
411+
it("should use the provider default temperature when reasoning is disabled for the vision alias", async () => {
373412
const visionHandler = new DeepSeekHandler({
374413
...mockOptions,
375414
apiModelId: "deepseek-v4-flash-vision-exp",
415+
enableReasoningEffort: false,
376416
})
377-
const visionMessages: Anthropic.Messages.MessageParam[] = [
378-
{
379-
role: "user",
380-
content: [
381-
{ type: "text", text: "Describe this image." },
382-
{
383-
type: "image",
384-
source: { type: "base64", media_type: "image/png", data: "image-data" },
385-
},
386-
],
387-
},
388-
]
389417

390-
await collectStream(visionHandler.createMessage(systemPrompt, visionMessages))
418+
await collectStream(visionHandler.createMessage(systemPrompt, messages))
391419

392-
const callArgs = mockCreate.mock.calls[0][0]
393-
expect(callArgs).toMatchObject({
420+
expect(mockCreate.mock.calls[0][0]).toMatchObject({
394421
model: "deepseek-v4-flash-vision-exp",
395-
thinking: { type: "enabled" },
396-
reasoning_effort: "high",
397-
max_completion_tokens: 200_000,
398-
})
399-
expect(callArgs.temperature).toBeUndefined()
400-
expect(callArgs.messages).toContainEqual({
401-
role: "user",
402-
content: expect.arrayContaining([
403-
{ type: "text", text: expect.stringContaining("Describe this image.") },
404-
{ type: "image_url", image_url: { url: "data:image/png;base64,image-data" } },
405-
]),
422+
thinking: { type: "disabled" },
423+
temperature: 0,
406424
})
425+
expect(mockCreate.mock.calls[0][0].reasoning_effort).toBeUndefined()
407426
})
408427

409428
it("should include usage information", async () => {

‎src/api/providers/deepseek.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ type DeepSeekChatCompletionParams = Omit<OpenAI.Chat.ChatCompletionCreateParamsS
2828
reasoning_effort?: "low" | "high" | "max"
2929
}
3030

31-
const deepSeekV4ThinkingModels = new Set(["deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v4-flash-vision-exp"])
31+
const deepSeekV4ThinkingModels = new Set([
32+
"deepseek-flash",
33+
"deepseek-v4-flash",
34+
"deepseek-v4-pro",
35+
"deepseek-v4-flash-vision-exp",
36+
])
3237
const supportsDeepSeekThinkingToggle = (modelId: string) => deepSeekV4ThinkingModels.has(modelId)
3338

3439
// Only known V4 models and the legacy reasoner alias support DeepSeek's
@@ -49,6 +54,7 @@ export const normalizeDeepSeekReasoningEffort = (
4954
): "low" | "high" | "max" | undefined => {
5055
// still check the modelId so non-supported models won't produce reasoning efforts
5156
switch (modelId) {
57+
case "deepseek-flash":
5258
case "deepseek-v4-flash":
5359
case "deepseek-v4-pro":
5460
case "deepseek-v4-flash-vision-exp":

‎src/api/providers/fetchers/__tests__/deepseek.spec.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,24 @@ describe("getDeepSeekModels", () => {
2929
const models = await getDeepSeekModels("http://127.0.0.1:43123/v1", "mock-key")
3030

3131
expect(globalThis.fetch).toHaveBeenCalledWith("http://127.0.0.1:43123/models", expect.any(Object))
32+
expect(models["deepseek-flash"]).toEqual(deepSeekModels["deepseek-flash"])
3233
expect(models["deepseek-v4-flash"]).toEqual(deepSeekModels["deepseek-v4-flash"])
3334
expect(models["deepseek-v4-pro"]).toEqual(deepSeekModels["deepseek-v4-pro"])
3435
expect(models["deepseek-v4-flash-vision-exp"]).toEqual(deepSeekModels["deepseek-v4-flash-vision-exp"])
3536
})
3637

38+
it("applies vision metadata to the canonical Flash model returned by DeepSeek", async () => {
39+
globalThis.fetch = vi.fn().mockResolvedValue({
40+
ok: true,
41+
json: vi.fn().mockResolvedValue({ data: [{ id: "deepseek-flash" }] }),
42+
}) as unknown as typeof fetch
43+
44+
const models = await getDeepSeekModels(undefined, "test-key")
45+
46+
expect(models["deepseek-flash"]).toEqual(deepSeekModels["deepseek-flash"])
47+
expect(models["deepseek-flash"].supportsImages).toBe(true)
48+
})
49+
3750
it("throws for 404 responses when fallback flag is not enabled", async () => {
3851
delete process.env.E2E_MOCK_MODEL_LIST_FALLBACK
3952

0 commit comments

Comments
 (0)