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

Commit 507d248

Browse files
committed
feat(deepseek): add DeepSeek V4 Flash/Pro models with reasoning effort support
- Add deepseek-v4-flash and deepseek-v4-pro model definitions with pricing - Set deepseek-v4-flash as the new default model - Mark legacy deepseek-chat and deepseek-reasoner as deprecated - Add reasoningEffort option to API handler settings (high/max) - Add ensureReasoningContentPreserved safety net to preserve reasoning content during convertToR1Format edge cases - Update zh-CN and en settings localization for thinking budget - Update ThinkingBudget UI component for new model properties
1 parent ad25634 commit 507d248

8 files changed

Lines changed: 167 additions & 24 deletions

File tree

packages/types/src/model.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export type ReasoningEffortWithMinimal = z.infer<typeof reasoningEffortWithMinim
2323
* Extended Reasoning Effort (includes "none" and "minimal")
2424
* Note: "disable" is a UI/control value, not a value sent as effort
2525
*/
26-
export const reasoningEffortsExtended = ["none", "minimal", "low", "medium", "high", "xhigh"] as const
26+
export const reasoningEffortsExtended = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const
2727

2828
export const reasoningEffortExtendedSchema = z.enum(reasoningEffortsExtended)
2929

@@ -32,7 +32,7 @@ export type ReasoningEffortExtended = z.infer<typeof reasoningEffortExtendedSche
3232
/**
3333
* Reasoning Effort user setting (includes "disable")
3434
*/
35-
export const reasoningEffortSettingValues = ["disable", "none", "minimal", "low", "medium", "high", "xhigh"] as const
35+
export const reasoningEffortSettingValues = ["disable", "none", "minimal", "low", "medium", "high", "xhigh", "max"] as const
3636
export const reasoningEffortSettingSchema = z.enum(reasoningEffortSettingValues)
3737

3838
/**
@@ -89,7 +89,7 @@ export const modelInfoSchema = z.object({
8989
defaultTemperature: z.number().optional(),
9090
requiredReasoningBudget: z.boolean().optional(),
9191
supportsReasoningEffort: z
92-
.union([z.boolean(), z.array(z.enum(["disable", "none", "minimal", "low", "medium", "high", "xhigh"]))])
92+
.union([z.boolean(), z.array(z.enum(["disable", "none", "minimal", "low", "medium", "high", "xhigh", "max"]))])
9393
.optional(),
9494
requiredReasoningEffort: z.boolean().optional(),
9595
preserveReasoning: z.boolean().optional(),

packages/types/src/providers/deepseek.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { ModelInfo } from "../model.js"
22

3-
// https://platform.deepseek.com/docs/api
3+
// https://api-docs.deepseek.com/zh-cn/quick_start/pricing
44
// preserveReasoning enables interleaved thinking mode for tool calls:
55
// DeepSeek requires reasoning_content to be passed back during tool call
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-chat"
9+
export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-v4-flash"
1010

1111
export const deepSeekModels = {
1212
"deepseek-chat": {
@@ -18,7 +18,7 @@ export const deepSeekModels = {
1818
outputPrice: 0.42, // $0.42 per million tokens - Updated Dec 9, 2025
1919
cacheWritesPrice: 0.28, // $0.28 per million tokens (cache miss) - Updated Dec 9, 2025
2020
cacheReadsPrice: 0.028, // $0.028 per million tokens (cache hit) - Updated Dec 9, 2025
21-
description: `DeepSeek-V3.2 (Non-thinking Mode) achieves a significant breakthrough in inference speed over previous models. It tops the leaderboard among open-source models and rivals the most advanced closed-source models globally. Supports JSON output, tool calls, chat prefix completion (beta), and FIM completion (beta).`,
21+
description: `DeepSeek-V3.2 (Non-thinking Mode) - Legacy model. Use deepseek-v4-flash for better performance.`,
2222
},
2323
"deepseek-reasoner": {
2424
maxTokens: 8192, // 8K max output
@@ -30,9 +30,37 @@ export const deepSeekModels = {
3030
outputPrice: 0.42, // $0.42 per million tokens - Updated Dec 9, 2025
3131
cacheWritesPrice: 0.28, // $0.28 per million tokens (cache miss) - Updated Dec 9, 2025
3232
cacheReadsPrice: 0.028, // $0.028 per million tokens (cache hit) - Updated Dec 9, 2025
33-
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).`,
33+
description: `DeepSeek-V3.2 (Thinking Mode) - Legacy model. Use deepseek-v4-pro for better performance.`,
34+
},
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+
preserveReasoning: true, // Also supports thinking mode
41+
supportsReasoningEffort: ["disable", "high", "max"],
42+
reasoningEffort: "high",
43+
inputPrice: 0.14, // $0.14 per million tokens (cache miss, ¥1/M)
44+
outputPrice: 0.28, // $0.28 per million tokens (¥2/M)
45+
cacheWritesPrice: 0.14, // $0.14 per million tokens (cache miss, ¥1/M)
46+
cacheReadsPrice: 0.03, // $0.03 per million tokens (cache hit, ¥0.2/M)
47+
description: `DeepSeek-V4-Flash - Fast and efficient model with 1M context window and 384K max output. Supports thinking mode for better reasoning. Best for general tasks. Supports JSON output, tool calls, and prompt caching.`,
48+
},
49+
"deepseek-v4-pro": {
50+
maxTokens: 384_000, // 384K max output
51+
contextWindow: 1_000_000, // 1M context window
52+
supportsImages: false,
53+
supportsPromptCache: true,
54+
preserveReasoning: true, // Enables interleaved thinking mode for tool calls
55+
supportsReasoningEffort: ["disable", "high", "max"],
56+
reasoningEffort: "high",
57+
inputPrice: 1.68, // $1.68 per million tokens (cache miss, ¥12/M)
58+
outputPrice: 3.36, // $3.36 per million tokens (¥24/M)
59+
cacheWritesPrice: 1.68, // $1.68 per million tokens (cache miss, ¥12/M)
60+
cacheReadsPrice: 0.14, // $0.14 per million tokens (cache hit, ¥1/M)
61+
description: `DeepSeek-V4-Pro (Thinking Mode) - Advanced reasoning model with Chain of Thought capabilities. 1M context window, 384K max output. Supports reasoning_effort parameter (high/max) for deeper thinking. Ideal for complex reasoning, math, and code tasks.`,
3462
},
3563
} as const satisfies Record<string, ModelInfo>
3664

3765
// https://api-docs.deepseek.com/quick_start/parameter_settings
38-
export const DEEP_SEEK_DEFAULT_TEMPERATURE = 0.3
66+
export const DEEP_SEEK_DEFAULT_TEMPERATURE = 0.3

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -255,12 +255,11 @@ describe("DeepSeekHandler", () => {
255255
const model = handlerWithInvalidModel.getModel()
256256
expect(model.id).toBe("invalid-model") // Returns provided ID
257257
expect(model.info).toBeDefined()
258-
// With the current implementation, it's the same object reference when using default model info
259-
expect(model.info).toBe(handler.getModel().info)
260-
// Should have the same base properties
261-
expect(model.info.contextWindow).toBe(handler.getModel().info.contextWindow)
262-
// And should have supportsPromptCache set to true
258+
// Falls back to the default model (deepseek-v4-flash) when ID is invalid
259+
expect(model.info.maxTokens).toBe(384_000) // v4-flash: 384K max output
260+
expect(model.info.contextWindow).toBe(1_000_000) // v4-flash: 1M context window
263261
expect(model.info.supportsPromptCache).toBe(true)
262+
expect(model.info.preserveReasoning).toBe(true) // v4-flash supports thinking mode
264263
})
265264

266265
it("should return default model if no model ID is provided", () => {

src/api/providers/deepseek.ts

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type { ApiHandlerCreateMessageMetadata } from "../index"
2020
// Custom interface for DeepSeek params to support thinking mode
2121
type DeepSeekChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParamsStreaming & {
2222
thinking?: { type: "enabled" | "disabled" }
23+
reasoning_effort?: "high" | "max"
2324
}
2425

2526
export class DeepSeekHandler extends OpenAiHandler {
@@ -55,27 +56,44 @@ export class DeepSeekHandler extends OpenAiHandler {
5556
const modelId = this.options.apiModelId ?? deepSeekDefaultModelId
5657
const { info: modelInfo } = this.getModel()
5758

58-
// Check if this is a thinking-enabled model (deepseek-reasoner)
59-
const isThinkingModel = modelId.includes("deepseek-reasoner")
59+
// Whether the model inherently supports thinking mode via preserveReasoning
60+
const hasThinkingCapability = modelInfo.preserveReasoning || modelId.includes("deepseek-v4-pro") || modelId.includes("deepseek-reasoner")
61+
// Respect user's toggle: enableReasoningEffort=false means disable thinking entirely
62+
// reasoningEffort="disable" also turns off thinking
63+
const isThinkingDisabled = this.options.enableReasoningEffort === false || (this.options as any).reasoningEffort === "disable"
64+
const isThinkingModel = hasThinkingCapability && !isThinkingDisabled
6065

6166
// Convert messages to R1 format (merges consecutive same-role messages)
6267
// 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
68+
// For thinking models, enable mergeToolResultText to preserve reasoning_content
6469
// during tool call sequences. Without this, environment_details text after tool_results would
6570
// create user messages that cause DeepSeek to drop all previous reasoning_content.
6671
// See: https://api-docs.deepseek.com/guides/thinking_mode
6772
const convertedMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages], {
6873
mergeToolResultText: isThinkingModel,
6974
})
7075

76+
// Pre-flight check: ensure reasoning_content is preserved on assistant messages
77+
// when thinking mode is enabled. DeepSeek requires reasoning_content from previous
78+
// turns to be passed back, otherwise it returns 400 error.
79+
// See: https://api-docs.deepseek.com/guides/thinking_mode
80+
if (isThinkingModel) {
81+
ensureReasoningContentPreserved(convertedMessages, messages)
82+
}
83+
7184
const requestOptions: DeepSeekChatCompletionParams = {
7285
model: modelId,
7386
temperature: this.options.modelTemperature ?? DEEP_SEEK_DEFAULT_TEMPERATURE,
7487
messages: convertedMessages,
7588
stream: true as const,
7689
stream_options: { include_usage: true },
77-
// Enable thinking mode for deepseek-reasoner or when tools are used with thinking model
90+
// Enable thinking mode for thinking-enabled models (respects user toggle)
7891
...(isThinkingModel && { thinking: { type: "enabled" } }),
92+
// Add reasoning_effort for v4 models (can be "high" or "max")
93+
// Only sent when thinking is enabled; user can set to "max" via settings
94+
...((modelId.includes("deepseek-v4-flash") || modelId.includes("deepseek-v4-pro")) && isThinkingModel && {
95+
reasoning_effort: (this.options as any).reasoningEffort === "max" ? "max" : "high",
96+
}),
7997
tools: this.convertToolsForOpenAI(metadata?.tools),
8098
tool_choice: metadata?.tool_choice,
8199
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
@@ -94,8 +112,29 @@ export class DeepSeekHandler extends OpenAiHandler {
94112
isAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {},
95113
)
96114
} catch (error) {
97-
const { handleOpenAIError } = await import("./utils/openai-error-handler")
98-
throw handleOpenAIError(error, "DeepSeek")
115+
// Attempt graceful degradation for thinking-mode reasoning_content errors.
116+
// This happens when DeepSeek requires reasoning_content to be passed back
117+
// but it was lost during message conversion (e.g., after conversation condense).
118+
// We retry without thinking enabled as a safe fallback.
119+
const errorMessage = String(error)
120+
if (
121+
isThinkingModel &&
122+
errorMessage.includes("reasoning_content") &&
123+
errorMessage.includes("must be passed back")
124+
) {
125+
console.warn("[DeepSeek] reasoning_content missing, retrying without thinking mode")
126+
const retryOptions: DeepSeekChatCompletionParams = {
127+
...requestOptions,
128+
thinking: undefined,
129+
}
130+
stream = await this.client.chat.completions.create(
131+
retryOptions,
132+
isAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {},
133+
)
134+
} else {
135+
const { handleOpenAIError } = await import("./utils/openai-error-handler")
136+
throw handleOpenAIError(error, "DeepSeek")
137+
}
99138
}
100139

101140
let lastUsage
@@ -154,3 +193,72 @@ export class DeepSeekHandler extends OpenAiHandler {
154193
}
155194
}
156195
}
196+
197+
/**
198+
* Pre-flight validation: ensures converted OpenAI messages retain reasoning_content
199+
* from source Anthropic messages when thinking mode is enabled.
200+
*
201+
* DeepSeek's thinking mode requires reasoning_content from previous assistant
202+
* responses to be passed back in subsequent requests within the same turn.
203+
* If convertToR1Format failed to preserve it (e.g., edge cases with nested
204+
* tool calls or conversation condense), we patch it here as a safety net.
205+
*
206+
* @param convertedMessages - The messages after convertToR1Format (will be mutated)
207+
* @param sourceMessages - The original Anthropic messages before conversion
208+
*/
209+
function ensureReasoningContentPreserved(
210+
convertedMessages: OpenAI.Chat.ChatCompletionMessageParam[],
211+
sourceMessages: Anthropic.Messages.MessageParam[],
212+
): void {
213+
// Scan source messages for any assistant message that had reasoning
214+
const sourceReasoning = extractReasoningFromMessages(sourceMessages)
215+
if (!sourceReasoning) {
216+
return // No reasoning in source, nothing to preserve
217+
}
218+
219+
// Check if converted assistant messages already have reasoning_content
220+
const assistantMsgs = convertedMessages.filter((m) => m.role === "assistant")
221+
const hasReasoningInConverted = assistantMsgs.some(
222+
(msg: any) => typeof msg.reasoning_content === "string" && msg.reasoning_content.trim().length > 0,
223+
)
224+
225+
if (hasReasoningInConverted) {
226+
return // Already preserved correctly
227+
}
228+
229+
// Reasoning was lost during conversion — patch it onto the last assistant
230+
// message that has tool_calls (this is the one DeepSeek requires it on).
231+
const lastToolAssistant = [...assistantMsgs].reverse().find((msg: any) => msg.tool_calls)
232+
if (lastToolAssistant) {
233+
;(lastToolAssistant as any).reasoning_content = sourceReasoning
234+
}
235+
}
236+
237+
/**
238+
* Extracts reasoning_content from Anthropic messages.
239+
* Checks both message-level reasoning_content and content blocks with type "reasoning".
240+
*/
241+
function extractReasoningFromMessages(
242+
messages: Anthropic.Messages.MessageParam[],
243+
): string | undefined {
244+
for (const msg of messages) {
245+
if (msg.role !== "assistant") continue
246+
247+
// Check message-level reasoning_content (set by some providers directly)
248+
const msgReasoning = (msg as any).reasoning_content
249+
if (typeof msgReasoning === "string" && msgReasoning.trim().length > 0) {
250+
return msgReasoning
251+
}
252+
253+
// Check content blocks for reasoning type (Task.ts stores it this way)
254+
if (Array.isArray(msg.content)) {
255+
for (const block of msg.content as any[]) {
256+
if (block.type === "reasoning" && typeof block.text === "string" && block.text.trim().length > 0) {
257+
return block.text
258+
}
259+
}
260+
}
261+
}
262+
263+
return undefined
264+
}

src/shared/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ export type ApiHandlerOptions = Omit<ProviderSettings, "apiProvider"> & {
2323
* When undefined, Ollama will use the model's default num_ctx from the Modelfile.
2424
*/
2525
ollamaNumCtx?: number
26+
/**
27+
* Optional reasoning_effort parameter for DeepSeek v4-pro model.
28+
* Controls the depth of reasoning: "high" or "max".
29+
* When undefined, defaults to "high".
30+
*/
31+
reasoningEffort?: "high" | "max"
2632
}
2733

2834
// RouterName

webview-ui/src/components/settings/ThinkingBudget.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
9191
// 1. requiredReasoningEffort is not true, AND
9292
// 2. supportsReasoningEffort is boolean true (not an explicit array)
9393
// When the model provides an explicit array, respect those exact values.
94-
type ReasoningEffortOption = ReasoningEffortWithMinimal | "none" | "disable"
94+
type ReasoningEffortOption = ReasoningEffortWithMinimal | "none" | "disable" | "max"
9595
const shouldAutoAddDisable =
9696
!modelInfo?.requiredReasoningEffort && supports === true && !baseAvailableOptions.includes("disable" as any)
9797
const availableOptions: ReadonlyArray<ReasoningEffortOption> = shouldAutoAddDisable
@@ -240,9 +240,9 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
240240
setApiConfigurationField("enableReasoningEffort", false)
241241
setApiConfigurationField("reasoningEffort", "disable")
242242
} else {
243-
// "none", "minimal", "low", "medium", "high" all enable reasoning
243+
// "none", "minimal", "low", "medium", "high", "max" all enable reasoning
244244
setApiConfigurationField("enableReasoningEffort", true)
245-
setApiConfigurationField("reasoningEffort", value as ReasoningEffortWithMinimal)
245+
setApiConfigurationField("reasoningEffort", value as any)
246246
}
247247
}}>
248248
<SelectTrigger className="w-full">

webview-ui/src/i18n/locales/en/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,8 @@
601601
"low": "Low",
602602
"medium": "Medium",
603603
"high": "High",
604-
"xhigh": "Extra High"
604+
"xhigh": "Extra High",
605+
"max": "Max"
605606
},
606607
"verbosity": {
607608
"label": "Output Verbosity",

webview-ui/src/i18n/locales/zh-CN/settings.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)