From 8eb5abf7f8d153396838ea1b94d4684f9c5d606d Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 3 Dec 2025 16:33:54 +0000 Subject: [PATCH] feat: add direct DeepSeek API support for V3/3.2 models - Add model aliases (deepseek-v3, deepseek-3.2) that map to deepseek-chat - Update DeepSeek handler to map alias model names to official API model names - Add tests to verify model alias mapping works correctly - Supports DeepSeek 3.2 models as requested in issue #9779 --- packages/types/src/providers/deepseek.ts | 43 +++++++++---- src/api/providers/__tests__/deepseek.spec.ts | 64 ++++++++++++++++++++ src/api/providers/deepseek.ts | 26 +++++++- 3 files changed, 119 insertions(+), 14 deletions(-) diff --git a/packages/types/src/providers/deepseek.ts b/packages/types/src/providers/deepseek.ts index c5c297cdb9..8a020b3cff 100644 --- a/packages/types/src/providers/deepseek.ts +++ b/packages/types/src/providers/deepseek.ts @@ -1,22 +1,35 @@ import type { ModelInfo } from "../model.js" // https://platform.deepseek.com/docs/api +// https://api-docs.deepseek.com/quick_start/pricing export type DeepSeekModelId = keyof typeof deepSeekModels export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-chat" +// DeepSeek V3 model info (shared between deepseek-chat and aliases) +const deepSeekV3Info: ModelInfo = { + maxTokens: 8192, // 8K max output + contextWindow: 128_000, + supportsImages: false, + supportsPromptCache: true, + supportsNativeTools: true, + 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 + cacheReadsPrice: 0.07, // $0.07 per million tokens (cache hit) - Updated Sept 5, 2025 + description: `DeepSeek-V3 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.`, +} + export const deepSeekModels = { - "deepseek-chat": { - maxTokens: 8192, // 8K max output - contextWindow: 128_000, - supportsImages: false, - supportsPromptCache: true, - supportsNativeTools: true, - 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 - cacheReadsPrice: 0.07, // $0.07 per million tokens (cache hit) - Updated Sept 5, 2025 - description: `DeepSeek-V3 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.`, + "deepseek-chat": deepSeekV3Info, + // Aliases for DeepSeek V3 - these all map to deepseek-chat when calling the API + "deepseek-v3": { + ...deepSeekV3Info, + description: `DeepSeek-V3 (alias for deepseek-chat). ${deepSeekV3Info.description}`, + }, + "deepseek-3.2": { + ...deepSeekV3Info, + description: `DeepSeek V3.2 (alias for deepseek-chat). ${deepSeekV3Info.description}`, }, "deepseek-reasoner": { maxTokens: 65536, // 64K max output for reasoning mode @@ -32,4 +45,12 @@ export const deepSeekModels = { }, } as const satisfies Record +// Map of model aliases to their official API model names +// The DeepSeek API uses specific model names, but users may use alternative names +export const deepSeekModelAliases: Record = { + "deepseek-v3": "deepseek-chat", + "deepseek-3.2": "deepseek-chat", + "deepseek-3.2-exp": "deepseek-chat", +} + export const DEEP_SEEK_DEFAULT_TEMPERATURE = 0.6 diff --git a/src/api/providers/__tests__/deepseek.spec.ts b/src/api/providers/__tests__/deepseek.spec.ts index 50cabfa922..8660588daf 100644 --- a/src/api/providers/__tests__/deepseek.spec.ts +++ b/src/api/providers/__tests__/deepseek.spec.ts @@ -147,6 +147,44 @@ describe("DeepSeekHandler", () => { const _handler = new DeepSeekHandler(mockOptions) expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: mockOptions.deepSeekApiKey })) }) + + it("should map deepseek-v3 alias to deepseek-chat for API calls", async () => { + vi.clearAllMocks() + const handlerWithV3 = new DeepSeekHandler({ + ...mockOptions, + apiModelId: "deepseek-v3", + }) + const stream = handlerWithV3.createMessage("test", []) + for await (const _chunk of stream) { + // consume stream + } + // Verify the API was called with deepseek-chat (not deepseek-v3) + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "deepseek-chat", + }), + expect.anything(), + ) + }) + + it("should map deepseek-3.2 alias to deepseek-chat for API calls", async () => { + vi.clearAllMocks() + const handlerWith32 = new DeepSeekHandler({ + ...mockOptions, + apiModelId: "deepseek-3.2", + }) + const stream = handlerWith32.createMessage("test", []) + for await (const _chunk of stream) { + // consume stream + } + // Verify the API was called with deepseek-chat (not deepseek-3.2) + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "deepseek-chat", + }), + expect.anything(), + ) + }) }) describe("getModel", () => { @@ -174,6 +212,32 @@ describe("DeepSeekHandler", () => { expect(model.info.supportsPromptCache).toBe(true) }) + it("should return correct model info for deepseek-v3 alias", () => { + const handlerWithV3 = new DeepSeekHandler({ + ...mockOptions, + apiModelId: "deepseek-v3", + }) + const model = handlerWithV3.getModel() + expect(model.id).toBe("deepseek-v3") // Returns user's model ID + expect(model.info).toBeDefined() + expect(model.info.maxTokens).toBe(8192) // Same as deepseek-chat + expect(model.info.contextWindow).toBe(128_000) + expect(model.info.supportsNativeTools).toBe(true) + }) + + it("should return correct model info for deepseek-3.2 alias", () => { + const handlerWith32 = new DeepSeekHandler({ + ...mockOptions, + apiModelId: "deepseek-3.2", + }) + const model = handlerWith32.getModel() + expect(model.id).toBe("deepseek-3.2") // Returns user's model ID + expect(model.info).toBeDefined() + expect(model.info.maxTokens).toBe(8192) // Same as deepseek-chat + expect(model.info.contextWindow).toBe(128_000) + expect(model.info.supportsNativeTools).toBe(true) + }) + it("should return provided model ID with default model info if model does not exist", () => { const handlerWithInvalidModel = new DeepSeekHandler({ ...mockOptions, diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index de119de6db..14bd3555d4 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -1,4 +1,4 @@ -import { deepSeekModels, deepSeekDefaultModelId } from "@roo-code/types" +import { deepSeekModels, deepSeekDefaultModelId, deepSeekModelAliases } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" @@ -7,20 +7,40 @@ import { getModelParams } from "../transform/model-params" import { OpenAiHandler } from "./openai" +/** + * Maps a user-provided model ID to the official DeepSeek API model name. + * The DeepSeek API uses specific model names (deepseek-chat, deepseek-reasoner), + * but users may use alternative names like deepseek-v3, deepseek-3.2, etc. + */ +function getApiModelId(modelId: string): string { + return deepSeekModelAliases[modelId] ?? modelId +} + export class DeepSeekHandler extends OpenAiHandler { constructor(options: ApiHandlerOptions) { + const userModelId = options.apiModelId ?? deepSeekDefaultModelId + // Map the user's model ID to the official API model name + const apiModelId = getApiModelId(userModelId) + super({ ...options, openAiApiKey: options.deepSeekApiKey ?? "not-provided", - openAiModelId: options.apiModelId ?? deepSeekDefaultModelId, + openAiModelId: apiModelId, // Use the mapped API model ID openAiBaseUrl: options.deepSeekBaseUrl ?? "https://api.deepseek.com", openAiStreamingEnabled: true, includeMaxTokens: true, }) + + // Store the original user model ID for getModel() + this.userModelId = userModelId } + // Store the user's original model ID (before alias mapping) + private userModelId: string + override getModel() { - const id = this.options.apiModelId ?? deepSeekDefaultModelId + // Use the user's original model ID for info lookup (so they see the model they selected) + const id = this.userModelId const info = deepSeekModels[id as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId] const params = getModelParams({ format: "openai", modelId: id, model: info, settings: this.options }) return { id, info, ...params }