From 58392601166073a7b60ec3c84b66394f3941e936 Mon Sep 17 00:00:00 2001 From: wangxiaolong Date: Tue, 2 Dec 2025 23:12:31 +0800 Subject: [PATCH 1/3] feat(deepseek): support native tool calling for deepseek-reasoner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add custom createMessage override for DeepSeekHandler - Use convertToOpenAiMessages for native tool protocol to properly handle tool_calls in assistant messages and tool role messages for results - Create dedicated OpenAI client for native tool calling support - Delegate to parent implementation for XML protocol or deepseek-chat model Reference: https://api-docs.deepseek.com/zh-cn/guides/thinking_mode#工具调用 --- src/api/providers/deepseek.ts | 122 +++++++++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 2 deletions(-) diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index de119de6dba..f50a8ca766c 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -1,13 +1,25 @@ -import { deepSeekModels, deepSeekDefaultModelId } from "@roo-code/types" +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import { deepSeekModels, deepSeekDefaultModelId, DEEP_SEEK_DEFAULT_TEMPERATURE } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" -import type { ApiStreamUsageChunk } from "../transform/stream" +import { XmlMatcher } from "../../utils/xml-matcher" + +import { convertToOpenAiMessages } from "../transform/openai-format" +import { convertToR1Format } from "../transform/r1-format" +import { ApiStream, type ApiStreamUsageChunk } from "../transform/stream" import { getModelParams } from "../transform/model-params" +import type { ApiHandlerCreateMessageMetadata } from "../index" import { OpenAiHandler } from "./openai" +import { DEFAULT_HEADERS } from "./constants" +import { getApiRequestTimeout } from "./utils/timeout-config" export class DeepSeekHandler extends OpenAiHandler { + private deepSeekClient: OpenAI + constructor(options: ApiHandlerOptions) { super({ ...options, @@ -17,6 +29,14 @@ export class DeepSeekHandler extends OpenAiHandler { openAiStreamingEnabled: true, includeMaxTokens: true, }) + + // Create our own client for native tool calling support + this.deepSeekClient = new OpenAI({ + baseURL: options.deepSeekBaseUrl ?? "https://api.deepseek.com", + apiKey: options.deepSeekApiKey ?? "not-provided", + defaultHeaders: DEFAULT_HEADERS, + timeout: getApiRequestTimeout(), + }) } override getModel() { @@ -26,6 +46,104 @@ export class DeepSeekHandler extends OpenAiHandler { return { id, info, ...params } } + override async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + const { id: modelId, info: modelInfo } = this.getModel() + const isDeepSeekReasoner = modelId.includes("deepseek-reasoner") + + // Only handle deepseek-reasoner with native tool protocol specially + // For other cases, delegate to parent implementation + if (!isDeepSeekReasoner || metadata?.toolProtocol !== "native") { + yield* super.createMessage(systemPrompt, messages, metadata) + return + } + + // For deepseek-reasoner with native tools, use OpenAI format + // which properly handles tool_calls and tool role messages + // Reference: https://api-docs.deepseek.com/zh-cn/guides/thinking_mode#工具调用 + const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = { + role: "system", + content: systemPrompt, + } + + const convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages)] + + const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { + model: modelId, + temperature: this.options.modelTemperature ?? DEEP_SEEK_DEFAULT_TEMPERATURE, + messages: convertedMessages, + stream: true as const, + stream_options: { include_usage: true }, + ...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }), + ...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }), + parallel_tool_calls: metadata.parallelToolCalls ?? false, + } + + // Add max_tokens if needed + if (this.options.includeMaxTokens === true) { + requestOptions.max_completion_tokens = this.options.modelMaxTokens || modelInfo.maxTokens + } + + const stream = await this.deepSeekClient.chat.completions.create(requestOptions) + + const matcher = new XmlMatcher( + "think", + (chunk) => + ({ + type: chunk.matched ? "reasoning" : "text", + text: chunk.data, + }) as const, + ) + + let lastUsage + + for await (const chunk of stream) { + const delta = chunk.choices?.[0]?.delta ?? {} + + if (delta.content) { + for (const c of matcher.update(delta.content)) { + yield c + } + } + + // Handle reasoning_content from DeepSeek's thinking mode + if ("reasoning_content" in delta && delta.reasoning_content) { + yield { + type: "reasoning", + text: (delta.reasoning_content as string | undefined) || "", + } + } + + // Handle tool calls + if (delta.tool_calls) { + for (const toolCall of delta.tool_calls) { + yield { + type: "tool_call_partial", + index: toolCall.index, + id: toolCall.id, + name: toolCall.function?.name, + arguments: toolCall.function?.arguments, + } + } + } + + if (chunk.usage) { + lastUsage = chunk.usage + } + } + + for (const c of matcher.final()) { + yield c + } + + if (lastUsage) { + yield this.processUsageMetrics(lastUsage) + } + } + // Override to handle DeepSeek's usage metrics, including caching. protected override processUsageMetrics(usage: any): ApiStreamUsageChunk { return { From 4819e26afa7eecd536cfd64cc059f033e0e22066 Mon Sep 17 00:00:00 2001 From: WangXiaolong Date: Tue, 2 Dec 2025 23:50:19 +0800 Subject: [PATCH 2/3] Update src/api/providers/deepseek.ts Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- src/api/providers/deepseek.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index f50a8ca766c..e5bb8a0f485 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -8,7 +8,6 @@ import type { ApiHandlerOptions } from "../../shared/api" import { XmlMatcher } from "../../utils/xml-matcher" import { convertToOpenAiMessages } from "../transform/openai-format" -import { convertToR1Format } from "../transform/r1-format" import { ApiStream, type ApiStreamUsageChunk } from "../transform/stream" import { getModelParams } from "../transform/model-params" import type { ApiHandlerCreateMessageMetadata } from "../index" From edbc6ab51629bd0a18942593c40f405e8ddb9a97 Mon Sep 17 00:00:00 2001 From: WangXiaolong Date: Wed, 3 Dec 2025 07:55:59 +0800 Subject: [PATCH 3/3] Add defaultToolProtocol to DeepSeek configuration Added defaultToolProtocol for DeepSeek R1 mode. --- packages/types/src/providers/deepseek.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/types/src/providers/deepseek.ts b/packages/types/src/providers/deepseek.ts index c5c297cdb94..62dee1a2278 100644 --- a/packages/types/src/providers/deepseek.ts +++ b/packages/types/src/providers/deepseek.ts @@ -24,6 +24,7 @@ export const deepSeekModels = { supportsImages: false, supportsPromptCache: true, supportsNativeTools: true, + defaultToolProtocol: "native", // DeepSeek R1 thinking mode works best with native tool calling inputPrice: 0.56, // $0.56 per million tokens (cache miss) - Updated Sept 5, 2025 outputPrice: 1.68, // $1.68 per million tokens - Updated Sept 5, 2025 cacheWritesPrice: 0.56, // $0.56 per million tokens (cache miss) - Updated Sept 5, 2025