Skip to content

Commit 438b1eb

Browse files
authored
Merge pull request #239 from RooVetGit/deepseek
Add DeepSeek to the list of providers
2 parents 948c66c + 6290f90 commit 438b1eb

File tree

9 files changed

+270
-3
lines changed

9 files changed

+270
-3
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Add the DeepSeek provider

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ A fork of Cline, an autonomous coding agent, with some additional experimental f
1313
- Includes current time in the system prompt
1414
- Uses a file system watcher to more reliably watch for file system changes
1515
- Language selection for Cline's communication (English, Japanese, Spanish, French, German, and more)
16+
- Support for DeepSeek V3
1617
- Support for Meta 3, 3.1, and 3.2 models via AWS Bedrock
1718
- Support for listing models from OpenAI-compatible providers
1819
- Per-tool MCP auto-approval

src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { OllamaHandler } from "./providers/ollama"
99
import { LmStudioHandler } from "./providers/lmstudio"
1010
import { GeminiHandler } from "./providers/gemini"
1111
import { OpenAiNativeHandler } from "./providers/openai-native"
12+
import { DeepSeekHandler } from "./providers/deepseek"
1213
import { ApiStream } from "./transform/stream"
1314

1415
export interface SingleCompletionHandler {
@@ -41,6 +42,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
4142
return new GeminiHandler(options)
4243
case "openai-native":
4344
return new OpenAiNativeHandler(options)
45+
case "deepseek":
46+
return new DeepSeekHandler(options)
4447
default:
4548
return new AnthropicHandler(options)
4649
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { DeepSeekHandler } from '../deepseek'
2+
import { ApiHandlerOptions } from '../../../shared/api'
3+
import OpenAI from 'openai'
4+
import { Anthropic } from '@anthropic-ai/sdk'
5+
6+
// Mock dependencies
7+
jest.mock('openai')
8+
9+
describe('DeepSeekHandler', () => {
10+
11+
const mockOptions: ApiHandlerOptions = {
12+
deepSeekApiKey: 'test-key',
13+
deepSeekModelId: 'deepseek-chat',
14+
}
15+
16+
beforeEach(() => {
17+
jest.clearAllMocks()
18+
})
19+
20+
test('constructor initializes with correct options', () => {
21+
const handler = new DeepSeekHandler(mockOptions)
22+
expect(handler).toBeInstanceOf(DeepSeekHandler)
23+
expect(OpenAI).toHaveBeenCalledWith({
24+
baseURL: 'https://api.deepseek.com/v1',
25+
apiKey: mockOptions.deepSeekApiKey,
26+
})
27+
})
28+
29+
test('getModel returns correct model info', () => {
30+
const handler = new DeepSeekHandler(mockOptions)
31+
const result = handler.getModel()
32+
33+
expect(result).toEqual({
34+
id: mockOptions.deepSeekModelId,
35+
info: expect.objectContaining({
36+
maxTokens: 8192,
37+
contextWindow: 64000,
38+
supportsPromptCache: false,
39+
supportsImages: false,
40+
inputPrice: 0.014,
41+
outputPrice: 0.28,
42+
})
43+
})
44+
})
45+
46+
test('getModel returns default model info when no model specified', () => {
47+
const handler = new DeepSeekHandler({ deepSeekApiKey: 'test-key' })
48+
const result = handler.getModel()
49+
50+
expect(result.id).toBe('deepseek-chat')
51+
expect(result.info.maxTokens).toBe(8192)
52+
})
53+
54+
test('createMessage handles string content correctly', async () => {
55+
const handler = new DeepSeekHandler(mockOptions)
56+
const mockStream = {
57+
async *[Symbol.asyncIterator]() {
58+
yield {
59+
choices: [{
60+
delta: {
61+
content: 'test response'
62+
}
63+
}]
64+
}
65+
}
66+
}
67+
68+
const mockCreate = jest.fn().mockResolvedValue(mockStream)
69+
;(OpenAI as jest.MockedClass<typeof OpenAI>).prototype.chat = {
70+
completions: { create: mockCreate }
71+
} as any
72+
73+
const systemPrompt = 'test system prompt'
74+
const messages: Anthropic.Messages.MessageParam[] = [
75+
{ role: 'user', content: 'test message' }
76+
]
77+
78+
const generator = handler.createMessage(systemPrompt, messages)
79+
const chunks = []
80+
81+
for await (const chunk of generator) {
82+
chunks.push(chunk)
83+
}
84+
85+
expect(chunks).toHaveLength(1)
86+
expect(chunks[0]).toEqual({
87+
type: 'text',
88+
text: 'test response'
89+
})
90+
91+
expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({
92+
model: mockOptions.deepSeekModelId,
93+
messages: [
94+
{ role: 'system', content: systemPrompt },
95+
{ role: 'user', content: 'test message' }
96+
],
97+
temperature: 0,
98+
stream: true,
99+
max_tokens: 8192,
100+
stream_options: { include_usage: true }
101+
}))
102+
})
103+
104+
test('createMessage handles complex content correctly', async () => {
105+
const handler = new DeepSeekHandler(mockOptions)
106+
const mockStream = {
107+
async *[Symbol.asyncIterator]() {
108+
yield {
109+
choices: [{
110+
delta: {
111+
content: 'test response'
112+
}
113+
}]
114+
}
115+
}
116+
}
117+
118+
const mockCreate = jest.fn().mockResolvedValue(mockStream)
119+
;(OpenAI as jest.MockedClass<typeof OpenAI>).prototype.chat = {
120+
completions: { create: mockCreate }
121+
} as any
122+
123+
const systemPrompt = 'test system prompt'
124+
const messages: Anthropic.Messages.MessageParam[] = [
125+
{
126+
role: 'user',
127+
content: [
128+
{ type: 'text', text: 'part 1' },
129+
{ type: 'text', text: 'part 2' }
130+
]
131+
}
132+
]
133+
134+
const generator = handler.createMessage(systemPrompt, messages)
135+
await generator.next()
136+
137+
expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({
138+
messages: [
139+
{ role: 'system', content: systemPrompt },
140+
{
141+
role: 'user',
142+
content: [
143+
{ type: 'text', text: 'part 1' },
144+
{ type: 'text', text: 'part 2' }
145+
]
146+
}
147+
]
148+
}))
149+
})
150+
151+
test('createMessage handles API errors', async () => {
152+
const handler = new DeepSeekHandler(mockOptions)
153+
const mockStream = {
154+
async *[Symbol.asyncIterator]() {
155+
throw new Error('API Error')
156+
}
157+
}
158+
159+
const mockCreate = jest.fn().mockResolvedValue(mockStream)
160+
;(OpenAI as jest.MockedClass<typeof OpenAI>).prototype.chat = {
161+
completions: { create: mockCreate }
162+
} as any
163+
164+
const generator = handler.createMessage('test', [])
165+
await expect(generator.next()).rejects.toThrow('API Error')
166+
})
167+
})

src/api/providers/deepseek.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { OpenAiHandler } from "./openai"
2+
import { ApiHandlerOptions, ModelInfo } from "../../shared/api"
3+
import { deepSeekModels, deepSeekDefaultModelId } from "../../shared/api"
4+
5+
export class DeepSeekHandler extends OpenAiHandler {
6+
constructor(options: ApiHandlerOptions) {
7+
if (!options.deepSeekApiKey) {
8+
throw new Error("DeepSeek API key is required. Please provide it in the settings.")
9+
}
10+
super({
11+
...options,
12+
openAiApiKey: options.deepSeekApiKey,
13+
openAiModelId: options.deepSeekModelId ?? deepSeekDefaultModelId,
14+
openAiBaseUrl: options.deepSeekBaseUrl ?? "https://api.deepseek.com/v1",
15+
includeMaxTokens: true
16+
})
17+
}
18+
19+
override getModel(): { id: string; info: ModelInfo } {
20+
const modelId = this.options.deepSeekModelId ?? deepSeekDefaultModelId
21+
return {
22+
id: modelId,
23+
info: deepSeekModels[modelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId]
24+
}
25+
}
26+
}

src/api/providers/openai.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { convertToOpenAiMessages } from "../transform/openai-format"
1111
import { ApiStream } from "../transform/stream"
1212

1313
export class OpenAiHandler implements ApiHandler {
14-
private options: ApiHandlerOptions
14+
protected options: ApiHandlerOptions
1515
private client: OpenAI
1616

1717
constructor(options: ApiHandlerOptions) {
@@ -38,12 +38,16 @@ export class OpenAiHandler implements ApiHandler {
3838
{ role: "system", content: systemPrompt },
3939
...convertToOpenAiMessages(messages),
4040
]
41+
const modelInfo = this.getModel().info
4142
const requestOptions: OpenAI.Chat.ChatCompletionCreateParams = {
4243
model: this.options.openAiModelId ?? "",
4344
messages: openAiMessages,
4445
temperature: 0,
4546
stream: true,
4647
}
48+
if (this.options.includeMaxTokens) {
49+
requestOptions.max_tokens = modelInfo.maxTokens
50+
}
4751

4852
if (this.options.includeStreamOptions ?? true) {
4953
requestOptions.stream_options = { include_usage: true }

src/core/webview/ClineProvider.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type SecretKey =
4040
| "openAiApiKey"
4141
| "geminiApiKey"
4242
| "openAiNativeApiKey"
43+
| "deepSeekApiKey"
4344
type GlobalStateKey =
4445
| "apiProvider"
4546
| "apiModelId"
@@ -443,6 +444,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
443444
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
444445
await this.storeSecret("geminiApiKey", geminiApiKey)
445446
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
447+
await this.storeSecret("deepSeekApiKey", message.apiConfiguration.deepSeekApiKey)
446448
await this.updateGlobalState("azureApiVersion", azureApiVersion)
447449
await this.updateGlobalState("openRouterModelId", openRouterModelId)
448450
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
@@ -1121,6 +1123,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
11211123
anthropicBaseUrl,
11221124
geminiApiKey,
11231125
openAiNativeApiKey,
1126+
deepSeekApiKey,
11241127
azureApiVersion,
11251128
openRouterModelId,
11261129
openRouterModelInfo,
@@ -1163,6 +1166,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
11631166
this.getGlobalState("anthropicBaseUrl") as Promise<string | undefined>,
11641167
this.getSecret("geminiApiKey") as Promise<string | undefined>,
11651168
this.getSecret("openAiNativeApiKey") as Promise<string | undefined>,
1169+
this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
11661170
this.getGlobalState("azureApiVersion") as Promise<string | undefined>,
11671171
this.getGlobalState("openRouterModelId") as Promise<string | undefined>,
11681172
this.getGlobalState("openRouterModelInfo") as Promise<ModelInfo | undefined>,
@@ -1222,6 +1226,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
12221226
anthropicBaseUrl,
12231227
geminiApiKey,
12241228
openAiNativeApiKey,
1229+
deepSeekApiKey,
12251230
azureApiVersion,
12261231
openRouterModelId,
12271232
openRouterModelInfo,
@@ -1344,6 +1349,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
13441349
"openAiApiKey",
13451350
"geminiApiKey",
13461351
"openAiNativeApiKey",
1352+
"deepSeekApiKey",
13471353
]
13481354
for (const key of secretKeys) {
13491355
await this.storeSecret(key, undefined)

src/shared/api.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type ApiProvider =
88
| "lmstudio"
99
| "gemini"
1010
| "openai-native"
11+
| "deepseek"
1112

1213
export interface ApiHandlerOptions {
1314
apiModelId?: string
@@ -38,6 +39,10 @@ export interface ApiHandlerOptions {
3839
openRouterUseMiddleOutTransform?: boolean
3940
includeStreamOptions?: boolean
4041
setAzureApiVersion?: boolean
42+
deepSeekBaseUrl?: string
43+
deepSeekApiKey?: string
44+
deepSeekModelId?: string
45+
includeMaxTokens?: boolean
4146
}
4247

4348
export type ApiConfiguration = ApiHandlerOptions & {
@@ -489,6 +494,22 @@ export const openAiNativeModels = {
489494
},
490495
} as const satisfies Record<string, ModelInfo>
491496

497+
// DeepSeek
498+
// https://platform.deepseek.com/docs/api
499+
export type DeepSeekModelId = keyof typeof deepSeekModels
500+
export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-chat"
501+
export const deepSeekModels = {
502+
"deepseek-chat": {
503+
maxTokens: 8192,
504+
contextWindow: 64_000,
505+
supportsImages: false,
506+
supportsPromptCache: false,
507+
inputPrice: 0.014, // $0.014 per million tokens
508+
outputPrice: 0.28, // $0.28 per million tokens
509+
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.`,
510+
},
511+
} as const satisfies Record<string, ModelInfo>
512+
492513
// Azure OpenAI
493514
// https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
494515
// https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#api-specs

0 commit comments

Comments
 (0)