diff --git a/CHANGELOG.md b/CHANGELOG.md index 668a91823..68ca94c1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,14 @@ Status: unreleased. ## 2026.1.24-2 +## [Unreleased] + +### Added +- DeepSeek provider support with `deepseek-chat` and `deepseek-reasoner` models + - Set `DEEPSEEK_API_KEY` environment variable to enable + - OpenAI-compatible API at https://api.deepseek.com/v1 + - Supports 128K context window + ### Fixes - Packaging: include dist/link-understanding output in npm tarball (fixes missing apply.js import on install). diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 680d0f53c..e987ecf06 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -281,6 +281,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { moonshot: "MOONSHOT_API_KEY", "kimi-code": "KIMICODE_API_KEY", minimax: "MINIMAX_API_KEY", + deepseek: "DEEPSEEK_API_KEY", synthetic: "SYNTHETIC_API_KEY", venice: "VENICE_API_KEY", mistral: "MISTRAL_API_KEY", diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 996f09dd0..88258ee78 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -64,6 +64,19 @@ const QWEN_PORTAL_DEFAULT_COST = { cacheWrite: 0, }; +const DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1"; +const DEEPSEEK_CHAT_MODEL_ID = "deepseek-chat"; +const DEEPSEEK_REASONER_MODEL_ID = "deepseek-reasoner"; +const DEEPSEEK_CONTEXT_WINDOW = 128000; +const DEEPSEEK_CHAT_MAX_TOKENS = 8192; +const DEEPSEEK_REASONER_MAX_TOKENS = 64000; +const DEEPSEEK_COST = { + input: 0.28, + output: 1.1, + cacheRead: 0.028, + cacheWrite: 0.28, +}; + const OLLAMA_BASE_URL = "http://127.0.0.1:11434/v1"; const OLLAMA_API_BASE_URL = "http://127.0.0.1:11434"; const OLLAMA_DEFAULT_CONTEXT_WINDOW = 128000; @@ -333,6 +346,33 @@ function buildQwenPortalProvider(): ProviderConfig { }; } +function buildDeepSeekProvider(): ProviderConfig { + return { + baseUrl: DEEPSEEK_BASE_URL, + api: "openai-completions", + models: [ + { + id: DEEPSEEK_CHAT_MODEL_ID, + name: "DeepSeek V3.2 Chat", + reasoning: false, + input: ["text"], + cost: DEEPSEEK_COST, + contextWindow: DEEPSEEK_CONTEXT_WINDOW, + maxTokens: DEEPSEEK_CHAT_MAX_TOKENS, + }, + { + id: DEEPSEEK_REASONER_MODEL_ID, + name: "DeepSeek V3.2 Reasoner", + reasoning: true, + input: ["text"], + cost: DEEPSEEK_COST, + contextWindow: DEEPSEEK_CONTEXT_WINDOW, + maxTokens: DEEPSEEK_REASONER_MAX_TOKENS, + }, + ], + }; +} + function buildSyntheticProvider(): ProviderConfig { return { baseUrl: SYNTHETIC_BASE_URL, @@ -388,6 +428,13 @@ export async function resolveImplicitProviders(params: { providers["kimi-code"] = { ...buildKimiCodeProvider(), apiKey: kimiCodeKey }; } + const deepseekKey = + resolveEnvApiKeyVarName("deepseek") ?? + resolveApiKeyFromProfiles({ provider: "deepseek", store: authStore }); + if (deepseekKey) { + providers.deepseek = { ...buildDeepSeekProvider(), apiKey: deepseekKey }; + } + const syntheticKey = resolveEnvApiKeyVarName("synthetic") ?? resolveApiKeyFromProfiles({ provider: "synthetic", store: authStore }); diff --git a/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts b/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts index 93b7ee628..8f084e40b 100644 --- a/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts +++ b/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts @@ -51,6 +51,7 @@ describe("models-config", () => { const previousKimiCode = process.env.KIMICODE_API_KEY; const previousMinimax = process.env.MINIMAX_API_KEY; const previousMoonshot = process.env.MOONSHOT_API_KEY; + const previousDeepSeek = process.env.DEEPSEEK_API_KEY; const previousSynthetic = process.env.SYNTHETIC_API_KEY; const previousVenice = process.env.VENICE_API_KEY; delete process.env.COPILOT_GITHUB_TOKEN; @@ -59,6 +60,7 @@ describe("models-config", () => { delete process.env.KIMICODE_API_KEY; delete process.env.MINIMAX_API_KEY; delete process.env.MOONSHOT_API_KEY; + delete process.env.DEEPSEEK_API_KEY; delete process.env.SYNTHETIC_API_KEY; delete process.env.VENICE_API_KEY; @@ -89,6 +91,8 @@ describe("models-config", () => { else process.env.MINIMAX_API_KEY = previousMinimax; if (previousMoonshot === undefined) delete process.env.MOONSHOT_API_KEY; else process.env.MOONSHOT_API_KEY = previousMoonshot; + if (previousDeepSeek === undefined) delete process.env.DEEPSEEK_API_KEY; + else process.env.DEEPSEEK_API_KEY = previousDeepSeek; if (previousSynthetic === undefined) delete process.env.SYNTHETIC_API_KEY; else process.env.SYNTHETIC_API_KEY = previousSynthetic; if (previousVenice === undefined) delete process.env.VENICE_API_KEY; @@ -181,3 +185,38 @@ describe("models-config", () => { }); }); }); + +it("adds deepseek provider when DEEPSEEK_API_KEY is set", async () => { + await withTempHome(async () => { + vi.resetModules(); + const prevKey = process.env.DEEPSEEK_API_KEY; + process.env.DEEPSEEK_API_KEY = "sk-deepseek-test"; + try { + const { ensureClawdbotModelsJson } = await import("./models-config.js"); + const { resolveClawdbotAgentDir } = await import("./agent-paths.js"); + + await ensureClawdbotModelsJson({}); + + const modelPath = path.join(resolveClawdbotAgentDir(), "models.json"); + const raw = await fs.readFile(modelPath, "utf8"); + const parsed = JSON.parse(raw) as { + providers: Record< + string, + { + baseUrl?: string; + apiKey?: string; + models?: Array<{ id: string }>; + } + >; + }; + expect(parsed.providers.deepseek?.baseUrl).toBe("https://api.deepseek.com/v1"); + expect(parsed.providers.deepseek?.apiKey).toBe("DEEPSEEK_API_KEY"); + const ids = parsed.providers.deepseek?.models?.map((model) => model.id); + expect(ids).toContain("deepseek-chat"); + expect(ids).toContain("deepseek-reasoner"); + } finally { + if (prevKey === undefined) delete process.env.DEEPSEEK_API_KEY; + else process.env.DEEPSEEK_API_KEY = prevKey; + } + }); +}); diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index f13eef365..d8bb498da 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -20,6 +20,7 @@ export type AuthChoiceGroupId = | "zai" | "opencode-zen" | "minimax" + | "deepseek" | "synthetic" | "venice" | "qwen"; @@ -55,6 +56,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "M2.1 (recommended)", choices: ["minimax-api", "minimax-api-lightning"], }, + { + value: "deepseek", + label: "DeepSeek", + hint: "V3.2 Chat + Reasoner", + choices: ["deepseek-api-key"], + }, { value: "qwen", label: "Qwen", diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 8be02008b..92b8e21ef 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, @@ -579,5 +581,64 @@ 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; + } + + if (!hasCredential) { + await params.prompter.note( + [ + "DeepSeek provides high-performance AI models with competitive pricing.", + "Get your API key at: https://platform.deepseek.com/", + "Supports both chat and reasoning models.", + ].join("\n"), + "DeepSeek", + ); + } + + 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/deepseek-chat", + applyDefaultConfig: applyDeepSeekConfig, + applyProviderConfig: applyDeepSeekProviderConfig, + noteDefault: "deepseek/deepseek-chat", + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + return { config: nextConfig, agentModelOverride }; + } + return null; } diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index 6fe26b59a..737328bca 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -20,6 +20,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { "zai-api-key": "zai", "synthetic-api-key": "synthetic", "venice-api-key": "venice", + "deepseek-api-key": "deepseek", "github-copilot": "github-copilot", "copilot-proxy": "copilot-proxy", "minimax-cloud": "minimax", diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 0d3a8523a..00ef68e39 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -411,6 +411,71 @@ export function applyVeniceConfig(cfg: ClawdbotConfig): ClawdbotConfig { }; } +const DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1"; +const DEEPSEEK_DEFAULT_MODEL_REF = "deepseek/deepseek-chat"; + +export function applyDeepSeekProviderConfig(cfg: ClawdbotConfig): ClawdbotConfig { + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.deepseek; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + 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: existingModels, + }; + + const models = { ...cfg.agents?.defaults?.models }; + models[DEEPSEEK_DEFAULT_MODEL_REF] = { + ...models[DEEPSEEK_DEFAULT_MODEL_REF], + alias: "DeepSeek", + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +export function applyDeepSeekConfig(cfg: ClawdbotConfig): ClawdbotConfig { + 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, + }, + }, + }, + }; +} + export function applyAuthProfileConfig( cfg: ClawdbotConfig, params: { diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index 0c7dff409..4deaca324 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -60,6 +60,19 @@ export async function setMinimaxApiKey(key: string, agentDir?: string) { }); } +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), + }); +} + export async function setMoonshotApiKey(key: string, agentDir?: string) { // Write to resolved agent dir so gateway finds credentials on startup. upsertAuthProfile({ diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index b122d89cf..abcdbbd72 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, @@ -35,6 +37,7 @@ export { export { OPENROUTER_DEFAULT_MODEL_REF, setAnthropicApiKey, + setDeepSeekApiKey, setGeminiApiKey, setKimiCodeApiKey, setMinimaxApiKey, diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index 84c15afc4..9844a488c 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -27,6 +27,7 @@ export type AuthChoice = | "minimax" | "minimax-api" | "minimax-api-lightning" + | "deepseek-api-key" | "opencode-zen" | "github-copilot" | "copilot-proxy" @@ -68,6 +69,7 @@ export type OnboardOptions = { geminiApiKey?: string; zaiApiKey?: string; minimaxApiKey?: string; + deepseekApiKey?: string; syntheticApiKey?: string; veniceApiKey?: string; opencodeZenApiKey?: string;