diff --git a/.github/labeler.yml b/.github/labeler.yml index c2b3da01b9..f82c50693c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -241,6 +241,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/CHANGELOG.md b/CHANGELOG.md index 09409774fe..86ebc46cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,6 +105,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Plugins/DeepSeek: refactor the bundled DeepSeek provider onto the shared single-provider plugin entry, move its coverage into the extension test lane, and keep bundled auth env-var metadata on the generated manifest path. (#48762) Thanks @07akioni. - Web tools/search provider lists: keep onboarding, configure, and docs provider lists alphabetical while preserving the separate runtime auto-detect precedence used for credential-based provider selection. - Media/Windows security: block remote-host `file://` media URLs and UNC/network paths before local filesystem resolution in core media loading and adjacent prompt/sandbox attachment seams, so the next release no longer allows structured local-media inputs to trigger outbound SMB credential handshakes on Windows. Thanks @RacerZ-fighting for reporting. - Gateway/discovery: fail closed on unresolved Bonjour and DNS-SD service endpoints in CLI discovery, onboarding, and `gateway status` so TXT-only hints can no longer steer routing or SSH auto-target selection. Thanks @nexrin for reporting. diff --git a/docs/docs.json b/docs/docs.json index 688e283c2f..740958ba11 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1183,6 +1183,7 @@ "providers/cloudflare-ai-gateway", "providers/claude-max-api-proxy", "providers/deepgram", + "providers/deepseek", "providers/github-copilot", "providers/google", "providers/groq", diff --git a/docs/providers/deepseek.md b/docs/providers/deepseek.md new file mode 100644 index 0000000000..392c6fceaf --- /dev/null +++ b/docs/providers/deepseek.md @@ -0,0 +1,53 @@ +--- +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 + +Set the API key (recommended: store it for the Gateway): + +```bash +openclaw onboard --auth-choice deepseek-api-key +``` + +This will prompt for your API key and set `deepseek/deepseek-chat` as the default model. + +## Non-interactive example + +```bash +openclaw onboard --non-interactive \ + --mode local \ + --auth-choice deepseek-api-key \ + --deepseek-api-key "$DEEPSEEK_API_KEY" \ + --skip-health \ + --accept-risk +``` + +## 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.test.ts b/extensions/deepseek/index.test.ts new file mode 100644 index 0000000000..ff2152ae4e --- /dev/null +++ b/extensions/deepseek/index.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; +import { resolveProviderPluginChoice } from "../../src/plugins/provider-wizard.js"; +import { registerSingleProviderPlugin } from "../../test/helpers/extensions/plugin-registration.js"; +import deepseekPlugin from "./index.js"; + +describe("deepseek provider plugin", () => { + it("registers DeepSeek with api-key auth wizard metadata", () => { + const provider = registerSingleProviderPlugin(deepseekPlugin); + const resolved = resolveProviderPluginChoice({ + providers: [provider], + choice: "deepseek-api-key", + }); + + expect(provider.id).toBe("deepseek"); + expect(provider.label).toBe("DeepSeek"); + expect(provider.envVars).toEqual(["DEEPSEEK_API_KEY"]); + expect(provider.auth).toHaveLength(1); + expect(resolved).not.toBeNull(); + expect(resolved?.provider.id).toBe("deepseek"); + expect(resolved?.method.id).toBe("api-key"); + }); + + it("builds the static DeepSeek model catalog", async () => { + const provider = registerSingleProviderPlugin(deepseekPlugin); + expect(provider.catalog).toBeDefined(); + + const catalog = await provider.catalog!.run({ + config: {}, + env: {}, + resolveProviderApiKey: () => ({ apiKey: "test-key" }), + resolveProviderAuth: () => ({ + apiKey: "test-key", + mode: "api_key", + source: "env", + }), + } as never); + + expect(catalog && "provider" in catalog).toBe(true); + if (!catalog || !("provider" in catalog)) { + throw new Error("expected single-provider catalog"); + } + + expect(catalog.provider.api).toBe("openai-completions"); + expect(catalog.provider.baseUrl).toBe("https://api.deepseek.com"); + expect(catalog.provider.models?.map((model) => model.id)).toEqual([ + "deepseek-chat", + "deepseek-reasoner", + ]); + expect( + catalog.provider.models?.find((model) => model.id === "deepseek-reasoner")?.reasoning, + ).toBe(true); + }); +}); diff --git a/extensions/deepseek/index.ts b/extensions/deepseek/index.ts new file mode 100644 index 0000000000..cc8086e23c --- /dev/null +++ b/extensions/deepseek/index.ts @@ -0,0 +1,38 @@ +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; +import { applyDeepSeekConfig, DEEPSEEK_DEFAULT_MODEL_REF } from "./onboard.js"; +import { buildDeepSeekProvider } from "./provider-catalog.js"; + +const PROVIDER_ID = "deepseek"; + +export default defineSingleProviderPluginEntry({ + id: PROVIDER_ID, + name: "DeepSeek Provider", + description: "Bundled DeepSeek provider plugin", + provider: { + label: "DeepSeek", + docsPath: "/providers/deepseek", + auth: [ + { + 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, + applyConfig: (cfg) => applyDeepSeekConfig(cfg), + wizard: { + choiceId: "deepseek-api-key", + choiceLabel: "DeepSeek API key", + groupId: "deepseek", + groupLabel: "DeepSeek", + groupHint: "API key", + }, + }, + ], + catalog: { + buildProvider: buildDeepSeekProvider, + }, + }, +}); diff --git a/extensions/deepseek/onboard.ts b/extensions/deepseek/onboard.ts new file mode 100644 index 0000000000..3bc613e63a --- /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 0000000000..55c9c2779e --- /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 0000000000..3a83021c04 --- /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 0000000000..ef8054d504 --- /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/pnpm-lock.yaml b/pnpm-lock.yaml index a31ed2f6a5..454c4b24b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -271,6 +271,8 @@ importers: extensions/deepgram: {} + extensions/deepseek: {} + extensions/diagnostics-otel: dependencies: '@opentelemetry/api': diff --git a/src/agents/deepseek-models.ts b/src/agents/deepseek-models.ts new file mode 100644 index 0000000000..925321933a --- /dev/null +++ b/src/agents/deepseek-models.ts @@ -0,0 +1,44 @@ +import type { ModelDefinitionConfig } from "../config/types.models.js"; + +export const DEEPSEEK_BASE_URL = "https://api.deepseek.com"; + +// TODO: fill in actual DeepSeek API pricing +// https://api-docs.deepseek.com/quick_start/pricing +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, + compat: { supportsUsageInStreaming: true }, + }, + { + id: "deepseek-reasoner", + name: "DeepSeek Reasoner", + reasoning: true, + input: ["text"], + contextWindow: 131072, + maxTokens: 65536, + cost: DEEPSEEK_DEFAULT_COST, + compat: { supportsUsageInStreaming: true }, + }, +]; + +export function buildDeepSeekModelDefinition( + model: (typeof DEEPSEEK_MODEL_CATALOG)[number], +): ModelDefinitionConfig { + return { + ...model, + api: "openai-completions", + }; +} diff --git a/src/agents/model-catalog.ts b/src/agents/model-catalog.ts index cd8ea84691..bed22e3581 100644 --- a/src/agents/model-catalog.ts +++ b/src/agents/model-catalog.ts @@ -34,7 +34,7 @@ const defaultImportPiSdk = () => import("./pi-model-discovery-runtime.js"); let importPiSdk = defaultImportPiSdk; let modelSuppressionPromise: Promise | undefined; -const NON_PI_NATIVE_MODEL_PROVIDERS = new Set(["kilocode"]); +const NON_PI_NATIVE_MODEL_PROVIDERS = new Set(["deepseek", "kilocode"]); function shouldLogModelCatalogTiming(): boolean { return process.env.OPENCLAW_DEBUG_INGRESS_TIMING === "1"; diff --git a/src/agents/models-config.providers.static.ts b/src/agents/models-config.providers.static.ts index 4de77f0e36..f078456549 100644 --- a/src/agents/models-config.providers.static.ts +++ b/src/agents/models-config.providers.static.ts @@ -1,8 +1,13 @@ export { ANTHROPIC_VERTEX_DEFAULT_MODEL_ID, buildAnthropicVertexProvider, +} from "../../extensions/anthropic-vertex/provider-catalog.js"; +export { buildBytePlusCodingProvider, buildBytePlusProvider, +} from "../../extensions/byteplus/provider-catalog.js"; +export { buildDeepSeekProvider } from "../../extensions/deepseek/provider-catalog.js"; +export { buildKimiCodingProvider, buildKilocodeProvider, buildMinimaxPortalProvider, diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index 832fae7544..748de6bc52 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/config/io.ts b/src/config/io.ts index fba17f253a..ea2abaf191 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -62,6 +62,7 @@ export { MissingEnvVarError } from "./env-substitution.js"; const SHELL_ENV_EXPECTED_KEYS = [ "OPENAI_API_KEY", "ANTHROPIC_API_KEY", + "DEEPSEEK_API_KEY", "ANTHROPIC_OAUTH_TOKEN", "GEMINI_API_KEY", "ZAI_API_KEY", diff --git a/src/plugin-sdk/provider-models.ts b/src/plugin-sdk/provider-models.ts index f69ca8addd..e9d86b95a4 100644 --- a/src/plugin-sdk/provider-models.ts +++ b/src/plugin-sdk/provider-models.ts @@ -85,6 +85,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/plugin-sdk/subpaths.test.ts b/src/plugin-sdk/subpaths.test.ts index 9bf95b8ebd..d8183d0eaf 100644 --- a/src/plugin-sdk/subpaths.test.ts +++ b/src/plugin-sdk/subpaths.test.ts @@ -20,6 +20,7 @@ import type { OpenClawPluginApi as CoreOpenClawPluginApi, PluginRuntime as CorePluginRuntime, } from "openclaw/plugin-sdk/core"; +import * as providerEntrySdk from "openclaw/plugin-sdk/provider-entry"; import { describe, expect, expectTypeOf, it } from "vitest"; import type { ChannelMessageActionContext } from "../channels/plugins/types.js"; import type { @@ -628,4 +629,8 @@ describe("plugin-sdk subpath exports", () => { expect(mod, `subpath ${id} should resolve`).toBeTruthy(); } }); + + it("exports single-provider plugin entry helpers from the dedicated subpath", () => { + expect(typeof providerEntrySdk.defineSingleProviderPluginEntry).toBe("function"); + }); }); diff --git a/src/plugins/bundled-plugin-metadata.generated.ts b/src/plugins/bundled-plugin-metadata.generated.ts index 74f631b11d..cdd00540c0 100644 --- a/src/plugins/bundled-plugin-metadata.generated.ts +++ b/src/plugins/bundled-plugin-metadata.generated.ts @@ -490,6 +490,47 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ }, }, }, + { + dirName: "deepseek", + idHint: "deepseek", + source: { + source: "./index.ts", + built: "index.js", + }, + packageName: "@openclaw/deepseek-provider", + packageVersion: "2026.3.14", + packageDescription: "OpenClaw DeepSeek provider plugin", + packageManifest: { + extensions: ["./index.ts"], + }, + manifest: { + id: "deepseek", + configSchema: { + type: "object", + additionalProperties: false, + properties: {}, + }, + 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", + }, + ], + }, + }, { dirName: "diagnostics-otel", idHint: "diagnostics-otel", diff --git a/src/plugins/bundled-provider-auth-env-vars.generated.ts b/src/plugins/bundled-provider-auth-env-vars.generated.ts index 4bb35f457a..611f11121c 100644 --- a/src/plugins/bundled-provider-auth-env-vars.generated.ts +++ b/src/plugins/bundled-provider-auth-env-vars.generated.ts @@ -6,6 +6,7 @@ export const BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES = { byteplus: ["BYTEPLUS_API_KEY"], chutes: ["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"], "cloudflare-ai-gateway": ["CLOUDFLARE_AI_GATEWAY_API_KEY"], + deepseek: ["DEEPSEEK_API_KEY"], exa: ["EXA_API_KEY"], fal: ["FAL_KEY"], firecrawl: ["FIRECRAWL_API_KEY"], diff --git a/src/plugins/config-state.ts b/src/plugins/config-state.ts index 986f038e4c..4566db4f30 100644 --- a/src/plugins/config-state.ts +++ b/src/plugins/config-state.ts @@ -33,6 +33,7 @@ export const BUNDLED_ENABLED_BY_DEFAULT = new Set([ "anthropic", "byteplus", "cloudflare-ai-gateway", + "deepseek", "device-pair", "github-copilot", "google",