import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" import { deepSeekModels, deepSeekDefaultModelId, DEEP_SEEK_DEFAULT_TEMPERATURE, OPENAI_AZURE_AI_INFERENCE_PATH, } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" import { getModelParams } from "../transform/model-params" import { convertToR1Format } from "../transform/r1-format" import { OpenAiHandler } from "./openai" import type { ApiHandlerCreateMessageMetadata } from "../index" // Custom interface for DeepSeek params to support thinking mode type DeepSeekChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParamsStreaming & { thinking?: { type: "enabled" | "disabled" } } export class DeepSeekHandler extends OpenAiHandler { constructor(options: ApiHandlerOptions) { super({ ...options, openAiApiKey: options.deepSeekApiKey ?? "not-provided", openAiModelId: options.apiModelId ?? deepSeekDefaultModelId, openAiBaseUrl: options.deepSeekBaseUrl || "https://api.deepseek.com", openAiStreamingEnabled: true, includeMaxTokens: true, }) } override getModel() { const id = this.options.apiModelId ?? deepSeekDefaultModelId const info = deepSeekModels[id as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId] const params = getModelParams({ format: "openai", modelId: id, model: info, settings: this.options, defaultTemperature: DEEP_SEEK_DEFAULT_TEMPERATURE, }) return { id, info, ...params } } override async *createMessage( systemPrompt: string, messages: Anthropic.Messages.MessageParam[], metadata?: ApiHandlerCreateMessageMetadata, ): ApiStream { const modelId = this.options.apiModelId ?? deepSeekDefaultModelId const { info: modelInfo } = this.getModel() // Check if this is a thinking-enabled model (deepseek-reasoner) const isThinkingModel = modelId.includes("deepseek-reasoner") // 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 // For thinking models (deepseek-reasoner), enable mergeToolResultText to preserve reasoning_content // during tool call sequences. Without this, environment_details text after tool_results would // create user messages that cause DeepSeek to drop all previous reasoning_content. // See: https://api-docs.deepseek.com/guides/thinking_mode const convertedMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages], { mergeToolResultText: isThinkingModel, }) const requestOptions: DeepSeekChatCompletionParams = { model: modelId, temperature: this.options.modelTemperature ?? DEEP_SEEK_DEFAULT_TEMPERATURE, messages: convertedMessages, stream: true as const, stream_options: { include_usage: true }, // Enable thinking mode for deepseek-reasoner or when tools are used with thinking model ...(isThinkingModel && { thinking: { type: "enabled" } }), tools: this.convertToolsForOpenAI(metadata?.tools), tool_choice: metadata?.tool_choice, parallel_tool_calls: metadata?.parallelToolCalls ?? true, } // Add max_tokens if needed this.addMaxTokensIfNeeded(requestOptions, modelInfo) // Check if base URL is Azure AI Inference (for DeepSeek via Azure) const isAzureAiInference = this._isAzureAiInference(this.options.deepSeekBaseUrl) let stream try { stream = await this.client.chat.completions.create( requestOptions, isAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, ) } catch (error) { const { handleOpenAIError } = await import("./utils/openai-error-handler") throw handleOpenAIError(error, "DeepSeek") } let lastUsage for await (const chunk of stream) { const delta = chunk.choices?.[0]?.delta ?? {} // Handle regular text content if (delta.content) { yield { type: "text", text: delta.content, } } // Handle reasoning_content from DeepSeek's interleaved thinking // This is the proper way DeepSeek sends thinking content in streaming if ("reasoning_content" in delta && delta.reasoning_content) { yield { type: "reasoning", text: (delta.reasoning_content as string) || "", } } // 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 } } if (lastUsage) { yield this.processUsageMetrics(lastUsage, modelInfo) } } // Override to handle DeepSeek's usage metrics, including caching. protected override processUsageMetrics(usage: any, _modelInfo?: any): ApiStreamUsageChunk { return { type: "usage", inputTokens: usage?.prompt_tokens || 0, outputTokens: usage?.completion_tokens || 0, cacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens, cacheReadTokens: usage?.prompt_tokens_details?.cached_tokens, } } }