diff --git a/packages/types/src/providers/deepseek.ts b/packages/types/src/providers/deepseek.ts index 0e40c2be13..9387d6a4ae 100644 --- a/packages/types/src/providers/deepseek.ts +++ b/packages/types/src/providers/deepseek.ts @@ -14,13 +14,14 @@ export const deepSeekModels = { contextWindow: 1_000_000, supportsImages: true, supportsPromptCache: true, - supportsReasoningEffort: ["disable", "low", "medium", "high", "xhigh"], + supportsReasoningEffort: ["disable", "low", "high", "max"], // Updated 2026-08-01 preserveReasoning: true, reasoningEffort: "high", - inputPrice: 0.14, // $0.14 per million tokens (cache miss) - Updated Apr 29, 2026 - outputPrice: 0.28, // $0.28 per million tokens - Updated Apr 29, 2026 - cacheWritesPrice: 0.14, // $0.14 per million tokens (cache miss) - Updated Apr 29, 2026 - cacheReadsPrice: 0.0028, // $0.0028 per million tokens (cache hit) - Updated Apr 29, 2026 + inputPrice: 0, // the inputs are priced as cache read/write, so `inputPrice` should be 0 + // the peak/off-peak pricing policy has not been implemented yet - Updated 2026-08-01 + outputPrice: 0.28, // $0.28 per million tokens - Updated 2026-08-01 + cacheWritesPrice: 0.14, // $0.14 per million tokens (cache miss) - Updated 2026-08-01 + cacheReadsPrice: 0.0028, // $0.0028 per million tokens (cache hit) - Updated 2026-08-01 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.`, }, "deepseek-v4-pro": { @@ -28,42 +29,17 @@ export const deepSeekModels = { contextWindow: 1_000_000, supportsImages: true, supportsPromptCache: true, - supportsReasoningEffort: ["disable", "low", "medium", "high", "xhigh"], + supportsReasoningEffort: ["disable", "high", "max"], // Updated 2026-08-01 preserveReasoning: true, reasoningEffort: "high", - // TODO(deepseek): Re-check V4 Pro discounted prices after DeepSeek's 2026-05-31 discount end date. - inputPrice: 0.435, // $0.435 per million tokens (cache miss, discounted) - Updated Apr 29, 2026 - outputPrice: 0.87, // $0.87 per million tokens (discounted) - Updated Apr 29, 2026 - cacheWritesPrice: 0.435, // $0.435 per million tokens (cache miss, discounted) - Updated Apr 29, 2026 - cacheReadsPrice: 0.003625, // $0.003625 per million tokens (cache hit, discounted) - Updated Apr 29, 2026 + inputPrice: 0, // the inputs are priced as cache read/write, so `inputPrice` should be 0 + // the peak/off-peak pricing policy has not been implemented yet - Updated 2026-08-01 + outputPrice: 0.87, // $0.87 per million tokens - Updated 2026-08-01 + cacheWritesPrice: 0.435, // $0.435 per million tokens (cache miss) - Updated 2026-08-01 + cacheReadsPrice: 0.003625, // $0.003625 per million tokens (cache hit) - Updated 2026-08-01 description: `DeepSeek-V4-Pro 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.`, }, - // TODO(deepseek): Remove this compatibility alias after DeepSeek's 2026-07-24 retirement date. - "deepseek-chat": { - maxTokens: 8192, // 8K max output - contextWindow: 128_000, - supportsImages: false, - supportsPromptCache: true, - inputPrice: 0.28, // $0.28 per million tokens (cache miss) - Updated Dec 9, 2025 - outputPrice: 0.42, // $0.42 per million tokens - Updated Dec 9, 2025 - cacheWritesPrice: 0.28, // $0.28 per million tokens (cache miss) - Updated Dec 9, 2025 - cacheReadsPrice: 0.028, // $0.028 per million tokens (cache hit) - Updated Dec 9, 2025 - description: `Legacy compatibility alias for the non-thinking mode of deepseek-v4-flash. DeepSeek plans to deprecate this model name on 2026-07-24.`, - }, - // TODO(deepseek): Remove this compatibility alias after DeepSeek's 2026-07-24 retirement date. - "deepseek-reasoner": { - maxTokens: 8192, // 8K max output - contextWindow: 128_000, - supportsImages: false, - supportsPromptCache: true, - preserveReasoning: true, - inputPrice: 0.28, // $0.28 per million tokens (cache miss) - Updated Dec 9, 2025 - outputPrice: 0.42, // $0.42 per million tokens - Updated Dec 9, 2025 - cacheWritesPrice: 0.28, // $0.28 per million tokens (cache miss) - Updated Dec 9, 2025 - cacheReadsPrice: 0.028, // $0.028 per million tokens (cache hit) - Updated Dec 9, 2025 - description: `Legacy compatibility alias for the thinking mode of deepseek-v4-flash. DeepSeek plans to deprecate this model name on 2026-07-24.`, - }, } as const satisfies Record // https://api-docs.deepseek.com/quick_start/parameter_settings -export const DEEP_SEEK_DEFAULT_TEMPERATURE = 0.3 +export const DEEP_SEEK_DEFAULT_TEMPERATURE = 0.0 diff --git a/src/api/providers/__tests__/deepseek.spec.ts b/src/api/providers/__tests__/deepseek.spec.ts index cc80e8769c..02f0ad4a6d 100644 --- a/src/api/providers/__tests__/deepseek.spec.ts +++ b/src/api/providers/__tests__/deepseek.spec.ts @@ -124,11 +124,11 @@ vi.mock("openai", () => { import OpenAI from "openai" import type { Anthropic } from "@anthropic-ai/sdk" -import { deepSeekDefaultModelId, DEEP_SEEK_DEFAULT_TEMPERATURE, type ModelInfo } from "@roo-code/types" +import { deepSeekDefaultModelId, DEEP_SEEK_DEFAULT_TEMPERATURE, type ModelInfo, DeepSeekModelId } from "@roo-code/types" import type { ApiHandlerOptions } from "../../../shared/api" -import { DeepSeekHandler } from "../deepseek" +import { DeepSeekHandler, normalizeDeepSeekReasoningEffort } from "../deepseek" describe("DeepSeekHandler", () => { let handler: DeepSeekHandler @@ -137,7 +137,7 @@ describe("DeepSeekHandler", () => { beforeEach(() => { mockOptions = { deepSeekApiKey: "test-api-key", - apiModelId: "deepseek-chat", + apiModelId: "deepseek-v4-flash", deepSeekBaseUrl: "https://api.deepseek.com", } handler = new DeepSeekHandler(mockOptions) @@ -208,11 +208,11 @@ describe("DeepSeekHandler", () => { const model = handler.getModel() expect(model.id).toBe(mockOptions.apiModelId) expect(model.info).toBeDefined() - expect(model.info.maxTokens).toBe(8192) // deepseek-chat legacy alias has 8K max - expect(model.info.contextWindow).toBe(128_000) - expect(model.info.supportsImages).toBe(false) + expect(model.info.maxTokens).toBe(384_000) + expect(model.info.contextWindow).toBe(1_000_000) + expect(model.info.supportsImages).toBe(true) expect(model.info.supportsPromptCache).toBe(true) // Should be true now - expect((model.info as ModelInfo).preserveReasoning).toBeUndefined() + expect((model.info as ModelInfo).preserveReasoning).toBe(true) }) it("should use deepseek-v4-flash as the default model ID for new configs", () => { @@ -226,21 +226,7 @@ describe("DeepSeekHandler", () => { expect(model.info.maxTokens).toBe(384_000) expect(model.info.contextWindow).toBe(1_000_000) expect(model.info.supportsImages).toBe(true) - expect((model.info as ModelInfo).supportsReasoningEffort).toContain("xhigh") - }) - - it("should return correct model info for deepseek-reasoner", () => { - const handlerWithReasoner = new DeepSeekHandler({ - ...mockOptions, - apiModelId: "deepseek-reasoner", - }) - const model = handlerWithReasoner.getModel() - expect(model.id).toBe("deepseek-reasoner") - expect(model.info).toBeDefined() - expect(model.info.maxTokens).toBe(8192) // deepseek-reasoner has 8K max - expect(model.info.contextWindow).toBe(128_000) - expect(model.info.supportsImages).toBe(false) - expect(model.info.supportsPromptCache).toBe(true) + expect((model.info as ModelInfo).supportsReasoningEffort).toContain("max") }) it("should return correct model info for deepseek-v4-pro", () => { @@ -259,31 +245,6 @@ describe("DeepSeekHandler", () => { expect((model.info as ModelInfo).reasoningEffort).toBe("high") }) - it("should have preserveReasoning enabled for deepseek-reasoner to support interleaved thinking", () => { - // This is critical for DeepSeek's interleaved thinking mode with tool calls. - // See: https://api-docs.deepseek.com/guides/thinking_mode - // The reasoning_content needs to be passed back during tool call continuation - // within the same turn for the model to continue reasoning properly. - const handlerWithReasoner = new DeepSeekHandler({ - ...mockOptions, - apiModelId: "deepseek-reasoner", - }) - const model = handlerWithReasoner.getModel() - // Cast to ModelInfo to access preserveReasoning which is an optional property - expect((model.info as ModelInfo).preserveReasoning).toBe(true) - }) - - it("should NOT have preserveReasoning enabled for deepseek-chat", () => { - // deepseek-chat doesn't use thinking mode, so no need to preserve reasoning - const chatHandler = new DeepSeekHandler({ - ...mockOptions, - apiModelId: "deepseek-chat", - }) - const model = chatHandler.getModel() - // Cast to ModelInfo to access preserveReasoning which is an optional property - expect((model.info as ModelInfo).preserveReasoning).toBeUndefined() - }) - it("should return provided model ID with default model info if model does not exist", () => { const handlerWithInvalidModel = new DeepSeekHandler({ ...mockOptions, @@ -540,10 +501,10 @@ describe("DeepSeekHandler", () => { }, ] - it("should handle reasoning_content in streaming responses for deepseek-reasoner", async () => { + it("should handle reasoning_content in streaming responses for deepseek-v4-pro", async () => { const reasonerHandler = new DeepSeekHandler({ ...mockOptions, - apiModelId: "deepseek-reasoner", + apiModelId: "deepseek-v4-pro", }) const stream = reasonerHandler.createMessage(systemPrompt, messages) @@ -559,10 +520,10 @@ describe("DeepSeekHandler", () => { expect(reasoningChunks[1].text).toBe(" I'll analyze step by step.") }) - it("should pass thinking parameter for deepseek-reasoner model", async () => { + it("should pass thinking parameter for deepseek-v4-pro model", async () => { const reasonerHandler = new DeepSeekHandler({ ...mockOptions, - apiModelId: "deepseek-reasoner", + apiModelId: "deepseek-v4-pro", }) const stream = reasonerHandler.createMessage(systemPrompt, messages) @@ -579,7 +540,7 @@ describe("DeepSeekHandler", () => { {}, // Empty path options for non-Azure URLs ) const callArgs = mockCreate.mock.calls[0][0] - expect(callArgs.reasoning_effort).toBeUndefined() + expect(callArgs.reasoning_effort).toBe("high") }) it("should enable thinking by default for deepseek-v4-flash", async () => { @@ -619,27 +580,6 @@ describe("DeepSeekHandler", () => { expect(callArgs.max_completion_tokens).toBe(32_000) }) - it("should map xhigh reasoning effort to DeepSeek max effort", async () => { - const v4Handler = new DeepSeekHandler({ - ...mockOptions, - apiModelId: "deepseek-v4-pro", - reasoningEffort: "xhigh", - }) - - const stream = v4Handler.createMessage(systemPrompt, messages) - for await (const _chunk of stream) { - // Consume the stream - } - - expect(mockCreate).toHaveBeenCalledWith( - expect.objectContaining({ - thinking: { type: "enabled" }, - reasoning_effort: "max", - }), - {}, - ) - }) - it("should disable thinking for deepseek-v4 models when reasoning is disabled", async () => { const v4Handler = new DeepSeekHandler({ ...mockOptions, @@ -674,26 +614,10 @@ describe("DeepSeekHandler", () => { expect(callArgs.temperature).toBe(DEEP_SEEK_DEFAULT_TEMPERATURE) }) - it("should NOT pass thinking parameter for deepseek-chat model", async () => { - const chatHandler = new DeepSeekHandler({ - ...mockOptions, - apiModelId: "deepseek-chat", - }) - - const stream = chatHandler.createMessage(systemPrompt, messages) - for await (const _chunk of stream) { - // Consume the stream - } - - // Verify that the thinking parameter was NOT passed to the API - const callArgs = mockCreate.mock.calls[0][0] - expect(callArgs.thinking).toBeUndefined() - }) - it("should handle tool calls with reasoning_content", async () => { const reasonerHandler = new DeepSeekHandler({ ...mockOptions, - apiModelId: "deepseek-reasoner", + apiModelId: "deepseek-v4-pro", }) const tools: any[] = [ @@ -723,4 +647,71 @@ describe("DeepSeekHandler", () => { expect(toolCallChunks[0].name).toBe("get_weather") }) }) + + describe("normalizeDeepSeekReasoningEffort", () => { + // https://api-docs.deepseek.com/guides/thinking_mode/ + it("should map acceptable reasoning efforts the same way as stated by the official documentation", async () => { + const mappings: { + modelId: DeepSeekModelId + rawReasoningEffort: string + mappedReasoningEffort: string | undefined + }[] = [ + { + modelId: "deepseek-v4-flash", + rawReasoningEffort: "disable", + mappedReasoningEffort: undefined, + }, + { + modelId: "deepseek-v4-flash", + rawReasoningEffort: "low", + mappedReasoningEffort: "low", + }, + { + modelId: "deepseek-v4-flash", + rawReasoningEffort: "high", + mappedReasoningEffort: "high", + }, + { + modelId: "deepseek-v4-flash", + rawReasoningEffort: "xhigh", + mappedReasoningEffort: "high", + }, + { + modelId: "deepseek-v4-flash", + rawReasoningEffort: "max", + mappedReasoningEffort: "max", + }, + { + modelId: "deepseek-v4-pro", + rawReasoningEffort: "disable", + mappedReasoningEffort: undefined, + }, + { + modelId: "deepseek-v4-pro", + rawReasoningEffort: "low", + mappedReasoningEffort: "high", + }, + { + modelId: "deepseek-v4-pro", + rawReasoningEffort: "high", + mappedReasoningEffort: "high", + }, + { + modelId: "deepseek-v4-pro", + rawReasoningEffort: "xhigh", + mappedReasoningEffort: "max", + }, + { + modelId: "deepseek-v4-pro", + rawReasoningEffort: "max", + mappedReasoningEffort: "max", + }, + ] + + for (const { modelId, rawReasoningEffort, mappedReasoningEffort } of mappings) { + const result = normalizeDeepSeekReasoningEffort(modelId, rawReasoningEffort) + expect(result).toBe(mappedReasoningEffort) + } + }) + }) }) diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index ef7839ad34..2e85c016b0 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -7,6 +7,7 @@ import { DEEP_SEEK_DEFAULT_TEMPERATURE, OPENAI_AZURE_AI_INFERENCE_PATH, type ModelInfo, + DeepSeekModelId, } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" @@ -23,7 +24,7 @@ import { handleOpenAIError } from "./utils/error-handler" // Custom interface for DeepSeek params to support thinking mode type DeepSeekChatCompletionParams = Omit & { thinking?: { type: "enabled" | "disabled" } - reasoning_effort?: "high" | "max" + reasoning_effort?: "low" | "high" | "max" } const deepSeekV4ThinkingModels = new Set(["deepseek-v4-flash", "deepseek-v4-pro"]) @@ -37,16 +38,49 @@ const isDeepSeekThinkingEnabled = (modelId: string, options: ApiHandlerOptions) return false } - return modelId === "deepseek-reasoner" || supportsDeepSeekThinkingToggle(modelId) + return supportsDeepSeekThinkingToggle(modelId) } -const normalizeDeepSeekReasoningEffort = (reasoningEffort?: string): "high" | "max" | undefined => { - if (!reasoningEffort || reasoningEffort === "disable") { - return undefined +// https://api-docs.deepseek.com/guides/thinking_mode/ +export const normalizeDeepSeekReasoningEffort = ( + modelId: DeepSeekModelId, + reasoningEffort?: string, +): "low" | "high" | "max" | undefined => { + switch (modelId) { + case "deepseek-v4-flash": + switch (reasoningEffort) { + case "low": + return "low" + + case "high": + return "high" + + case "xhigh": + return "high" + + case "max": + return "max" + } + break + + case "deepseek-v4-pro": + switch (reasoningEffort) { + case "low": + return "high" + + case "high": + return "high" + + case "xhigh": + return "max" + + case "max": + return "max" + } + break } - // DeepSeek currently maps low/medium to high and xhigh to max in thinking mode. - return reasoningEffort === "xhigh" ? "max" : "high" + return undefined } // Use the computed maxTokens from getModelParams rather than raw model metadata. @@ -92,7 +126,7 @@ export class DeepSeekHandler extends OpenAiHandler { messages: Anthropic.Messages.MessageParam[], metadata?: ApiHandlerCreateMessageMetadata, ): ApiStream { - const modelId = this.options.apiModelId ?? deepSeekDefaultModelId + const modelId = (this.options.apiModelId as DeepSeekModelId) ?? deepSeekDefaultModelId const { info: modelInfo, temperature, reasoningEffort, maxTokens } = this.getModel() const isThinkingModel = isDeepSeekThinkingEnabled(modelId, this.options) @@ -101,7 +135,7 @@ export class DeepSeekHandler extends OpenAiHandler { : isThinkingModel ? ({ type: "enabled" } as const) : undefined - const deepSeekReasoningEffort = isThinkingModel ? normalizeDeepSeekReasoningEffort(reasoningEffort) : undefined + const deepSeekReasoningEffort = normalizeDeepSeekReasoningEffort(modelId, reasoningEffort) // Convert messages to R1 format (merges consecutive same-role messages) // This is required for DeepSeek which does not support successive messages with the same role diff --git a/src/core/config/__tests__/importExport.spec.ts b/src/core/config/__tests__/importExport.spec.ts index 9cbb364c92..313183c795 100644 --- a/src/core/config/__tests__/importExport.spec.ts +++ b/src/core/config/__tests__/importExport.spec.ts @@ -2510,21 +2510,15 @@ describe("importExport", () => { { testCase: "supportsReasoningBudget is false", providerName: "deepseek-provider", - modelId: "deepseek-chat", + modelId: "deepseek-v4-flash", providerId: "deepseek-id", }, { testCase: "requiredReasoningBudget is false", providerName: "deepseek-provider-2", - modelId: "deepseek-coder", + modelId: "deepseek-v4-pro", providerId: "deepseek-id-2", }, - { - testCase: "both supportsReasoningBudget and requiredReasoningBudget are false", - providerName: "deepseek-provider-3", - modelId: "deepseek-reasoner", - providerId: "deepseek-id-3", - }, ])( "should exclude modelMaxTokens and modelMaxThinkingTokens when $testCase", async ({ providerName, modelId, providerId }) => { diff --git a/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts index a50e73cb16..3ceeb2f895 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts @@ -128,7 +128,7 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => { getModelsMock.mockImplementation(async (options: any) => { if (options?.provider === "deepseek") { - return { "deepseek-chat": { contextWindow: 128000, supportsPromptCache: true } } + return { "deepseek-v4-flash": { contextWindow: 1_000_000, supportsPromptCache: true } } } switch (options?.provider) { @@ -163,7 +163,7 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => { ) expect(call).toBeTruthy() expect(call[0].routerModels.deepseek).toEqual({ - "deepseek-chat": { contextWindow: 128000, supportsPromptCache: true }, + "deepseek-v4-flash": { contextWindow: 1_000_000, supportsPromptCache: true }, }) })