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
This commit is contained in:
Roo Code 2025-12-03 16:33:54 +00:00
parent d48fb302aa
commit 8eb5abf7f8
3 changed files with 119 additions and 14 deletions

View file

@ -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<string, ModelInfo>
// 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<string, string> = {
"deepseek-v3": "deepseek-chat",
"deepseek-3.2": "deepseek-chat",
"deepseek-3.2-exp": "deepseek-chat",
}
export const DEEP_SEEK_DEFAULT_TEMPERATURE = 0.6

View file

@ -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,

View file

@ -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 }