Skip to content
43 changes: 32 additions & 11 deletions packages/types/src/providers/deepseek.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
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)
// DeepSeek V3.2 supports thinking mode with tool calling via the "thinking" parameter
// See: https://api-docs.deepseek.com/guides/thinking_mode
const deepSeekV3Info: ModelInfo = {
maxTokens: 8192, // 8K max output
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
supportsReasoningBinary: true, // Supports thinking mode via { thinking: { type: "enabled" } }
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. Supports thinking mode with tool calling when enabled.`,
}

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,
// deepseek-3.2 is an alias for deepseek-chat (V3.2 is the current version)
// Note: The DeepSeek API only supports "deepseek-chat" and "deepseek-reasoner"
// See: https://api-docs.deepseek.com/quick_start/pricing
"deepseek-3.2": {
...deepSeekV3Info,
description: `DeepSeek V3.2 (alias for deepseek-chat). ${deepSeekV3Info.description}`,
},
Comment on lines +31 to 34
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deepseek-3.2-exp alias is missing from the deepSeekModels object. While it's included in deepSeekModelAliases (line 53), there's no corresponding model entry like there is for deepseek-v3 and deepseek-3.2. This creates an inconsistency where deepseek-3.2-exp users will get model info via fallback to the default rather than an explicit entry with a descriptive message.

Suggested change
"deepseek-3.2": {
...deepSeekV3Info,
description: `DeepSeek V3.2 (alias for deepseek-chat). ${deepSeekV3Info.description}`,
},
"deepseek-3.2": {
...deepSeekV3Info,
description: `DeepSeek V3.2 (alias for deepseek-chat). ${deepSeekV3Info.description}`,
},
"deepseek-3.2-exp": {
...deepSeekV3Info,
description: `DeepSeek V3.2 Experimental (alias for deepseek-chat). ${deepSeekV3Info.description}`,
},

Fix it with Roo Code or mention @roomote and request a fix.

"deepseek-reasoner": {
maxTokens: 65536, // 64K max output for reasoning mode
Expand All @@ -32,4 +46,11 @@ export const deepSeekModels = {
},
} as const satisfies Record<string, ModelInfo>

// Map of model aliases to their official API model names
// The DeepSeek API only supports "deepseek-chat" and "deepseek-reasoner"
// See: https://api-docs.deepseek.com/quick_start/pricing
export const deepSeekModelAliases: Record<string, string> = {
"deepseek-3.2": "deepseek-chat",
}

export const DEEP_SEEK_DEFAULT_TEMPERATURE = 0.6
247 changes: 247 additions & 0 deletions src/api/providers/__tests__/deepseek.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,25 @@ describe("DeepSeekHandler", () => {
const _handler = new DeepSeekHandler(mockOptions)
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: mockOptions.deepSeekApiKey }))
})

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", () => {
Expand Down Expand Up @@ -174,6 +193,19 @@ describe("DeepSeekHandler", () => {
expect(model.info.supportsPromptCache).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,
Expand Down Expand Up @@ -317,4 +349,219 @@ describe("DeepSeekHandler", () => {
expect(result.cacheReadTokens).toBeUndefined()
})
})

describe("Thinking Mode Support", () => {
it("should add thinking parameter when enableReasoningEffort is true for V3 models", async () => {
vi.clearAllMocks()
const handlerWithThinking = new DeepSeekHandler({
...mockOptions,
apiModelId: "deepseek-chat",
enableReasoningEffort: true,
})
const stream = handlerWithThinking.createMessage("test", [])
for await (const _chunk of stream) {
// consume stream
}
// Verify the API was called with the thinking parameter
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: "deepseek-chat",
thinking: { type: "enabled" },
}),
)
})

it("should add thinking parameter when enableReasoningEffort is true for deepseek-3.2 alias", async () => {
vi.clearAllMocks()
const handlerWithThinking = new DeepSeekHandler({
...mockOptions,
apiModelId: "deepseek-3.2",
enableReasoningEffort: true,
})
const stream = handlerWithThinking.createMessage("test", [])
for await (const _chunk of stream) {
// consume stream
}
// Verify the API was called with the thinking parameter and mapped model ID
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: "deepseek-chat",
thinking: { type: "enabled" },
}),
)
})

it("should NOT add thinking parameter when enableReasoningEffort is false", async () => {
vi.clearAllMocks()
const handlerWithoutThinking = new DeepSeekHandler({
...mockOptions,
apiModelId: "deepseek-chat",
enableReasoningEffort: false,
})
const stream = handlerWithoutThinking.createMessage("test", [])
for await (const _chunk of stream) {
// consume stream
}
// Verify the API was called WITHOUT the thinking parameter
expect(mockCreate).toHaveBeenCalledWith(
expect.not.objectContaining({
thinking: expect.anything(),
}),
expect.anything(),
)
})

it("should NOT add thinking parameter for deepseek-reasoner model even with enableReasoningEffort", async () => {
vi.clearAllMocks()
const handlerReasoner = new DeepSeekHandler({
...mockOptions,
apiModelId: "deepseek-reasoner",
enableReasoningEffort: true,
})
const stream = handlerReasoner.createMessage("test", [])
for await (const _chunk of stream) {
// consume stream
}
// Verify the API was called WITHOUT the thinking parameter
// (deepseek-reasoner uses R1 format, not thinking mode)
expect(mockCreate).toHaveBeenCalledWith(
expect.not.objectContaining({
thinking: expect.anything(),
}),
expect.anything(),
)
})

it("should handle reasoning_content in response when thinking mode is enabled", async () => {
// Mock a response with reasoning_content
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: { reasoning_content: "Let me think about this..." },
index: 0,
},
],
usage: null,
}
yield {
choices: [
{
delta: { content: "Here is my answer." },
index: 0,
},
],
usage: null,
}
yield {
choices: [
{
delta: {},
index: 0,
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
}
},
}))

const handlerWithThinking = new DeepSeekHandler({
...mockOptions,
apiModelId: "deepseek-chat",
enableReasoningEffort: true,
})
const stream = handlerWithThinking.createMessage("test", [])
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}

// Should have a reasoning chunk
const reasoningChunks = chunks.filter((c) => c.type === "reasoning")
expect(reasoningChunks.length).toBeGreaterThan(0)
expect(reasoningChunks[0].text).toBe("Let me think about this...")

// Should have a text chunk
const textChunks = chunks.filter((c) => c.type === "text")
expect(textChunks.length).toBeGreaterThan(0)
expect(textChunks[0].text).toBe("Here is my answer.")
})

it("should handle tool calls with thinking mode enabled", async () => {
// Mock a response with tool calls in thinking mode
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: { reasoning_content: "I need to call a tool..." },
index: 0,
},
],
usage: null,
}
yield {
choices: [
{
delta: {
tool_calls: [
{
index: 0,
id: "call_123",
function: {
name: "read_file",
arguments: '{"path": "/test.txt"}',
},
},
],
},
index: 0,
},
],
usage: null,
}
yield {
choices: [
{
delta: {},
index: 0,
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 20,
total_tokens: 30,
},
}
},
}))

const handlerWithThinking = new DeepSeekHandler({
...mockOptions,
apiModelId: "deepseek-chat",
enableReasoningEffort: true,
})
// Note: tools are passed in Anthropic format and converted internally
const stream = handlerWithThinking.createMessage("test", [])
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}

// Should have a reasoning chunk
const reasoningChunks = chunks.filter((c) => c.type === "reasoning")
expect(reasoningChunks.length).toBeGreaterThan(0)

// Should have a tool call chunk
const toolCallChunks = chunks.filter((c) => c.type === "tool_call_partial")
expect(toolCallChunks.length).toBeGreaterThan(0)
expect(toolCallChunks[0].name).toBe("read_file")
expect(toolCallChunks[0].id).toBe("call_123")
})
})
})
Loading
Loading