Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions packages/types/src/__tests__/deepseek-v4-pro.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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.
Expand Down
41 changes: 27 additions & 14 deletions packages/types/src/providers/deepseek.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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<string, ModelInfo>

Expand Down
83 changes: 51 additions & 32 deletions src/api/providers/__tests__/deepseek.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})

Expand Down Expand Up @@ -290,7 +290,6 @@ describe("DeepSeekHandler", () => {
supportsPromptCache: true,
preserveReasoning: true,
reasoningEffort: "high",
defaultTemperature: 1.0,
})
})

Expand Down Expand Up @@ -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 () => {
Expand Down
8 changes: 7 additions & 1 deletion src/api/providers/deepseek.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@
reasoning_effort?: "low" | "high" | "max"
}

const deepSeekV4ThinkingModels = new Set(["deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v4-flash-vision-exp"])
const deepSeekV4ThinkingModels = new Set([

Check warning on line 31 in src/api/providers/deepseek.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

Survived ArrayDeclaration mutant (replacement: []). See the job summary for the complete list and resolution guidance.
"deepseek-flash",
"deepseek-v4-flash",

Check warning on line 33 in src/api/providers/deepseek.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

Survived StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.
"deepseek-v4-pro",
"deepseek-v4-flash-vision-exp",

Check warning on line 35 in src/api/providers/deepseek.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

Survived StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.
])
const supportsDeepSeekThinkingToggle = (modelId: string) => deepSeekV4ThinkingModels.has(modelId)

// Only known V4 models and the legacy reasoner alias support DeepSeek's
Expand All @@ -49,6 +54,7 @@
): "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":
Expand Down
13 changes: 13 additions & 0 deletions src/api/providers/fetchers/__tests__/deepseek.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading