diff --git a/.github/labeler.yml b/.github/labeler.yml index b6422060fea..5005cdd6b9b 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -234,6 +234,10 @@ - changed-files: - any-glob-to-any-file: - "extensions/byteplus/**" +"extensions: deepseek": + - changed-files: + - any-glob-to-any-file: + - "extensions/deepseek/**" "extensions: anthropic": - changed-files: - any-glob-to-any-file: diff --git a/docs/docs.json b/docs/docs.json index 80409046397..42852e724c9 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1106,6 +1106,7 @@ "providers/cloudflare-ai-gateway", "providers/claude-max-api-proxy", "providers/deepgram", + "providers/deepseek", "providers/github-copilot", "providers/huggingface", "providers/kilocode", diff --git a/docs/providers/deepseek.md b/docs/providers/deepseek.md new file mode 100644 index 00000000000..755e4c408e2 --- /dev/null +++ b/docs/providers/deepseek.md @@ -0,0 +1,63 @@ +--- +summary: "DeepSeek setup (auth + model selection)" +read_when: + - You want to use DeepSeek with OpenClaw + - You need the API key env var or CLI auth choice +--- + +# DeepSeek + +[DeepSeek](https://www.deepseek.com) provides powerful AI models with an OpenAI-compatible API. + +- Provider: `deepseek` +- Auth: `DEEPSEEK_API_KEY` +- API: OpenAI-compatible + +## Quick start + +1. Set the API key (recommended: store it for the Gateway): + +```bash +openclaw setup --wizard --auth-choice deepseek-api-key +``` + +2. Set a default model: + +```json5 +{ + agents: { + defaults: { + model: { primary: "deepseek/deepseek-chat" }, + }, + }, +} +``` + +## Non-interactive example + +```bash +openclaw setup --wizard --non-interactive \ + --mode local \ + --auth-choice deepseek-api-key \ + --deepseek-api-key "$DEEPSEEK_API_KEY" +``` + +This will set `deepseek/deepseek-chat` as the default model. + +## Environment note + +If the Gateway runs as a daemon (launchd/systemd), make sure `DEEPSEEK_API_KEY` +is available to that process (for example, in `~/.openclaw/.env` or via +`env.shellEnv`). + +## Available models + +| Model ID | Name | Type | Context | +| ------------------- | ------------------------ | --------- | ------- | +| `deepseek-chat` | DeepSeek Chat (V3.2) | General | 128K | +| `deepseek-reasoner` | DeepSeek Reasoner (V3.2) | Reasoning | 128K | + +- **deepseek-chat** corresponds to DeepSeek-V3.2 in non-thinking mode. +- **deepseek-reasoner** corresponds to DeepSeek-V3.2 in thinking mode with chain-of-thought reasoning. + +Get your API key at [platform.deepseek.com](https://platform.deepseek.com/api_keys). diff --git a/extensions/deepseek/index.ts b/extensions/deepseek/index.ts new file mode 100644 index 00000000000..531e3afff13 --- /dev/null +++ b/extensions/deepseek/index.ts @@ -0,0 +1,55 @@ +import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core"; +import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth"; +import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog"; +import { applyDeepSeekConfig, DEEPSEEK_DEFAULT_MODEL_REF } from "./onboard.js"; +import { buildDeepSeekProvider } from "./provider-catalog.js"; + +const PROVIDER_ID = "deepseek"; + +const deepseekPlugin = { + id: PROVIDER_ID, + name: "DeepSeek Provider", + description: "Bundled DeepSeek provider plugin", + configSchema: emptyPluginConfigSchema(), + register(api: OpenClawPluginApi) { + api.registerProvider({ + id: PROVIDER_ID, + label: "DeepSeek", + docsPath: "/providers/deepseek", + envVars: ["DEEPSEEK_API_KEY"], + auth: [ + createProviderApiKeyAuthMethod({ + providerId: PROVIDER_ID, + methodId: "api-key", + label: "DeepSeek API key", + hint: "API key", + optionKey: "deepseekApiKey", + flagName: "--deepseek-api-key", + envVar: "DEEPSEEK_API_KEY", + promptMessage: "Enter DeepSeek API key", + defaultModel: DEEPSEEK_DEFAULT_MODEL_REF, + expectedProviders: ["deepseek"], + applyConfig: (cfg) => applyDeepSeekConfig(cfg), + wizard: { + choiceId: "deepseek-api-key", + choiceLabel: "DeepSeek API key", + groupId: "deepseek", + groupLabel: "DeepSeek", + groupHint: "API key", + }, + }), + ], + catalog: { + order: "simple", + run: (ctx) => + buildSingleProviderApiKeyCatalog({ + ctx, + providerId: PROVIDER_ID, + buildProvider: buildDeepSeekProvider, + }), + }, + }); + }, +}; + +export default deepseekPlugin; diff --git a/extensions/deepseek/onboard.ts b/extensions/deepseek/onboard.ts new file mode 100644 index 00000000000..3bc613e63ab --- /dev/null +++ b/extensions/deepseek/onboard.ts @@ -0,0 +1,35 @@ +import { + buildDeepSeekModelDefinition, + DEEPSEEK_BASE_URL, + DEEPSEEK_MODEL_CATALOG, +} from "openclaw/plugin-sdk/provider-models"; +import { + applyAgentDefaultModelPrimary, + applyProviderConfigWithModelCatalog, + type OpenClawConfig, +} from "openclaw/plugin-sdk/provider-onboard"; + +export const DEEPSEEK_DEFAULT_MODEL_REF = "deepseek/deepseek-chat"; + +export function applyDeepSeekProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[DEEPSEEK_DEFAULT_MODEL_REF] = { + ...models[DEEPSEEK_DEFAULT_MODEL_REF], + alias: models[DEEPSEEK_DEFAULT_MODEL_REF]?.alias ?? "DeepSeek", + }; + + return applyProviderConfigWithModelCatalog(cfg, { + agentModels: models, + providerId: "deepseek", + api: "openai-completions", + baseUrl: DEEPSEEK_BASE_URL, + catalogModels: DEEPSEEK_MODEL_CATALOG.map(buildDeepSeekModelDefinition), + }); +} + +export function applyDeepSeekConfig(cfg: OpenClawConfig): OpenClawConfig { + return applyAgentDefaultModelPrimary( + applyDeepSeekProviderConfig(cfg), + DEEPSEEK_DEFAULT_MODEL_REF, + ); +} diff --git a/extensions/deepseek/openclaw.plugin.json b/extensions/deepseek/openclaw.plugin.json new file mode 100644 index 00000000000..55c9c2779e8 --- /dev/null +++ b/extensions/deepseek/openclaw.plugin.json @@ -0,0 +1,27 @@ +{ + "id": "deepseek", + "providers": ["deepseek"], + "providerAuthEnvVars": { + "deepseek": ["DEEPSEEK_API_KEY"] + }, + "providerAuthChoices": [ + { + "provider": "deepseek", + "method": "api-key", + "choiceId": "deepseek-api-key", + "choiceLabel": "DeepSeek API key", + "groupId": "deepseek", + "groupLabel": "DeepSeek", + "groupHint": "API key", + "optionKey": "deepseekApiKey", + "cliFlag": "--deepseek-api-key", + "cliOption": "--deepseek-api-key ", + "cliDescription": "DeepSeek API key" + } + ], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + } +} diff --git a/extensions/deepseek/package.json b/extensions/deepseek/package.json new file mode 100644 index 00000000000..3a83021c043 --- /dev/null +++ b/extensions/deepseek/package.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/deepseek-provider", + "version": "2026.3.14", + "private": true, + "description": "OpenClaw DeepSeek provider plugin", + "type": "module", + "openclaw": { + "extensions": [ + "./index.ts" + ] + } +} diff --git a/extensions/deepseek/provider-catalog.ts b/extensions/deepseek/provider-catalog.ts new file mode 100644 index 00000000000..ef8054d5046 --- /dev/null +++ b/extensions/deepseek/provider-catalog.ts @@ -0,0 +1,14 @@ +import { + buildDeepSeekModelDefinition, + DEEPSEEK_BASE_URL, + DEEPSEEK_MODEL_CATALOG, + type ModelProviderConfig, +} from "openclaw/plugin-sdk/provider-models"; + +export function buildDeepSeekProvider(): ModelProviderConfig { + return { + baseUrl: DEEPSEEK_BASE_URL, + api: "openai-completions", + models: DEEPSEEK_MODEL_CATALOG.map(buildDeepSeekModelDefinition), + }; +} diff --git a/src/agents/deepseek-models.ts b/src/agents/deepseek-models.ts new file mode 100644 index 00000000000..71a83e2de2f --- /dev/null +++ b/src/agents/deepseek-models.ts @@ -0,0 +1,46 @@ +import type { ModelDefinitionConfig } from "../config/types.models.js"; + +export const DEEPSEEK_BASE_URL = "https://api.deepseek.com"; + +const DEEPSEEK_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; + +export const DEEPSEEK_MODEL_CATALOG: ModelDefinitionConfig[] = [ + { + id: "deepseek-chat", + name: "DeepSeek Chat", + reasoning: false, + input: ["text"], + contextWindow: 131072, + maxTokens: 8192, + cost: DEEPSEEK_DEFAULT_COST, + }, + { + id: "deepseek-reasoner", + name: "DeepSeek Reasoner", + reasoning: true, + input: ["text"], + contextWindow: 131072, + maxTokens: 8192, + cost: DEEPSEEK_DEFAULT_COST, + }, +]; + +export function buildDeepSeekModelDefinition( + model: (typeof DEEPSEEK_MODEL_CATALOG)[number], +): ModelDefinitionConfig { + return { + id: model.id, + name: model.name, + api: "openai-completions", + reasoning: model.reasoning, + input: model.input, + cost: model.cost, + contextWindow: model.contextWindow, + maxTokens: model.maxTokens, + }; +} diff --git a/src/agents/models-config.providers.deepseek.test.ts b/src/agents/models-config.providers.deepseek.test.ts new file mode 100644 index 00000000000..99678e2bdc3 --- /dev/null +++ b/src/agents/models-config.providers.deepseek.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { buildDeepSeekProvider } from "./models-config.providers.static.js"; + +describe("DeepSeek provider", () => { + it("should build deepseek provider with correct configuration", () => { + const provider = buildDeepSeekProvider(); + expect(provider.baseUrl).toBe("https://api.deepseek.com"); + expect(provider.api).toBe("openai-completions"); + expect(provider.models).toBeDefined(); + expect(provider.models.length).toBeGreaterThan(0); + }); + + it("should include both deepseek-chat and deepseek-reasoner models", () => { + const provider = buildDeepSeekProvider(); + const modelIds = provider.models.map((m) => m.id); + expect(modelIds).toContain("deepseek-chat"); + expect(modelIds).toContain("deepseek-reasoner"); + }); + + it("should mark deepseek-reasoner as a reasoning model", () => { + const provider = buildDeepSeekProvider(); + const reasoner = provider.models.find((m) => m.id === "deepseek-reasoner"); + const chat = provider.models.find((m) => m.id === "deepseek-chat"); + expect(reasoner?.reasoning).toBe(true); + expect(chat?.reasoning).toBe(false); + }); +}); diff --git a/src/agents/models-config.providers.static.ts b/src/agents/models-config.providers.static.ts index 71184e12286..d6fd8179958 100644 --- a/src/agents/models-config.providers.static.ts +++ b/src/agents/models-config.providers.static.ts @@ -2,6 +2,7 @@ export { buildBytePlusCodingProvider, buildBytePlusProvider, } from "../../extensions/byteplus/provider-catalog.js"; +export { buildDeepSeekProvider } from "../../extensions/deepseek/provider-catalog.js"; export { buildKimiCodingProvider } from "../../extensions/kimi-coding/provider-catalog.js"; export { buildKilocodeProvider } from "../../extensions/kilocode/provider-catalog.js"; export { diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index 832fae75448..748de6bc522 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -10,6 +10,7 @@ export type BuiltInAuthChoice = | "claude-cli" | "token" | "chutes" + | "deepseek-api-key" | "openai-codex" | "openai-api-key" | "openrouter-api-key" @@ -58,6 +59,7 @@ export type BuiltInAuthChoiceGroupId = | "openai" | "anthropic" | "chutes" + | "deepseek" | "google" | "copilot" | "openrouter" @@ -115,6 +117,7 @@ export type OnboardOptions = { /** API key persistence mode for setup flows (default: plaintext). */ secretInputMode?: SecretInputMode; anthropicApiKey?: string; + deepseekApiKey?: string; openaiApiKey?: string; mistralApiKey?: string; openrouterApiKey?: string; diff --git a/src/plugin-sdk/provider-models.ts b/src/plugin-sdk/provider-models.ts index 5694a540075..b3d903b61b4 100644 --- a/src/plugin-sdk/provider-models.ts +++ b/src/plugin-sdk/provider-models.ts @@ -41,6 +41,11 @@ export { SYNTHETIC_DEFAULT_MODEL_REF, SYNTHETIC_MODEL_CATALOG, } from "../agents/synthetic-models.js"; +export { + buildDeepSeekModelDefinition, + DEEPSEEK_BASE_URL, + DEEPSEEK_MODEL_CATALOG, +} from "../agents/deepseek-models.js"; export { buildTogetherModelDefinition, TOGETHER_BASE_URL, diff --git a/src/plugins/bundled-provider-auth-env-vars.ts b/src/plugins/bundled-provider-auth-env-vars.ts index 42ca376959d..fac2cae8a73 100644 --- a/src/plugins/bundled-provider-auth-env-vars.ts +++ b/src/plugins/bundled-provider-auth-env-vars.ts @@ -2,6 +2,7 @@ import ANTHROPIC_MANIFEST from "../../extensions/anthropic/openclaw.plugin.json" import BYTEPLUS_MANIFEST from "../../extensions/byteplus/openclaw.plugin.json" with { type: "json" }; import CLOUDFLARE_AI_GATEWAY_MANIFEST from "../../extensions/cloudflare-ai-gateway/openclaw.plugin.json" with { type: "json" }; import COPILOT_PROXY_MANIFEST from "../../extensions/copilot-proxy/openclaw.plugin.json" with { type: "json" }; +import DEEPSEEK_MANIFEST from "../../extensions/deepseek/openclaw.plugin.json" with { type: "json" }; import GITHUB_COPILOT_MANIFEST from "../../extensions/github-copilot/openclaw.plugin.json" with { type: "json" }; import GOOGLE_MANIFEST from "../../extensions/google/openclaw.plugin.json" with { type: "json" }; import HUGGINGFACE_MANIFEST from "../../extensions/huggingface/openclaw.plugin.json" with { type: "json" }; @@ -63,6 +64,7 @@ export const BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES = collectBundledProviderAu BYTEPLUS_MANIFEST, CLOUDFLARE_AI_GATEWAY_MANIFEST, COPILOT_PROXY_MANIFEST, + DEEPSEEK_MANIFEST, GITHUB_COPILOT_MANIFEST, GOOGLE_MANIFEST, HUGGINGFACE_MANIFEST,