Merge pull request #239 from RooVetGit/deepseek

Add DeepSeek to the list of providers
This commit is contained in:
Matt Rubens
2024-12-29 12:26:59 -08:00
committed by GitHub
9 changed files with 270 additions and 3 deletions

View File

@@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Add the DeepSeek provider

View File

@@ -13,6 +13,7 @@ A fork of Cline, an autonomous coding agent, with some additional experimental f
- Includes current time in the system prompt - Includes current time in the system prompt
- Uses a file system watcher to more reliably watch for file system changes - Uses a file system watcher to more reliably watch for file system changes
- Language selection for Cline's communication (English, Japanese, Spanish, French, German, and more) - Language selection for Cline's communication (English, Japanese, Spanish, French, German, and more)
- Support for DeepSeek V3
- Support for Meta 3, 3.1, and 3.2 models via AWS Bedrock - Support for Meta 3, 3.1, and 3.2 models via AWS Bedrock
- Support for listing models from OpenAI-compatible providers - Support for listing models from OpenAI-compatible providers
- Per-tool MCP auto-approval - Per-tool MCP auto-approval

View File

@@ -9,6 +9,7 @@ import { OllamaHandler } from "./providers/ollama"
import { LmStudioHandler } from "./providers/lmstudio" import { LmStudioHandler } from "./providers/lmstudio"
import { GeminiHandler } from "./providers/gemini" import { GeminiHandler } from "./providers/gemini"
import { OpenAiNativeHandler } from "./providers/openai-native" import { OpenAiNativeHandler } from "./providers/openai-native"
import { DeepSeekHandler } from "./providers/deepseek"
import { ApiStream } from "./transform/stream" import { ApiStream } from "./transform/stream"
export interface SingleCompletionHandler { export interface SingleCompletionHandler {
@@ -41,6 +42,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new GeminiHandler(options) return new GeminiHandler(options)
case "openai-native": case "openai-native":
return new OpenAiNativeHandler(options) return new OpenAiNativeHandler(options)
case "deepseek":
return new DeepSeekHandler(options)
default: default:
return new AnthropicHandler(options) return new AnthropicHandler(options)
} }

View File

@@ -0,0 +1,167 @@
import { DeepSeekHandler } from '../deepseek'
import { ApiHandlerOptions } from '../../../shared/api'
import OpenAI from 'openai'
import { Anthropic } from '@anthropic-ai/sdk'
// Mock dependencies
jest.mock('openai')
describe('DeepSeekHandler', () => {
const mockOptions: ApiHandlerOptions = {
deepSeekApiKey: 'test-key',
deepSeekModelId: 'deepseek-chat',
}
beforeEach(() => {
jest.clearAllMocks()
})
test('constructor initializes with correct options', () => {
const handler = new DeepSeekHandler(mockOptions)
expect(handler).toBeInstanceOf(DeepSeekHandler)
expect(OpenAI).toHaveBeenCalledWith({
baseURL: 'https://api.deepseek.com/v1',
apiKey: mockOptions.deepSeekApiKey,
})
})
test('getModel returns correct model info', () => {
const handler = new DeepSeekHandler(mockOptions)
const result = handler.getModel()
expect(result).toEqual({
id: mockOptions.deepSeekModelId,
info: expect.objectContaining({
maxTokens: 8192,
contextWindow: 64000,
supportsPromptCache: false,
supportsImages: false,
inputPrice: 0.014,
outputPrice: 0.28,
})
})
})
test('getModel returns default model info when no model specified', () => {
const handler = new DeepSeekHandler({ deepSeekApiKey: 'test-key' })
const result = handler.getModel()
expect(result.id).toBe('deepseek-chat')
expect(result.info.maxTokens).toBe(8192)
})
test('createMessage handles string content correctly', async () => {
const handler = new DeepSeekHandler(mockOptions)
const mockStream = {
async *[Symbol.asyncIterator]() {
yield {
choices: [{
delta: {
content: 'test response'
}
}]
}
}
}
const mockCreate = jest.fn().mockResolvedValue(mockStream)
;(OpenAI as jest.MockedClass<typeof OpenAI>).prototype.chat = {
completions: { create: mockCreate }
} as any
const systemPrompt = 'test system prompt'
const messages: Anthropic.Messages.MessageParam[] = [
{ role: 'user', content: 'test message' }
]
const generator = handler.createMessage(systemPrompt, messages)
const chunks = []
for await (const chunk of generator) {
chunks.push(chunk)
}
expect(chunks).toHaveLength(1)
expect(chunks[0]).toEqual({
type: 'text',
text: 'test response'
})
expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({
model: mockOptions.deepSeekModelId,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: 'test message' }
],
temperature: 0,
stream: true,
max_tokens: 8192,
stream_options: { include_usage: true }
}))
})
test('createMessage handles complex content correctly', async () => {
const handler = new DeepSeekHandler(mockOptions)
const mockStream = {
async *[Symbol.asyncIterator]() {
yield {
choices: [{
delta: {
content: 'test response'
}
}]
}
}
}
const mockCreate = jest.fn().mockResolvedValue(mockStream)
;(OpenAI as jest.MockedClass<typeof OpenAI>).prototype.chat = {
completions: { create: mockCreate }
} as any
const systemPrompt = 'test system prompt'
const messages: Anthropic.Messages.MessageParam[] = [
{
role: 'user',
content: [
{ type: 'text', text: 'part 1' },
{ type: 'text', text: 'part 2' }
]
}
]
const generator = handler.createMessage(systemPrompt, messages)
await generator.next()
expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({
messages: [
{ role: 'system', content: systemPrompt },
{
role: 'user',
content: [
{ type: 'text', text: 'part 1' },
{ type: 'text', text: 'part 2' }
]
}
]
}))
})
test('createMessage handles API errors', async () => {
const handler = new DeepSeekHandler(mockOptions)
const mockStream = {
async *[Symbol.asyncIterator]() {
throw new Error('API Error')
}
}
const mockCreate = jest.fn().mockResolvedValue(mockStream)
;(OpenAI as jest.MockedClass<typeof OpenAI>).prototype.chat = {
completions: { create: mockCreate }
} as any
const generator = handler.createMessage('test', [])
await expect(generator.next()).rejects.toThrow('API Error')
})
})

View File

@@ -0,0 +1,26 @@
import { OpenAiHandler } from "./openai"
import { ApiHandlerOptions, ModelInfo } from "../../shared/api"
import { deepSeekModels, deepSeekDefaultModelId } from "../../shared/api"
export class DeepSeekHandler extends OpenAiHandler {
constructor(options: ApiHandlerOptions) {
if (!options.deepSeekApiKey) {
throw new Error("DeepSeek API key is required. Please provide it in the settings.")
}
super({
...options,
openAiApiKey: options.deepSeekApiKey,
openAiModelId: options.deepSeekModelId ?? deepSeekDefaultModelId,
openAiBaseUrl: options.deepSeekBaseUrl ?? "https://api.deepseek.com/v1",
includeMaxTokens: true
})
}
override getModel(): { id: string; info: ModelInfo } {
const modelId = this.options.deepSeekModelId ?? deepSeekDefaultModelId
return {
id: modelId,
info: deepSeekModels[modelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId]
}
}
}

View File

@@ -11,7 +11,7 @@ import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream" import { ApiStream } from "../transform/stream"
export class OpenAiHandler implements ApiHandler { export class OpenAiHandler implements ApiHandler {
private options: ApiHandlerOptions protected options: ApiHandlerOptions
private client: OpenAI private client: OpenAI
constructor(options: ApiHandlerOptions) { constructor(options: ApiHandlerOptions) {
@@ -38,12 +38,16 @@ export class OpenAiHandler implements ApiHandler {
{ role: "system", content: systemPrompt }, { role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages), ...convertToOpenAiMessages(messages),
] ]
const modelInfo = this.getModel().info
const requestOptions: OpenAI.Chat.ChatCompletionCreateParams = { const requestOptions: OpenAI.Chat.ChatCompletionCreateParams = {
model: this.options.openAiModelId ?? "", model: this.options.openAiModelId ?? "",
messages: openAiMessages, messages: openAiMessages,
temperature: 0, temperature: 0,
stream: true, stream: true,
} }
if (this.options.includeMaxTokens) {
requestOptions.max_tokens = modelInfo.maxTokens
}
if (this.options.includeStreamOptions ?? true) { if (this.options.includeStreamOptions ?? true) {
requestOptions.stream_options = { include_usage: true } requestOptions.stream_options = { include_usage: true }

View File

@@ -40,6 +40,7 @@ type SecretKey =
| "openAiApiKey" | "openAiApiKey"
| "geminiApiKey" | "geminiApiKey"
| "openAiNativeApiKey" | "openAiNativeApiKey"
| "deepSeekApiKey"
type GlobalStateKey = type GlobalStateKey =
| "apiProvider" | "apiProvider"
| "apiModelId" | "apiModelId"
@@ -443,6 +444,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl) await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
await this.storeSecret("geminiApiKey", geminiApiKey) await this.storeSecret("geminiApiKey", geminiApiKey)
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey) await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
await this.storeSecret("deepSeekApiKey", message.apiConfiguration.deepSeekApiKey)
await this.updateGlobalState("azureApiVersion", azureApiVersion) await this.updateGlobalState("azureApiVersion", azureApiVersion)
await this.updateGlobalState("openRouterModelId", openRouterModelId) await this.updateGlobalState("openRouterModelId", openRouterModelId)
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo) await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
@@ -1121,6 +1123,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
anthropicBaseUrl, anthropicBaseUrl,
geminiApiKey, geminiApiKey,
openAiNativeApiKey, openAiNativeApiKey,
deepSeekApiKey,
azureApiVersion, azureApiVersion,
openRouterModelId, openRouterModelId,
openRouterModelInfo, openRouterModelInfo,
@@ -1163,6 +1166,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("anthropicBaseUrl") as Promise<string | undefined>, this.getGlobalState("anthropicBaseUrl") as Promise<string | undefined>,
this.getSecret("geminiApiKey") as Promise<string | undefined>, this.getSecret("geminiApiKey") as Promise<string | undefined>,
this.getSecret("openAiNativeApiKey") as Promise<string | undefined>, this.getSecret("openAiNativeApiKey") as Promise<string | undefined>,
this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
this.getGlobalState("azureApiVersion") as Promise<string | undefined>, this.getGlobalState("azureApiVersion") as Promise<string | undefined>,
this.getGlobalState("openRouterModelId") as Promise<string | undefined>, this.getGlobalState("openRouterModelId") as Promise<string | undefined>,
this.getGlobalState("openRouterModelInfo") as Promise<ModelInfo | undefined>, this.getGlobalState("openRouterModelInfo") as Promise<ModelInfo | undefined>,
@@ -1222,6 +1226,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
anthropicBaseUrl, anthropicBaseUrl,
geminiApiKey, geminiApiKey,
openAiNativeApiKey, openAiNativeApiKey,
deepSeekApiKey,
azureApiVersion, azureApiVersion,
openRouterModelId, openRouterModelId,
openRouterModelInfo, openRouterModelInfo,
@@ -1344,6 +1349,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
"openAiApiKey", "openAiApiKey",
"geminiApiKey", "geminiApiKey",
"openAiNativeApiKey", "openAiNativeApiKey",
"deepSeekApiKey",
] ]
for (const key of secretKeys) { for (const key of secretKeys) {
await this.storeSecret(key, undefined) await this.storeSecret(key, undefined)

View File

@@ -8,6 +8,7 @@ export type ApiProvider =
| "lmstudio" | "lmstudio"
| "gemini" | "gemini"
| "openai-native" | "openai-native"
| "deepseek"
export interface ApiHandlerOptions { export interface ApiHandlerOptions {
apiModelId?: string apiModelId?: string
@@ -38,6 +39,10 @@ export interface ApiHandlerOptions {
openRouterUseMiddleOutTransform?: boolean openRouterUseMiddleOutTransform?: boolean
includeStreamOptions?: boolean includeStreamOptions?: boolean
setAzureApiVersion?: boolean setAzureApiVersion?: boolean
deepSeekBaseUrl?: string
deepSeekApiKey?: string
deepSeekModelId?: string
includeMaxTokens?: boolean
} }
export type ApiConfiguration = ApiHandlerOptions & { export type ApiConfiguration = ApiHandlerOptions & {
@@ -489,6 +494,22 @@ export const openAiNativeModels = {
}, },
} as const satisfies Record<string, ModelInfo> } as const satisfies Record<string, ModelInfo>
// DeepSeek
// https://platform.deepseek.com/docs/api
export type DeepSeekModelId = keyof typeof deepSeekModels
export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-chat"
export const deepSeekModels = {
"deepseek-chat": {
maxTokens: 8192,
contextWindow: 64_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.014, // $0.014 per million tokens
outputPrice: 0.28, // $0.28 per million tokens
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.`,
},
} as const satisfies Record<string, ModelInfo>
// Azure OpenAI // Azure OpenAI
// https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation // https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
// https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#api-specs // https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#api-specs

View File

@@ -17,6 +17,8 @@ import {
azureOpenAiDefaultApiVersion, azureOpenAiDefaultApiVersion,
bedrockDefaultModelId, bedrockDefaultModelId,
bedrockModels, bedrockModels,
deepSeekDefaultModelId,
deepSeekModels,
geminiDefaultModelId, geminiDefaultModelId,
geminiModels, geminiModels,
openAiModelInfoSaneDefaults, openAiModelInfoSaneDefaults,
@@ -130,10 +132,11 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption> <VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
<VSCodeOption value="anthropic">Anthropic</VSCodeOption> <VSCodeOption value="anthropic">Anthropic</VSCodeOption>
<VSCodeOption value="gemini">Google Gemini</VSCodeOption> <VSCodeOption value="gemini">Google Gemini</VSCodeOption>
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption> <VSCodeOption value="deepseek">DeepSeek</VSCodeOption>
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
<VSCodeOption value="openai-native">OpenAI</VSCodeOption> <VSCodeOption value="openai-native">OpenAI</VSCodeOption>
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption> <VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
<VSCodeOption value="lmstudio">LM Studio</VSCodeOption> <VSCodeOption value="lmstudio">LM Studio</VSCodeOption>
<VSCodeOption value="ollama">Ollama</VSCodeOption> <VSCodeOption value="ollama">Ollama</VSCodeOption>
</VSCodeDropdown> </VSCodeDropdown>
@@ -560,6 +563,34 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
</div> </div>
)} )}
{selectedProvider === "deepseek" && (
<div>
<VSCodeTextField
value={apiConfiguration?.deepSeekApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("deepSeekApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>DeepSeek API Key</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
This key is stored locally and only used to make API requests from this extension.
{!apiConfiguration?.deepSeekApiKey && (
<VSCodeLink
href="https://platform.deepseek.com/"
style={{ display: "inline", fontSize: "inherit" }}>
You can get a DeepSeek API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)}
{selectedProvider === "ollama" && ( {selectedProvider === "ollama" && (
<div> <div>
<VSCodeTextField <VSCodeTextField
@@ -652,6 +683,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
{selectedProvider === "vertex" && createDropdown(vertexModels)} {selectedProvider === "vertex" && createDropdown(vertexModels)}
{selectedProvider === "gemini" && createDropdown(geminiModels)} {selectedProvider === "gemini" && createDropdown(geminiModels)}
{selectedProvider === "openai-native" && createDropdown(openAiNativeModels)} {selectedProvider === "openai-native" && createDropdown(openAiNativeModels)}
{selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
</div> </div>
<ModelInfoView <ModelInfoView
@@ -836,6 +868,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
return getProviderData(vertexModels, vertexDefaultModelId) return getProviderData(vertexModels, vertexDefaultModelId)
case "gemini": case "gemini":
return getProviderData(geminiModels, geminiDefaultModelId) return getProviderData(geminiModels, geminiDefaultModelId)
case "deepseek":
return getProviderData(deepSeekModels, deepSeekDefaultModelId)
case "openai-native": case "openai-native":
return getProviderData(openAiNativeModels, openAiNativeDefaultModelId) return getProviderData(openAiNativeModels, openAiNativeDefaultModelId)
case "openrouter": case "openrouter":