diff --git a/src/api/index.ts b/src/api/index.ts index ec35c2a2af..287f843642 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -10,6 +10,7 @@ import { LmStudioHandler } from "./providers/lmstudio" import { GeminiHandler } from "./providers/gemini" import { OpenAiNativeHandler } from "./providers/openai-native" import { ApiStream } from "./transform/stream" +import { DeepSeekHandler } from "./providers/deepseek" export interface ApiHandler { createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream @@ -37,6 +38,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler { return new GeminiHandler(options) case "openai-native": return new OpenAiNativeHandler(options) + case "deepseek": + return new DeepSeekHandler(options) default: return new AnthropicHandler(options) } diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts new file mode 100644 index 0000000000..48afab68ba --- /dev/null +++ b/src/api/providers/deepseek.ts @@ -0,0 +1,61 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" +import { ApiHandler } from "../" +import { ApiHandlerOptions, DeepSeekModelId, ModelInfo, deepSeekDefaultModelId, deepSeekModels } from "../../shared/api" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { ApiStream } from "../transform/stream" + +export class DeepSeekHandler implements ApiHandler { + private options: ApiHandlerOptions + private client: OpenAI + + constructor(options: ApiHandlerOptions) { + this.options = options + this.client = new OpenAI({ + baseURL: "https://api.deepseek.com/v1", + apiKey: this.options.deepSeekApiKey, + }) + } + + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const stream = await this.client.chat.completions.create({ + model: this.getModel().id, + max_completion_tokens: this.getModel().info.maxTokens, + temperature: 0, + messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], + stream: true, + stream_options: { include_usage: true }, + }) + + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta + if (delta?.content) { + yield { + type: "text", + text: delta.content, + } + } + + if (chunk.usage) { + yield { + type: "usage", + inputTokens: 0, //chunk.usage.prompt_tokens || 0, (deepseek reports total input AND cache reads/writes, see context caching: https://api-docs.deepseek.com/guides/kv_cache) + outputTokens: chunk.usage.completion_tokens || 0, + // @ts-ignore-next-line + cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0, + // @ts-ignore-next-line + cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0, + } + } + } + } + + getModel(): { id: DeepSeekModelId; info: ModelInfo } { + const modelId = this.options.apiModelId + if (modelId && modelId in deepSeekModels) { + const id = modelId as DeepSeekModelId + return { id, info: deepSeekModels[id] } + } + return { id: deepSeekDefaultModelId, info: deepSeekModels[deepSeekDefaultModelId] } + } +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 8a556f9369..78cc71b460 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -39,6 +39,7 @@ type SecretKey = | "openAiApiKey" | "geminiApiKey" | "openAiNativeApiKey" + | "deepSeekApiKey" type GlobalStateKey = | "apiProvider" | "apiModelId" @@ -378,6 +379,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { anthropicBaseUrl, geminiApiKey, openAiNativeApiKey, + deepSeekApiKey, azureApiVersion, openRouterModelId, openRouterModelInfo, @@ -403,6 +405,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl) await this.storeSecret("geminiApiKey", geminiApiKey) await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey) + await this.storeSecret("deepSeekApiKey", deepSeekApiKey) await this.updateGlobalState("azureApiVersion", azureApiVersion) await this.updateGlobalState("openRouterModelId", openRouterModelId) await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo) @@ -916,6 +919,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { anthropicBaseUrl, geminiApiKey, openAiNativeApiKey, + deepSeekApiKey, azureApiVersion, openRouterModelId, openRouterModelInfo, @@ -945,6 +949,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("anthropicBaseUrl") as Promise, this.getSecret("geminiApiKey") as Promise, this.getSecret("openAiNativeApiKey") as Promise, + this.getSecret("deepSeekApiKey") as Promise, this.getGlobalState("azureApiVersion") as Promise, this.getGlobalState("openRouterModelId") as Promise, this.getGlobalState("openRouterModelInfo") as Promise, @@ -991,6 +996,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { anthropicBaseUrl, geminiApiKey, openAiNativeApiKey, + deepSeekApiKey, azureApiVersion, openRouterModelId, openRouterModelInfo, @@ -1074,6 +1080,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { "openAiApiKey", "geminiApiKey", "openAiNativeApiKey", + "deepSeekApiKey", ] for (const key of secretKeys) { await this.storeSecret(key, undefined) diff --git a/src/shared/api.ts b/src/shared/api.ts index 868e9f06e9..578b1083e1 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -8,6 +8,7 @@ export type ApiProvider = | "lmstudio" | "gemini" | "openai-native" + | "deepseek" export interface ApiHandlerOptions { apiModelId?: string @@ -32,6 +33,7 @@ export interface ApiHandlerOptions { lmStudioBaseUrl?: string geminiApiKey?: string openAiNativeApiKey?: string + deepSeekApiKey?: string azureApiVersion?: string } @@ -347,3 +349,20 @@ export const openAiNativeModels = { // 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 export const azureOpenAiDefaultApiVersion = "2024-08-01-preview" + +// DeepSeek +// https://api-docs.deepseek.com/quick_start/pricing +export type DeepSeekModelId = keyof typeof deepSeekModels +export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-chat" +export const deepSeekModels = { + "deepseek-chat": { + maxTokens: 8_000, + contextWindow: 64_000, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.14, + outputPrice: 0.28, + cacheWritesPrice: 0.14, + cacheReadsPrice: 0.014, + }, +} as const satisfies Record diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index e77c83b50b..4419430336 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -17,6 +17,8 @@ import { azureOpenAiDefaultApiVersion, bedrockDefaultModelId, bedrockModels, + deepSeekDefaultModelId, + deepSeekModels, geminiDefaultModelId, geminiModels, openAiModelInfoSaneDefaults, @@ -129,6 +131,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }: OpenRouter Anthropic Google Gemini + DeepSeek GCP Vertex AI AWS Bedrock OpenAI @@ -217,6 +220,34 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }: )} + {selectedProvider === "deepseek" && ( +
+ + DeepSeek API Key + +

+ This key is stored locally and only used to make API requests from this extension. + {!apiConfiguration?.deepSeekApiKey && ( + + You can get a DeepSeek API key by signing up here. + + )} +

+
+ )} + {selectedProvider === "openrouter" && (
key !== undefined) : false setShowWelcome(!hasKey) diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 437e1267f3..98920aba4f 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -33,6 +33,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s return "You must provide a valid API key or choose a different provider." } break + case "deepseek": + if (!apiConfiguration.deepSeekApiKey) { + return "You must provide a valid API key or choose a different provider." + } + break case "openai": if ( !apiConfiguration.openAiBaseUrl ||