From 359ae527c7e2408a88057b8be9aebd73cc1cadee Mon Sep 17 00:00:00 2001 From: shiso Date: Sat, 12 Jul 2025 15:01:24 +0800 Subject: [PATCH] feat: add DeepSeek API support - Add DeepSeek models: chat, coder, and reasoner (R1) - Implement DeepSeek provider client using OpenAI-compatible API - Add DEEPSEEK_API_KEY environment variable support - Update configuration defaults and priority order - Add DeepSeek to supported models in README - Update configuration schema with new models DeepSeek models offer high-performance AI at competitive pricing: - deepseek-chat: General conversation model - deepseek-coder: Code generation optimized model - deepseek-reasoner: Advanced reasoning model with CoT support Pricing starts at .14/1M input tokens, .28/1M output tokens. --- README.md | 11 ++ cmd/schema/main.go | 1 + internal/config/config.go | 42 +++++ internal/llm/models/deepseek.go | 56 +++++++ internal/llm/models/models.go | 10 +- internal/llm/provider/deepseek.go | 25 +++ internal/llm/provider/provider.go | 5 + opencode-schema.json | 260 ++++++++++++++++-------------- 8 files changed, 282 insertions(+), 128 deletions(-) create mode 100644 internal/llm/models/deepseek.go create mode 100644 internal/llm/provider/deepseek.go diff --git a/README.md b/README.md index eee06acd..f9eba6b2 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ You can configure OpenCode using environment variables: | `VERTEXAI_PROJECT` | For Google Cloud VertexAI (Gemini) | | `VERTEXAI_LOCATION` | For Google Cloud VertexAI (Gemini) | | `GROQ_API_KEY` | For Groq models | +| `DEEPSEEK_API_KEY` | For DeepSeek models | | `AWS_ACCESS_KEY_ID` | For AWS Bedrock (Claude) | | `AWS_SECRET_ACCESS_KEY` | For AWS Bedrock (Claude) | | `AWS_REGION` | For AWS Bedrock (Claude) | @@ -154,6 +155,10 @@ This is useful if you want to use a different shell than your default system she "apiKey": "your-api-key", "disabled": false }, + "deepseek": { + "apiKey": "your-api-key", + "disabled": false + }, "openrouter": { "apiKey": "your-api-key", "disabled": false @@ -256,6 +261,12 @@ OpenCode supports a variety of AI models from different providers: - Deepseek R1 distill Llama 70b - Llama 3.3 70b Versatile +### DeepSeek + +- DeepSeek Chat +- DeepSeek Coder +- DeepSeek Reasoner (R1) + ### Azure OpenAI - GPT-4.1 family (gpt-4.1, gpt-4.1-mini, gpt-4.1-nano) diff --git a/cmd/schema/main.go b/cmd/schema/main.go index 429267bc..5b5f7c43 100644 --- a/cmd/schema/main.go +++ b/cmd/schema/main.go @@ -196,6 +196,7 @@ func generateSchema() map[string]any { string(models.ProviderOpenAI), string(models.ProviderGemini), string(models.ProviderGROQ), + string(models.ProviderDeepSeek), string(models.ProviderOpenRouter), string(models.ProviderBedrock), string(models.ProviderAzure), diff --git a/internal/config/config.go b/internal/config/config.go index 630fac9b..590fa07a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -267,6 +267,9 @@ func setProviderDefaults() { if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" { viper.SetDefault("providers.groq.apiKey", apiKey) } + if apiKey := os.Getenv("DEEPSEEK_API_KEY"); apiKey != "" { + viper.SetDefault("providers.deepseek.apiKey", apiKey) + } if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" { viper.SetDefault("providers.openrouter.apiKey", apiKey) } @@ -340,6 +343,15 @@ func setProviderDefaults() { return } + // DeepSeek configuration + if key := viper.GetString("providers.deepseek.apiKey"); strings.TrimSpace(key) != "" { + viper.SetDefault("agents.coder.model", models.DeepSeekCoder) + viper.SetDefault("agents.summarizer.model", models.DeepSeekChat) + viper.SetDefault("agents.task.model", models.DeepSeekChat) + viper.SetDefault("agents.title.model", models.DeepSeekChat) + return + } + // OpenRouter configuration if key := viper.GetString("providers.openrouter.apiKey"); strings.TrimSpace(key) != "" { viper.SetDefault("agents.coder.model", models.OpenRouterClaude37Sonnet) @@ -651,6 +663,8 @@ func getProviderAPIKey(provider models.ModelProvider) string { return os.Getenv("GEMINI_API_KEY") case models.ProviderGROQ: return os.Getenv("GROQ_API_KEY") + case models.ProviderDeepSeek: + return os.Getenv("DEEPSEEK_API_KEY") case models.ProviderAzure: return os.Getenv("AZURE_OPENAI_API_KEY") case models.ProviderOpenRouter: @@ -781,6 +795,34 @@ func setDefaultModelForAgent(agent AgentName) bool { return true } + if apiKey := os.Getenv("DEEPSEEK_API_KEY"); apiKey != "" { + var model models.ModelID + maxTokens := int64(5000) + reasoningEffort := "" + + switch agent { + case AgentTitle: + model = models.DeepSeekChat + maxTokens = 80 + case AgentTask: + model = models.DeepSeekChat + default: + model = models.DeepSeekCoder + } + + // Check if model supports reasoning + if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason { + reasoningEffort = "medium" + } + + cfg.Agents[agent] = Agent{ + Model: model, + MaxTokens: maxTokens, + ReasoningEffort: reasoningEffort, + } + return true + } + if hasAWSCredentials() { maxTokens := int64(5000) if agent == AgentTitle { diff --git a/internal/llm/models/deepseek.go b/internal/llm/models/deepseek.go new file mode 100644 index 00000000..6a88b86f --- /dev/null +++ b/internal/llm/models/deepseek.go @@ -0,0 +1,56 @@ +package models + +const ( + ProviderDeepSeek ModelProvider = "deepseek" + + // DeepSeek Models + DeepSeekChat ModelID = "deepseek-chat" + DeepSeekCoder ModelID = "deepseek-coder" + DeepSeekReasoner ModelID = "deepseek-reasoner" +) + +// DeepSeek API official models +// https://platform.deepseek.com/api-docs/ +// Pricing as of 2025-07-01 +var DeepSeekModels = map[ModelID]Model{ + DeepSeekChat: { + ID: DeepSeekChat, + Name: "DeepSeek Chat", + Provider: ProviderDeepSeek, + APIModel: "deepseek-chat", + CostPer1MIn: 0.14, + CostPer1MInCached: 0.02, + CostPer1MOutCached: 0.0, + CostPer1MOut: 0.28, + ContextWindow: 128_000, // 官方上限 128k,推荐 ≤ 100k + DefaultMaxTokens: 8000, // 官方建议输出 ≤ 8k + SupportsAttachments: false, // DeepSeek 目前不支持文件上传或函数调用 + }, + DeepSeekCoder: { + ID: DeepSeekCoder, + Name: "DeepSeek Coder", + Provider: ProviderDeepSeek, + APIModel: "deepseek-coder", + CostPer1MIn: 0.14, + CostPer1MInCached: 0.02, + CostPer1MOutCached: 0.0, + CostPer1MOut: 0.28, + ContextWindow: 128_000, // 官方上限 128k,推荐 ≤ 100k + DefaultMaxTokens: 8000, // 官方建议输出 ≤ 8k + SupportsAttachments: false, // DeepSeek 目前不支持文件上传或函数调用 + }, + DeepSeekReasoner: { + ID: DeepSeekReasoner, + Name: "DeepSeek Reasoner (R1)", + Provider: ProviderDeepSeek, + APIModel: "deepseek-reasoner", + CostPer1MIn: 0.55, + CostPer1MInCached: 0.14, + CostPer1MOutCached: 0.0, + CostPer1MOut: 2.19, + ContextWindow: 65_536, // R1 模型上下文窗口 + DefaultMaxTokens: 16000, // R1 建议输出 ≤ 16k + CanReason: true, + SupportsAttachments: false, // DeepSeek 目前不支持文件上传或函数调用 + }, +} diff --git a/internal/llm/models/models.go b/internal/llm/models/models.go index 2bcb508e..d71e19f3 100644 --- a/internal/llm/models/models.go +++ b/internal/llm/models/models.go @@ -41,10 +41,11 @@ var ProviderPopularity = map[ModelProvider]int{ ProviderOpenAI: 3, ProviderGemini: 4, ProviderGROQ: 5, - ProviderOpenRouter: 6, - ProviderBedrock: 7, - ProviderAzure: 8, - ProviderVertexAI: 9, + ProviderDeepSeek: 6, + ProviderOpenRouter: 7, + ProviderBedrock: 8, + ProviderAzure: 9, + ProviderVertexAI: 10, } var SupportedModels = map[ModelID]Model{ @@ -95,4 +96,5 @@ func init() { maps.Copy(SupportedModels, XAIModels) maps.Copy(SupportedModels, VertexAIGeminiModels) maps.Copy(SupportedModels, CopilotModels) + maps.Copy(SupportedModels, DeepSeekModels) } diff --git a/internal/llm/provider/deepseek.go b/internal/llm/provider/deepseek.go new file mode 100644 index 00000000..8c1e8c21 --- /dev/null +++ b/internal/llm/provider/deepseek.go @@ -0,0 +1,25 @@ +package provider + +type deepseekClient struct { + *openaiClient +} + +type DeepSeekClient ProviderClient + +func newDeepSeekClient(opts providerClientOptions) DeepSeekClient { + // DeepSeek API 的基础 URL + baseURL := "https://api.deepseek.com" + + // 将基础 URL 添加到 OpenAI 客户端选项中 + opts.openaiOptions = append(opts.openaiOptions, + WithOpenAIBaseURL(baseURL), + ) + + // 创建并返回一个包装了 openaiClient 的 deepseekClient + return &deepseekClient{ + openaiClient: newOpenAIClient(opts).(*openaiClient), + } +} + +// DeepSeek 客户端实际上就是 OpenAI 客户端,只是指向不同的 API 端点 +// 所有方法都通过嵌入的 openaiClient 来处理 diff --git a/internal/llm/provider/provider.go b/internal/llm/provider/provider.go index d5be0ba0..74127fbc 100644 --- a/internal/llm/provider/provider.go +++ b/internal/llm/provider/provider.go @@ -122,6 +122,11 @@ func NewProvider(providerName models.ModelProvider, opts ...ProviderClientOption options: clientOptions, client: newOpenAIClient(clientOptions), }, nil + case models.ProviderDeepSeek: + return &baseProvider[DeepSeekClient]{ + options: clientOptions, + client: newDeepSeekClient(clientOptions), + }, nil case models.ProviderAzure: return &baseProvider[AzureClient]{ options: clientOptions, diff --git a/opencode-schema.json b/opencode-schema.json index 406c75f8..b78e56c7 100644 --- a/opencode-schema.json +++ b/opencode-schema.json @@ -12,83 +12,89 @@ "model": { "description": "Model ID for the agent", "enum": [ - "gpt-4.1", - "llama-3.3-70b-versatile", - "azure.gpt-4.1", - "openrouter.gpt-4o", - "openrouter.o1-mini", - "openrouter.claude-3-haiku", - "claude-3-opus", - "gpt-4o", + "o4-mini", + "openrouter.gemini-2.5", + "openrouter.claude-3.5-haiku", + "claude-3.5-sonnet", + "claude-3.7-sonnet", + "o3", "gpt-4o-mini", - "o1", + "azure.gpt-4o", + "copilot.o4-mini", + "o1-pro", "meta-llama/llama-4-maverick-17b-128e-instruct", - "azure.o3-mini", - "openrouter.gpt-4o-mini", - "openrouter.o1", + "bedrock.claude-3.7-sonnet", "claude-3.5-haiku", - "o4-mini", - "azure.gpt-4.1-mini", - "openrouter.o3", - "grok-3-beta", + "gemini-2.5-flash", + "openrouter.o3-mini", + "openrouter.claude-3-haiku", + "copilot.o3-mini", + "copilot.claude-3.5-sonnet", + "gemini-2.0-flash-lite", + "llama-3.3-70b-versatile", + "openrouter.gemini-2.5-flash", "o3-mini", + "azure.o3-mini", + "openrouter.gpt-4.1-nano", + "openrouter.o4-mini", + "gemini-2.5", + "meta-llama/llama-4-scout-17b-16e-instruct", + "deepseek-r1-distill-llama-70b", + "openrouter.claude-3-opus", + "openrouter.deepseek-r1-free", + "copilot.gemini-2.0-flash", "qwen-qwq", - "azure.o1", - "openrouter.gemini-2.5-flash", - "openrouter.gemini-2.5", + "azure.o3", + "copilot.gpt-4", "o1-mini", - "azure.gpt-4o", - "openrouter.gpt-4.1-mini", - "openrouter.claude-3.5-sonnet", - "openrouter.o3-mini", + "gpt-4.1", + "openrouter.gpt-4o-mini", + "copilot.gemini-2.5-pro", + "openrouter.o1-mini", + "openrouter.claude-3.7-sonnet", + "openrouter.gpt-4.1", "gpt-4.1-mini", - "gpt-4.5-preview", + "claude-3-opus", + "claude-4-sonnet", + "openrouter.gpt-4.1-mini", + "copilot.o1", + "claude-4-opus", + "o1", "gpt-4.1-nano", - "deepseek-r1-distill-llama-70b", + "azure.gpt-4.1-nano", "azure.gpt-4o-mini", - "openrouter.gpt-4.1", - "bedrock.claude-3.7-sonnet", - "claude-3-haiku", - "o3", - "gemini-2.0-flash-lite", - "azure.o3", + "grok-3-mini-beta", + "copilot.claude-sonnet-4", + "gpt-4.5-preview", + "gpt-4o", "azure.gpt-4.5-preview", - "openrouter.claude-3-opus", - "grok-3-mini-fast-beta", - "claude-4-sonnet", - "azure.o4-mini", - "grok-3-fast-beta", - "claude-3.5-sonnet", - "azure.o1-mini", - "openrouter.claude-3.7-sonnet", + "openrouter.o1-pro", "openrouter.gpt-4.5-preview", - "grok-3-mini-beta", - "claude-3.7-sonnet", + "grok-3-fast-beta", + "copilot.gpt-3.5-turbo", + "copilot.claude-3.7-sonnet", "gemini-2.0-flash", - "openrouter.deepseek-r1-free", + "openrouter.o1", + "openrouter.o3", + "openrouter.claude-3.5-sonnet", + "deepseek-reasoner", + "azure.o4-mini", + "azure.gpt-4.1", + "grok-3-mini-fast-beta", + "grok-3-beta", "vertexai.gemini-2.5-flash", + "deepseek-coder", + "azure.o1-mini", + "azure.gpt-4.1-mini", + "azure.o1", "vertexai.gemini-2.5", - "o1-pro", - "gemini-2.5", - "meta-llama/llama-4-scout-17b-16e-instruct", - "azure.gpt-4.1-nano", - "openrouter.gpt-4.1-nano", - "gemini-2.5-flash", - "openrouter.o4-mini", - "openrouter.claude-3.5-haiku", - "claude-4-opus", - "openrouter.o1-pro", + "copilot.gpt-4.1", + "deepseek-chat", + "claude-3-haiku", + "openrouter.gpt-4o", "copilot.gpt-4o", "copilot.gpt-4o-mini", - "copilot.gpt-4.1", - "copilot.claude-3.5-sonnet", - "copilot.claude-3.7-sonnet", - "copilot.claude-sonnet-4", - "copilot.o1", - "copilot.o3-mini", - "copilot.o4-mini", - "copilot.gemini-2.0-flash", - "copilot.gemini-2.5-pro" + "copilot.claude-3.7-sonnet-thought" ], "type": "string" }, @@ -122,83 +128,89 @@ "model": { "description": "Model ID for the agent", "enum": [ - "gpt-4.1", - "llama-3.3-70b-versatile", - "azure.gpt-4.1", - "openrouter.gpt-4o", - "openrouter.o1-mini", - "openrouter.claude-3-haiku", - "claude-3-opus", - "gpt-4o", + "o4-mini", + "openrouter.gemini-2.5", + "openrouter.claude-3.5-haiku", + "claude-3.5-sonnet", + "claude-3.7-sonnet", + "o3", "gpt-4o-mini", - "o1", + "azure.gpt-4o", + "copilot.o4-mini", + "o1-pro", "meta-llama/llama-4-maverick-17b-128e-instruct", - "azure.o3-mini", - "openrouter.gpt-4o-mini", - "openrouter.o1", + "bedrock.claude-3.7-sonnet", "claude-3.5-haiku", - "o4-mini", - "azure.gpt-4.1-mini", - "openrouter.o3", - "grok-3-beta", + "gemini-2.5-flash", + "openrouter.o3-mini", + "openrouter.claude-3-haiku", + "copilot.o3-mini", + "copilot.claude-3.5-sonnet", + "gemini-2.0-flash-lite", + "llama-3.3-70b-versatile", + "openrouter.gemini-2.5-flash", "o3-mini", + "azure.o3-mini", + "openrouter.gpt-4.1-nano", + "openrouter.o4-mini", + "gemini-2.5", + "meta-llama/llama-4-scout-17b-16e-instruct", + "deepseek-r1-distill-llama-70b", + "openrouter.claude-3-opus", + "openrouter.deepseek-r1-free", + "copilot.gemini-2.0-flash", "qwen-qwq", - "azure.o1", - "openrouter.gemini-2.5-flash", - "openrouter.gemini-2.5", + "azure.o3", + "copilot.gpt-4", "o1-mini", - "azure.gpt-4o", - "openrouter.gpt-4.1-mini", - "openrouter.claude-3.5-sonnet", - "openrouter.o3-mini", + "gpt-4.1", + "openrouter.gpt-4o-mini", + "copilot.gemini-2.5-pro", + "openrouter.o1-mini", + "openrouter.claude-3.7-sonnet", + "openrouter.gpt-4.1", "gpt-4.1-mini", - "gpt-4.5-preview", + "claude-3-opus", + "claude-4-sonnet", + "openrouter.gpt-4.1-mini", + "copilot.o1", + "claude-4-opus", + "o1", "gpt-4.1-nano", - "deepseek-r1-distill-llama-70b", + "azure.gpt-4.1-nano", "azure.gpt-4o-mini", - "openrouter.gpt-4.1", - "bedrock.claude-3.7-sonnet", - "claude-3-haiku", - "o3", - "gemini-2.0-flash-lite", - "azure.o3", + "grok-3-mini-beta", + "copilot.claude-sonnet-4", + "gpt-4.5-preview", + "gpt-4o", "azure.gpt-4.5-preview", - "openrouter.claude-3-opus", - "grok-3-mini-fast-beta", - "claude-4-sonnet", - "azure.o4-mini", - "grok-3-fast-beta", - "claude-3.5-sonnet", - "azure.o1-mini", - "openrouter.claude-3.7-sonnet", + "openrouter.o1-pro", "openrouter.gpt-4.5-preview", - "grok-3-mini-beta", - "claude-3.7-sonnet", + "grok-3-fast-beta", + "copilot.gpt-3.5-turbo", + "copilot.claude-3.7-sonnet", "gemini-2.0-flash", - "openrouter.deepseek-r1-free", + "openrouter.o1", + "openrouter.o3", + "openrouter.claude-3.5-sonnet", + "deepseek-reasoner", + "azure.o4-mini", + "azure.gpt-4.1", + "grok-3-mini-fast-beta", + "grok-3-beta", "vertexai.gemini-2.5-flash", + "deepseek-coder", + "azure.o1-mini", + "azure.gpt-4.1-mini", + "azure.o1", "vertexai.gemini-2.5", - "o1-pro", - "gemini-2.5", - "meta-llama/llama-4-scout-17b-16e-instruct", - "azure.gpt-4.1-nano", - "openrouter.gpt-4.1-nano", - "gemini-2.5-flash", - "openrouter.o4-mini", - "openrouter.claude-3.5-haiku", - "claude-4-opus", - "openrouter.o1-pro", + "copilot.gpt-4.1", + "deepseek-chat", + "claude-3-haiku", + "openrouter.gpt-4o", "copilot.gpt-4o", "copilot.gpt-4o-mini", - "copilot.gpt-4.1", - "copilot.claude-3.5-sonnet", - "copilot.claude-3.7-sonnet", - "copilot.claude-sonnet-4", - "copilot.o1", - "copilot.o3-mini", - "copilot.o4-mini", - "copilot.gemini-2.0-flash", - "copilot.gemini-2.5-pro" + "copilot.claude-3.7-sonnet-thought" ], "type": "string" }, @@ -379,11 +391,11 @@ "openai", "gemini", "groq", + "deepseek", "openrouter", "bedrock", "azure", - "vertexai", - "copilot" + "vertexai" ], "type": "string" }