From b0d70b7b1493934eddb4df1d28936dbe7d15ba73 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 29 Jan 2026 17:53:20 +0800 Subject: [PATCH] feat: add official DeepSeek API support - Add deepseek-chat (V3.2 non-thinking mode) model - Add deepseek-reasoner (V3.2 thinking mode) model - Support both models with 128K context window - Include accurate pricing information ($0.28/$0.42 per 1M tokens) - Compatible with OpenAI API format - Integrate with onboarding wizard for easy setup - Add API key configuration support Implements official DeepSeek API provider as alternative to Synthetic/Venice. Users can now use their own DeepSeek API keys directly. --- src/agents/models-config.providers.ts | 50 ++++++++++++ src/commands/auth-choice-options.ts | 12 +++ .../auth-choice.apply.api-providers.ts | 53 +++++++++++++ src/commands/onboard-auth.config-core.ts | 77 +++++++++++++++++++ src/commands/onboard-auth.credentials.ts | 13 ++++ src/commands/onboard-auth.models.ts | 39 ++++++++++ src/commands/onboard-auth.ts | 9 +++ src/commands/onboard-types.ts | 2 + 8 files changed, 255 insertions(+) diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index f38ad46c7..04b5af497 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -86,6 +86,21 @@ const OLLAMA_DEFAULT_COST = { cacheWrite: 0, }; +const DEEPSEEK_API_BASE_URL = "https://api.deepseek.com"; +const DEEPSEEK_CHAT_MODEL_ID = "deepseek-chat"; +const DEEPSEEK_REASONER_MODEL_ID = "deepseek-reasoner"; +const DEEPSEEK_DEFAULT_CONTEXT_WINDOW = 128000; +const DEEPSEEK_CHAT_MAX_TOKENS = 8192; +const DEEPSEEK_REASONER_MAX_TOKENS = 64000; +// DeepSeek pricing (per 1M tokens ) - Updated 2026-01-29 +// https://api-docs.deepseek.com/quick_start/pricing +const DEEPSEEK_API_COST = { + input: 0.28, // Input (Cache Miss ): $0.28 per 1M tokens + output: 0.42, // Output: $0.42 per 1M tokens + cacheRead: 0.028, // Input (Cache Hit): $0.028 per 1M tokens + cacheWrite: 0.28, // Treat same as regular input for write +}; + interface OllamaModel { name: string; modified_at: string; @@ -388,6 +403,33 @@ async function buildOllamaProvider(): Promise { }; } +function buildDeepSeekProvider(): ProviderConfig { + return { + baseUrl: DEEPSEEK_API_BASE_URL, + api: "openai-completions", + models: [ + { + id: DEEPSEEK_CHAT_MODEL_ID, + name: "DeepSeek Chat (V3.2)", + reasoning: false, + input: ["text"], + cost: DEEPSEEK_API_COST, + contextWindow: DEEPSEEK_DEFAULT_CONTEXT_WINDOW, + maxTokens: DEEPSEEK_CHAT_MAX_TOKENS, + }, + { + id: DEEPSEEK_REASONER_MODEL_ID, + name: "DeepSeek Reasoner (V3.2)", + reasoning: true, + input: ["text"], + cost: DEEPSEEK_API_COST, + contextWindow: DEEPSEEK_DEFAULT_CONTEXT_WINDOW, + maxTokens: DEEPSEEK_REASONER_MAX_TOKENS, + }, + ], + }; +} + export async function resolveImplicitProviders(params: { agentDir: string; }): Promise { @@ -454,6 +496,14 @@ export async function resolveImplicitProviders(params: { providers.ollama = { ...(await buildOllamaProvider()), apiKey: ollamaKey }; } + // DeepSeek provider + const deepseekKey = + resolveEnvApiKeyVarName("deepseek") ?? + resolveApiKeyFromProfiles({ provider: "deepseek", store: authStore }); + if (deepseekKey) { + providers.deepseek = { ...buildDeepSeekProvider(), apiKey: deepseekKey }; + } + return providers; } diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 5acddf4e3..49f9a159e 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -21,6 +21,7 @@ export type AuthChoiceGroupId = | "minimax" | "synthetic" | "venice" + | "deepseek" | "qwen"; export type AuthChoiceGroup = { @@ -60,6 +61,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "OAuth", choices: ["qwen-portal"], }, + { + value: "deepseek", + label: "DeepSeek", + hint: "V3.2 (chat + reasoning)", + choices: ["deepseek-api-key"], + }, { value: "synthetic", label: "Synthetic", @@ -194,6 +201,11 @@ export function buildAuthChoiceOptions(params: { label: "MiniMax M2.1 Lightning", hint: "Faster, higher output cost", }); + options.push({ + value: "deepseek-api-key", + label: "DeepSeek API key", + hint: "V3.2 chat and reasoning models", + }); if (params.includeSkip) { options.push({ value: "skip", label: "Skip for now" }); } diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index fa4fc77e7..48921bcf3 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -13,6 +13,8 @@ import { } from "./google-gemini-model-default.js"; import { applyAuthProfileConfig, + applyDeepSeekConfig, + applyDeepSeekProviderConfig, applyKimiCodeConfig, applyKimiCodeProviderConfig, applyMoonshotConfig, @@ -30,6 +32,7 @@ import { applyXiaomiConfig, applyXiaomiProviderConfig, applyZaiConfig, + DEEPSEEK_DEFAULT_MODEL_REF, KIMI_CODE_MODEL_REF, MOONSHOT_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, @@ -37,6 +40,7 @@ import { VENICE_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, XIAOMI_DEFAULT_MODEL_REF, + setDeepSeekApiKey, setGeminiApiKey, setKimiCodeApiKey, setMoonshotApiKey, @@ -89,6 +93,8 @@ export async function applyAuthChoiceApiProviders( authChoice = "synthetic-api-key"; } else if (params.opts.tokenProvider === "venice") { authChoice = "venice-api-key"; + } else if (params.opts.tokenProvider === "deepseek") { + authChoice = "deepseek-api-key"; } else if (params.opts.tokenProvider === "opencode") { authChoice = "opencode-zen"; } @@ -633,5 +639,52 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "deepseek-api-key") { + let hasCredential = false; + + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "deepseek") { + await setDeepSeekApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + const envKey = resolveEnvApiKey("deepseek"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing DEEPSEEK_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setDeepSeekApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter DeepSeek API key", + validate: validateApiKeyInput, + }); + await setDeepSeekApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "deepseek:default", + provider: "deepseek", + mode: "api_key", + }); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: DEEPSEEK_DEFAULT_MODEL_REF, + applyDefaultConfig: applyDeepSeekConfig, + applyProviderConfig: applyDeepSeekProviderConfig, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + return { config: nextConfig, agentModelOverride }; + } + return null; } diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 222f0a5c6..75493d536 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -19,8 +19,13 @@ import { ZAI_DEFAULT_MODEL_REF, } from "./onboard-auth.credentials.js"; import { + buildDeepSeekModelDefinition, buildKimiCodeModelDefinition, buildMoonshotModelDefinition, + DEEPSEEK_BASE_URL, + DEEPSEEK_CHAT_MODEL_ID, + DEEPSEEK_DEFAULT_MODEL_REF, + DEEPSEEK_REASONER_MODEL_ID, KIMI_CODE_BASE_URL, KIMI_CODE_MODEL_ID, KIMI_CODE_MODEL_REF, @@ -532,3 +537,75 @@ export function applyAuthProfileConfig( }, }; } + +export function applyDeepSeekProviderConfig(cfg: MoltbotConfig): MoltbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[DEEPSEEK_DEFAULT_MODEL_REF] = { + ...models[DEEPSEEK_DEFAULT_MODEL_REF], + alias: models[DEEPSEEK_DEFAULT_MODEL_REF]?.alias ?? "DeepSeek", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.deepseek; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const deepseekModels = [ + buildDeepSeekModelDefinition(DEEPSEEK_CHAT_MODEL_ID), + buildDeepSeekModelDefinition(DEEPSEEK_REASONER_MODEL_ID), + ]; + const mergedModels = [ + ...existingModels, + ...deepseekModels.filter( + (model) => !existingModels.some((existing) => existing.id === model.id), + ), + ]; + const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< + string, + unknown + > as { apiKey?: string }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + providers.deepseek = { + ...existingProviderRest, + baseUrl: DEEPSEEK_BASE_URL, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: mergedModels.length > 0 ? mergedModels : deepseekModels, + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +export function applyDeepSeekConfig(cfg: MoltbotConfig): MoltbotConfig { + const next = applyDeepSeekProviderConfig(cfg); + const existingModel = next.agents?.defaults?.model; + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, + model: { + ...(existingModel && "fallbacks" in (existingModel as Record) + ? { + fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks, + } + : undefined), + primary: DEEPSEEK_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index 053026162..2ee51dbc5 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -177,3 +177,16 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) { agentDir: resolveAuthAgentDir(agentDir), }); } + +export async function setDeepSeekApiKey(key: string, agentDir?: string) { + // Write to resolved agent dir so gateway finds credentials on startup. + upsertAuthProfile({ + profileId: "deepseek:default", + credential: { + type: "api_key", + provider: "deepseek", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} diff --git a/src/commands/onboard-auth.models.ts b/src/commands/onboard-auth.models.ts index de5a4edaa..9ba9e1b49 100644 --- a/src/commands/onboard-auth.models.ts +++ b/src/commands/onboard-auth.models.ts @@ -116,3 +116,42 @@ export function buildKimiCodeModelDefinition(): ModelDefinitionConfig { compat: KIMI_CODE_COMPAT, }; } + +export const DEEPSEEK_BASE_URL = "https://api.deepseek.com"; +export const DEEPSEEK_CHAT_MODEL_ID = "deepseek-chat"; +export const DEEPSEEK_REASONER_MODEL_ID = "deepseek-reasoner"; +export const DEEPSEEK_DEFAULT_MODEL_REF = `deepseek/${DEEPSEEK_CHAT_MODEL_ID}`; +export const DEEPSEEK_REASONER_MODEL_REF = `deepseek/${DEEPSEEK_REASONER_MODEL_ID}`; + +// DeepSeek pricing (per 1M tokens ) +const DEEPSEEK_DEFAULT_COST = { + input: 0.28, // Input (Cache Miss) + output: 0.42, // Output + cacheRead: 0.028, // Input (Cache Hit) + cacheWrite: 0.28, // Treat same as regular input for write +}; + +export function buildDeepSeekModelDefinition(modelId: string): ModelDefinitionConfig { + if (modelId === DEEPSEEK_CHAT_MODEL_ID) { + return { + id: DEEPSEEK_CHAT_MODEL_ID, + name: "DeepSeek Chat (V3.2)", + reasoning: false, + input: ["text"], + cost: DEEPSEEK_DEFAULT_COST, + contextWindow: 128000, + maxTokens: 8192, + }; + } else if (modelId === DEEPSEEK_REASONER_MODEL_ID) { + return { + id: DEEPSEEK_REASONER_MODEL_ID, + name: "DeepSeek Reasoner (V3.2)", + reasoning: true, + input: ["text"], + cost: DEEPSEEK_DEFAULT_COST, + contextWindow: 128000, + maxTokens: 64000, + }; + } + throw new Error(`Unknown DeepSeek model: ${modelId}`); +} diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index 612b24865..75db43cc8 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -5,6 +5,8 @@ export { export { VENICE_DEFAULT_MODEL_ID, VENICE_DEFAULT_MODEL_REF } from "../agents/venice-models.js"; export { applyAuthProfileConfig, + applyDeepSeekConfig, + applyDeepSeekProviderConfig, applyKimiCodeConfig, applyKimiCodeProviderConfig, applyMoonshotConfig, @@ -37,6 +39,7 @@ export { export { OPENROUTER_DEFAULT_MODEL_REF, setAnthropicApiKey, + setDeepSeekApiKey, setGeminiApiKey, setKimiCodeApiKey, setMinimaxApiKey, @@ -54,11 +57,17 @@ export { ZAI_DEFAULT_MODEL_REF, } from "./onboard-auth.credentials.js"; export { + buildDeepSeekModelDefinition, buildKimiCodeModelDefinition, buildMinimaxApiModelDefinition, buildMinimaxModelDefinition, buildMoonshotModelDefinition, DEFAULT_MINIMAX_BASE_URL, + DEEPSEEK_BASE_URL, + DEEPSEEK_CHAT_MODEL_ID, + DEEPSEEK_DEFAULT_MODEL_REF, + DEEPSEEK_REASONER_MODEL_ID, + DEEPSEEK_REASONER_MODEL_REF, KIMI_CODE_BASE_URL, KIMI_CODE_MODEL_ID, KIMI_CODE_MODEL_REF, diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index f4154bc6d..9dab969bd 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -17,6 +17,7 @@ export type AuthChoice = | "kimi-code-api-key" | "synthetic-api-key" | "venice-api-key" + | "deepseek-api-key" | "codex-cli" | "apiKey" | "gemini-api-key" @@ -72,6 +73,7 @@ export type OnboardOptions = { minimaxApiKey?: string; syntheticApiKey?: string; veniceApiKey?: string; + deepseekApiKey?: string; opencodeZenApiKey?: string; gatewayPort?: number; gatewayBind?: GatewayBind;