Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit f7496cc

Browse files
committed
feat: add DeepSeek V4 Flash and V4 Pro model support
- Add deepseek-v4-flash and deepseek-v4-pro model entries with 1M context window, 384K max output, and correct pricing - Support optional thinking mode via thinking: { type: "enabled" } parameter - Support reasoning_effort parameter (high/max) for V4 models - Update handler to detect V4 models via supportsReasoningEffort and conditionally enable thinking based on user settings - Add comprehensive tests for V4 model info, thinking parameters, and reasoning effort control
1 parent 96d6e43 commit f7496cc

3 files changed

Lines changed: 161 additions & 12 deletions

File tree

packages/types/src/providers/deepseek.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,36 @@ export const deepSeekModels = {
3232
cacheReadsPrice: 0.028, // $0.028 per million tokens (cache hit) - Updated Dec 9, 2025
3333
description: `DeepSeek-V3.2 (Thinking Mode) achieves performance comparable to OpenAI-o1 across math, code, and reasoning tasks. Supports Chain of Thought reasoning with up to 8K output tokens. Supports JSON output, tool calls, and chat prefix completion (beta).`,
3434
},
35+
"deepseek-v4-flash": {
36+
maxTokens: 384_000, // 384K max output
37+
contextWindow: 1_000_000, // 1M context window
38+
supportsImages: false,
39+
supportsPromptCache: true,
40+
supportsReasoningEffort: ["disable", "high"],
41+
reasoningEffort: "high",
42+
preserveReasoning: true,
43+
inputPrice: 0.14, // $0.14 per million tokens (cache miss)
44+
outputPrice: 0.28, // $0.28 per million tokens
45+
cacheWritesPrice: 0.14, // $0.14 per million tokens (cache miss)
46+
cacheReadsPrice: 0.028, // $0.028 per million tokens (cache hit)
47+
description:
48+
"DeepSeek V4 Flash is a fast, cost-effective model with 1M context window and optional thinking mode. Supports thinking and non-thinking modes with configurable reasoning effort.",
49+
},
50+
"deepseek-v4-pro": {
51+
maxTokens: 384_000, // 384K max output
52+
contextWindow: 1_000_000, // 1M context window
53+
supportsImages: false,
54+
supportsPromptCache: true,
55+
supportsReasoningEffort: ["disable", "high"],
56+
reasoningEffort: "high",
57+
preserveReasoning: true,
58+
inputPrice: 1.74, // $1.74 per million tokens (cache miss)
59+
outputPrice: 3.48, // $3.48 per million tokens
60+
cacheWritesPrice: 1.74, // $1.74 per million tokens (cache miss)
61+
cacheReadsPrice: 0.145, // $0.145 per million tokens (cache hit)
62+
description:
63+
"DeepSeek V4 Pro is a high-performance model with 1M context window and optional thinking mode. Delivers top-tier reasoning capabilities with configurable reasoning effort.",
64+
},
3565
} as const satisfies Record<string, ModelInfo>
3666

3767
// https://api-docs.deepseek.com/quick_start/parameter_settings

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

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,16 @@ vi.mock("openai", () => {
3131

3232
// Check if this is a reasoning_content test by looking at model
3333
const isReasonerModel = options.model?.includes("deepseek-reasoner")
34+
const isV4ThinkingModel =
35+
options.thinking?.type === "enabled" && options.model?.includes("deepseek-v4")
36+
const isThinkingModel = isReasonerModel || isV4ThinkingModel
3437
const isToolCallTest = options.tools?.length > 0
3538

3639
// Return async iterator for streaming
3740
return {
3841
[Symbol.asyncIterator]: async function* () {
39-
// For reasoner models, emit reasoning_content first
40-
if (isReasonerModel) {
42+
// For thinking models, emit reasoning_content first
43+
if (isThinkingModel) {
4144
yield {
4245
choices: [
4346
{
@@ -58,8 +61,8 @@ vi.mock("openai", () => {
5861
}
5962
}
6063

61-
// For tool call tests with reasoner, emit tool call
62-
if (isReasonerModel && isToolCallTest) {
64+
// For tool call tests with thinking models, emit tool call
65+
if (isThinkingModel && isToolCallTest) {
6366
yield {
6467
choices: [
6568
{
@@ -508,4 +511,103 @@ describe("DeepSeekHandler", () => {
508511
expect(toolCallChunks[0].name).toBe("get_weather")
509512
})
510513
})
514+
515+
describe("V4 models", () => {
516+
const systemPrompt = "You are a helpful assistant."
517+
const messages: Anthropic.Messages.MessageParam[] = [
518+
{
519+
role: "user",
520+
content: [
521+
{
522+
type: "text" as const,
523+
text: "Hello!",
524+
},
525+
],
526+
},
527+
]
528+
529+
it("should return correct model info for deepseek-v4-flash", () => {
530+
const v4Handler = new DeepSeekHandler({
531+
...mockOptions,
532+
apiModelId: "deepseek-v4-flash",
533+
})
534+
const model = v4Handler.getModel()
535+
expect(model.id).toBe("deepseek-v4-flash")
536+
expect(model.info.maxTokens).toBe(384_000)
537+
expect(model.info.contextWindow).toBe(1_000_000)
538+
expect(model.info.supportsPromptCache).toBe(true)
539+
expect((model.info as ModelInfo).preserveReasoning).toBe(true)
540+
expect((model.info as ModelInfo).supportsReasoningEffort).toEqual(["disable", "high"])
541+
})
542+
543+
it("should return correct model info for deepseek-v4-pro", () => {
544+
const v4Handler = new DeepSeekHandler({
545+
...mockOptions,
546+
apiModelId: "deepseek-v4-pro",
547+
})
548+
const model = v4Handler.getModel()
549+
expect(model.id).toBe("deepseek-v4-pro")
550+
expect(model.info.maxTokens).toBe(384_000)
551+
expect(model.info.contextWindow).toBe(1_000_000)
552+
expect(model.info.supportsPromptCache).toBe(true)
553+
expect((model.info as ModelInfo).preserveReasoning).toBe(true)
554+
expect((model.info as ModelInfo).supportsReasoningEffort).toEqual(["disable", "high"])
555+
})
556+
557+
it("should pass thinking and reasoning_effort for V4 models with default reasoning effort", async () => {
558+
const v4Handler = new DeepSeekHandler({
559+
...mockOptions,
560+
apiModelId: "deepseek-v4-pro",
561+
})
562+
563+
const stream = v4Handler.createMessage(systemPrompt, messages)
564+
for await (const _chunk of stream) {
565+
// Consume the stream
566+
}
567+
568+
expect(mockCreate).toHaveBeenCalledWith(
569+
expect.objectContaining({
570+
thinking: { type: "enabled" },
571+
reasoning_effort: "high",
572+
}),
573+
{},
574+
)
575+
})
576+
577+
it("should NOT pass thinking for V4 models when reasoning effort is disabled", async () => {
578+
const v4Handler = new DeepSeekHandler({
579+
...mockOptions,
580+
apiModelId: "deepseek-v4-flash",
581+
enableReasoningEffort: true,
582+
reasoningEffort: "disable" as any,
583+
})
584+
585+
const stream = v4Handler.createMessage(systemPrompt, messages)
586+
for await (const _chunk of stream) {
587+
// Consume the stream
588+
}
589+
590+
const callArgs = mockCreate.mock.calls[0][0]
591+
expect(callArgs.thinking).toBeUndefined()
592+
expect(callArgs.reasoning_effort).toBeUndefined()
593+
})
594+
595+
it("should handle reasoning_content in streaming responses for V4 models", async () => {
596+
const v4Handler = new DeepSeekHandler({
597+
...mockOptions,
598+
apiModelId: "deepseek-v4-pro",
599+
})
600+
601+
const stream = v4Handler.createMessage(systemPrompt, messages)
602+
const chunks: any[] = []
603+
for await (const chunk of stream) {
604+
chunks.push(chunk)
605+
}
606+
607+
// Should have reasoning chunks since thinking is enabled by default
608+
const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning")
609+
expect(reasoningChunks.length).toBeGreaterThan(0)
610+
expect(reasoningChunks[0].text).toBe("Let me think about this...")
611+
})
612+
})
511613
})

src/api/providers/deepseek.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import {
66
deepSeekDefaultModelId,
77
DEEP_SEEK_DEFAULT_TEMPERATURE,
88
OPENAI_AZURE_AI_INFERENCE_PATH,
9+
type ModelInfo,
910
} from "@roo-code/types"
1011

11-
import type { ApiHandlerOptions } from "../../shared/api"
12+
import { type ApiHandlerOptions, shouldUseReasoningEffort } from "../../shared/api"
1213

1314
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
1415
import { getModelParams } from "../transform/model-params"
@@ -17,9 +18,10 @@ import { convertToR1Format } from "../transform/r1-format"
1718
import { OpenAiHandler } from "./openai"
1819
import type { ApiHandlerCreateMessageMetadata } from "../index"
1920

20-
// Custom interface for DeepSeek params to support thinking mode
21+
// Custom interface for DeepSeek params to support thinking mode and reasoning effort
2122
type DeepSeekChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParamsStreaming & {
2223
thinking?: { type: "enabled" | "disabled" }
24+
reasoning_effort?: string
2325
}
2426

2527
export class DeepSeekHandler extends OpenAiHandler {
@@ -55,27 +57,42 @@ export class DeepSeekHandler extends OpenAiHandler {
5557
const modelId = this.options.apiModelId ?? deepSeekDefaultModelId
5658
const { info: modelInfo } = this.getModel()
5759

58-
// Check if this is a thinking-enabled model (deepseek-reasoner)
59-
const isThinkingModel = modelId.includes("deepseek-reasoner")
60+
// Check if this is a thinking-enabled model
61+
// deepseek-reasoner always uses thinking mode
62+
// V4 models (deepseek-v4-flash, deepseek-v4-pro) support optional thinking via supportsReasoningEffort
63+
const isLegacyThinkingModel = modelId.includes("deepseek-reasoner")
64+
const isV4ThinkingModel = Array.isArray((modelInfo as ModelInfo).supportsReasoningEffort)
65+
const useThinking =
66+
isLegacyThinkingModel ||
67+
(isV4ThinkingModel && shouldUseReasoningEffort({ model: modelInfo, settings: this.options }))
6068

6169
// Convert messages to R1 format (merges consecutive same-role messages)
6270
// This is required for DeepSeek which does not support successive messages with the same role
63-
// For thinking models (deepseek-reasoner), enable mergeToolResultText to preserve reasoning_content
71+
// For thinking models, enable mergeToolResultText to preserve reasoning_content
6472
// during tool call sequences. Without this, environment_details text after tool_results would
6573
// create user messages that cause DeepSeek to drop all previous reasoning_content.
6674
// See: https://api-docs.deepseek.com/guides/thinking_mode
6775
const convertedMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages], {
68-
mergeToolResultText: isThinkingModel,
76+
mergeToolResultText: useThinking,
6977
})
7078

79+
// Resolve reasoning effort for V4 models, filtering out control values
80+
const rawEffort =
81+
isV4ThinkingModel && useThinking
82+
? (this.options.reasoningEffort ?? (modelInfo as ModelInfo).reasoningEffort ?? "high")
83+
: undefined
84+
const reasoningEffort = rawEffort && rawEffort !== "disable" && rawEffort !== "none" ? rawEffort : undefined
85+
7186
const requestOptions: DeepSeekChatCompletionParams = {
7287
model: modelId,
7388
temperature: this.options.modelTemperature ?? DEEP_SEEK_DEFAULT_TEMPERATURE,
7489
messages: convertedMessages,
7590
stream: true as const,
7691
stream_options: { include_usage: true },
77-
// Enable thinking mode for deepseek-reasoner or when tools are used with thinking model
78-
...(isThinkingModel && { thinking: { type: "enabled" } }),
92+
// Enable thinking mode for deepseek-reasoner (always) or V4 models (when reasoning is enabled)
93+
...(useThinking && { thinking: { type: "enabled" } }),
94+
// Add reasoning_effort for V4 models when thinking is enabled
95+
...(reasoningEffort && { reasoning_effort: reasoningEffort as "low" | "medium" | "high" }),
7996
tools: this.convertToolsForOpenAI(metadata?.tools),
8097
tool_choice: metadata?.tool_choice,
8198
parallel_tool_calls: metadata?.parallelToolCalls ?? true,

0 commit comments

Comments
 (0)