feat: add DeepSeek provider support

This commit is contained in:
Dadash Huseynzade 2026-01-26 20:52:19 +04:00
parent c4a80f4edb
commit 0e7ccb090f
11 changed files with 247 additions and 0 deletions

View File

@ -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).

View File

@ -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",

View File

@ -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 });

View File

@ -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;
}
});
});

View File

@ -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",

View File

@ -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;
}

View File

@ -20,6 +20,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
"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",

View File

@ -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<string, unknown>)
? {
fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks,
}
: undefined),
primary: DEEPSEEK_DEFAULT_MODEL_REF,
},
},
},
};
}
export function applyAuthProfileConfig(
cfg: ClawdbotConfig,
params: {

View File

@ -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({

View File

@ -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,

View File

@ -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;