Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
# OpenClaw DeepSeek Provider
Official OpenClaw provider plugin for DeepSeek.
Install from OpenClaw:
```bash
openclaw plugins install @openclaw/deepseek-provider
openclaw gateway restart
```
See <https://docs.openclaw.ai/providers/deepseek> for setup and configuration.

View File

@@ -0,0 +1,8 @@
// Deepseek API module exposes the plugin public contract.
export {
buildDeepSeekModelDefinition,
DEEPSEEK_BASE_URL,
DEEPSEEK_MODEL_CATALOG,
} from "./models.js";
export { buildDeepSeekProvider } from "./provider-catalog.js";
export { createDeepSeekV4ThinkingWrapper } from "./stream.js";

View File

@@ -0,0 +1,233 @@
// Deepseek tests cover deepseek plugin behavior.
import {
completeSimple,
streamSimple,
type AssistantMessage,
type Context,
type Model,
} from "openclaw/plugin-sdk/llm";
import {
createSingleUserPromptMessage,
extractNonEmptyAssistantText,
isLiveTestEnabled,
} from "openclaw/plugin-sdk/test-env";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import { buildDeepSeekProvider } from "./provider-catalog.js";
import { createDeepSeekV4ThinkingWrapper } from "./stream.js";
const DEEPSEEK_KEY = process.env.DEEPSEEK_API_KEY ?? "";
const DEEPSEEK_LIVE_MODEL = process.env.OPENCLAW_LIVE_DEEPSEEK_MODEL?.trim() || "deepseek-v4-flash";
const LIVE = isLiveTestEnabled(["DEEPSEEK_LIVE_TEST"]);
const describeLive = LIVE && DEEPSEEK_KEY ? describe : describe.skip;
const ZERO_USAGE: AssistantMessage["usage"] = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
},
};
function forceDeepSeekNonThinkingPath(payload: unknown): void {
if (!payload || typeof payload !== "object") {
return;
}
const request = payload as Record<string, unknown>;
request.thinking = { type: "disabled" };
delete request.reasoning_effort;
}
function resolveDeepSeekLiveModel(): Model<"openai-completions"> {
const provider = buildDeepSeekProvider();
const model = provider.models?.find((entry) => entry.id === DEEPSEEK_LIVE_MODEL);
if (!model) {
throw new Error(`DeepSeek bundled catalog does not include ${DEEPSEEK_LIVE_MODEL}`);
}
return {
provider: "deepseek",
baseUrl: provider.baseUrl,
...model,
api: "openai-completions",
} as Model<"openai-completions">;
}
function resolveDeepSeekV4LiveModel(): Model<"openai-completions"> {
const provider = buildDeepSeekProvider();
const requestedModel =
DEEPSEEK_LIVE_MODEL === "deepseek-v4-flash" || DEEPSEEK_LIVE_MODEL === "deepseek-v4-pro"
? DEEPSEEK_LIVE_MODEL
: "deepseek-v4-flash";
const model = provider.models?.find((entry) => entry.id === requestedModel);
if (!model) {
throw new Error(`DeepSeek bundled catalog does not include ${requestedModel}`);
}
return {
provider: "deepseek",
baseUrl: provider.baseUrl,
...model,
api: "openai-completions",
} as Model<"openai-completions">;
}
describeLive("deepseek plugin live", () => {
it("returns assistant text from the bundled V4 model catalog", async () => {
const res = await completeSimple(
resolveDeepSeekLiveModel(),
{
messages: createSingleUserPromptMessage(),
},
{
apiKey: DEEPSEEK_KEY,
maxTokens: 64,
onPayload: forceDeepSeekNonThinkingPath,
},
);
if (res.stopReason === "error") {
throw new Error(res.errorMessage || "DeepSeek returned error with no message");
}
const text = extractNonEmptyAssistantText(res.content);
expect(text.length).toBeGreaterThan(0);
}, 60_000);
it("accepts V4 thinking replay after a prior provider tool call", async () => {
const toolCallId = "call_deepseek_live_replay_1";
const context: Context = {
messages: [
{
role: "user",
content: "Use the noop tool.",
timestamp: Date.now() - 3,
},
{
role: "assistant",
api: "openai-completions",
provider: "openai",
model: "gpt-5.4",
content: [{ type: "toolCall", id: toolCallId, name: "noop", arguments: {} }],
usage: ZERO_USAGE,
stopReason: "toolUse",
timestamp: Date.now() - 2,
},
{
role: "toolResult",
toolCallId,
toolName: "noop",
content: [{ type: "text", text: "ok" }],
isError: false,
timestamp: Date.now() - 1,
},
{
role: "user",
content: "Reply with exactly: ok",
timestamp: Date.now(),
},
],
tools: [
{
name: "noop",
description: "Return ok.",
parameters: Type.Object({}, { additionalProperties: false }),
},
],
};
let capturedPayload: Record<string, unknown> | undefined;
const streamFn = createDeepSeekV4ThinkingWrapper(streamSimple, "high");
if (!streamFn) {
throw new Error("expected DeepSeek V4 thinking stream wrapper");
}
const stream = streamFn(resolveDeepSeekV4LiveModel(), context, {
apiKey: DEEPSEEK_KEY,
maxTokens: 64,
onPayload: (payload) => {
capturedPayload = payload as Record<string, unknown>;
},
});
const result = await (await stream).result();
if (result.stopReason === "error") {
throw new Error(result.errorMessage || "DeepSeek V4 replay returned error with no message");
}
const messages = capturedPayload?.messages;
expect(Array.isArray(messages)).toBe(true);
const assistantMessage = (messages as Array<Record<string, unknown>>)[1];
expect(assistantMessage?.role).toBe("assistant");
expect(assistantMessage?.reasoning_content).toBe("");
const toolCalls = assistantMessage?.tool_calls;
expect(Array.isArray(toolCalls)).toBe(true);
const toolCall = (toolCalls as Array<Record<string, unknown>>)[0];
expect(toolCall?.id).toBe(toolCallId);
expect(toolCall?.type).toBe("function");
const toolFunction = toolCall?.function as Record<string, unknown> | undefined;
expect(toolFunction?.name).toBe("noop");
expect(toolFunction?.arguments).toBe("{}");
expect(extractNonEmptyAssistantText(result.content).length).toBeGreaterThan(0);
}, 60_000);
it("accepts V4 thinking replay after a prior plain assistant message", async () => {
const context: Context = {
messages: [
{
role: "user",
content: "Say hello.",
timestamp: Date.now() - 2,
},
{
role: "assistant",
api: "openai-completions",
provider: "openai",
model: "gpt-5.4",
content: [{ type: "text", text: "Hello." }],
usage: ZERO_USAGE,
stopReason: "stop",
timestamp: Date.now() - 1,
},
{
role: "user",
content: "Reply with exactly: ok",
timestamp: Date.now(),
},
],
};
let capturedPayload: Record<string, unknown> | undefined;
const streamFn = createDeepSeekV4ThinkingWrapper(streamSimple, "high");
if (!streamFn) {
throw new Error("expected DeepSeek V4 thinking stream wrapper");
}
const stream = streamFn(resolveDeepSeekV4LiveModel(), context, {
apiKey: DEEPSEEK_KEY,
maxTokens: 64,
onPayload: (payload) => {
capturedPayload = payload as Record<string, unknown>;
},
});
const result = await (await stream).result();
if (result.stopReason === "error") {
throw new Error(
result.errorMessage || "DeepSeek V4 plain replay returned error with no message",
);
}
const messages = capturedPayload?.messages;
expect(Array.isArray(messages)).toBe(true);
const assistantMessage = (messages as Array<Record<string, unknown>>)[1];
expect(assistantMessage?.role).toBe("assistant");
expect(assistantMessage?.content).toBe("Hello.");
expect(assistantMessage?.reasoning_content).toBe("");
expect(extractNonEmptyAssistantText(result.content).length).toBeGreaterThan(0);
}, 60_000);
});

View File

@@ -0,0 +1,527 @@
// Deepseek tests cover index plugin behavior.
import type { Context, Model } from "openclaw/plugin-sdk/llm";
import { createAssistantMessageEventStream } from "openclaw/plugin-sdk/llm";
import {
registerSingleProviderPlugin,
resolveProviderPluginChoice,
} from "openclaw/plugin-sdk/plugin-test-runtime";
import { buildOpenAICompletionsParams } from "openclaw/plugin-sdk/provider-transport-runtime";
import { createProviderUsageFetch, makeResponse } from "openclaw/plugin-sdk/test-env";
import { describe, expect, it } from "vitest";
import { runSingleProviderCatalog } from "../test-support/provider-model-test-helpers.js";
import deepseekPlugin from "./index.js";
import { createDeepSeekV4ThinkingWrapper } from "./stream.js";
type OpenAICompletionsModel = Model<"openai-completions">;
type PayloadCapture = {
payload?: Record<string, unknown>;
};
type ThinkingPayload = {
type?: unknown;
};
type ReplayToolCall = {
id?: unknown;
type?: unknown;
function?: {
name?: unknown;
arguments?: unknown;
};
};
type RegisteredProvider = Awaited<ReturnType<typeof registerSingleProviderPlugin>>;
const emptyUsage = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};
function requireThinkingProfileResolver(
provider: RegisteredProvider,
): NonNullable<RegisteredProvider["resolveThinkingProfile"]> {
if (!provider.resolveThinkingProfile) {
throw new Error("DeepSeek provider did not register a thinking profile resolver");
}
return provider.resolveThinkingProfile;
}
const readToolCall = { type: "toolCall", id: "call_1", name: "read", arguments: {} };
const readToolResult = {
role: "toolResult",
toolCallId: "call_1",
toolName: "read",
content: [{ type: "text", text: "ok" }],
isError: false,
timestamp: 3,
};
const readTool = {
name: "read",
description: "Read data",
parameters: { type: "object", properties: {}, required: [], additionalProperties: false },
};
function deepSeekV4Model(id: "deepseek-v4-flash" | "deepseek-v4-pro"): OpenAICompletionsModel {
return {
provider: "deepseek",
id,
name: id === "deepseek-v4-flash" ? "DeepSeek V4 Flash" : "DeepSeek V4 Pro",
api: "openai-completions",
baseUrl: "https://api.deepseek.com",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1_000_000,
maxTokens: 384_000,
compat: {
supportsUsageInStreaming: true,
supportsReasoningEffort: true,
maxTokensField: "max_tokens",
},
} as OpenAICompletionsModel;
}
function replayAssistantMessage(params: {
provider: string;
model: string;
content: Array<Record<string, unknown>>;
stopReason: "stop" | "toolUse";
}) {
return {
role: "assistant",
api: "openai-completions",
provider: params.provider,
model: params.model,
content: params.content,
usage: emptyUsage,
stopReason: params.stopReason,
timestamp: 2,
};
}
function readToolReplayContext(assistantMessage: ReturnType<typeof replayAssistantMessage>) {
return {
messages: [{ role: "user", content: "hi", timestamp: 1 }, assistantMessage, readToolResult],
tools: [readTool],
} as Context;
}
function deepSeekReasoningToolReplayContext() {
return readToolReplayContext(
replayAssistantMessage({
provider: "deepseek",
model: "deepseek-v4-flash",
content: [
{
type: "thinking",
thinking: "call reasoning",
thinkingSignature: "reasoning_content",
},
readToolCall,
],
stopReason: "toolUse",
}),
);
}
function createPayloadCapturingStream(capture: PayloadCapture) {
return (
streamModel: OpenAICompletionsModel,
streamContext: Context,
options?: { onPayload?: (payload: unknown, model: unknown) => unknown },
) => {
capture.payload = buildOpenAICompletionsParams(streamModel, streamContext, {
reasoning: "high",
} as never);
options?.onPayload?.(capture.payload, streamModel);
const stream = createAssistantMessageEventStream();
queueMicrotask(() => stream.end());
return stream;
};
}
function requireThinkingWrapper(
wrapper: ReturnType<typeof createDeepSeekV4ThinkingWrapper>,
label: string,
): NonNullable<ReturnType<typeof createDeepSeekV4ThinkingWrapper>> {
if (!wrapper) {
throw new Error(`expected DeepSeek thinking wrapper for ${label}`);
}
return wrapper;
}
function readThinking(payload: Record<string, unknown> | undefined): ThinkingPayload | undefined {
return payload?.thinking as ThinkingPayload | undefined;
}
function readPayloadMessage(
capture: PayloadCapture,
index: number,
): Record<string, unknown> | undefined {
return (capture.payload?.messages as Array<Record<string, unknown>> | undefined)?.[index];
}
function readFirstToolCall(
message: Record<string, unknown> | undefined,
): ReplayToolCall | undefined {
return (message?.tool_calls as ReplayToolCall[] | undefined)?.[0];
}
describe("deepseek provider plugin", () => {
it("registers DeepSeek with api-key auth wizard metadata", async () => {
const provider = await 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);
if (!resolved) {
throw new Error("expected DeepSeek api-key auth choice");
}
expect(resolved.provider.id).toBe("deepseek");
expect(resolved.method.id).toBe("api-key");
});
it("builds the static DeepSeek model catalog", async () => {
const provider = await registerSingleProviderPlugin(deepseekPlugin);
const catalogProvider = await runSingleProviderCatalog(provider);
expect(catalogProvider.api).toBe("openai-completions");
expect(catalogProvider.baseUrl).toBe("https://api.deepseek.com");
expect(catalogProvider.models?.map((model) => model.id)).toEqual([
"deepseek-v4-flash",
"deepseek-v4-pro",
"deepseek-chat",
"deepseek-reasoner",
]);
const flashModel = catalogProvider.models?.find((model) => model.id === "deepseek-v4-flash");
expect(flashModel?.reasoning).toBe(true);
expect(flashModel?.contextWindow).toBe(1_000_000);
expect(flashModel?.maxTokens).toBe(384_000);
expect(flashModel?.compat?.supportsReasoningEffort).toBe(true);
expect(flashModel?.compat?.maxTokensField).toBe("max_tokens");
expect(
catalogProvider.models?.find((model) => model.id === "deepseek-reasoner")?.reasoning,
).toBe(true);
});
it("resolves API-key usage auth from DeepSeek config sources", async () => {
const provider = await registerSingleProviderPlugin(deepseekPlugin);
await expect(
provider.resolveUsageAuth?.({
env: {},
resolveApiKeyFromConfigAndStore: (options?: { envDirect?: Array<string | undefined> }) => {
expect(options?.envDirect).toEqual([undefined]);
return "config-deepseek-key";
},
} as never),
).resolves.toEqual({ token: "config-deepseek-key" });
});
it("fetches DeepSeek usage balance through the provider hook", async () => {
const provider = await registerSingleProviderPlugin(deepseekPlugin);
const mockFetch = createProviderUsageFetch(async () =>
makeResponse(200, {
is_available: true,
balance_infos: [{ currency: "CNY", total_balance: "8.88", granted_balance: "1.00" }],
}),
);
await expect(
provider.fetchUsageSnapshot?.({
token: "deepseek-key",
timeoutMs: 5000,
fetchFn: mockFetch,
} as never),
).resolves.toMatchObject({
provider: "deepseek",
displayName: "DeepSeek",
windows: [],
summary: "Balance ¥8.88 · Granted ¥1.00",
});
});
it("owns OpenAI-compatible replay policy", async () => {
const provider = await registerSingleProviderPlugin(deepseekPlugin);
const replayPolicy = provider.buildReplayPolicy?.({ modelApi: "openai-completions" } as never);
expect(replayPolicy?.sanitizeToolCallIds).toBe(true);
expect(replayPolicy?.toolCallIdMode).toBe("strict");
expect(replayPolicy?.validateGeminiTurns).toBe(true);
expect(replayPolicy?.validateAnthropicTurns).toBe(true);
});
it("owns DeepSeek tool schema compatibility for MCP union schemas", async () => {
const provider = await registerSingleProviderPlugin(deepseekPlugin);
const mcpTool = {
name: "unusual-whales__get_balance_sheet_screener",
description: "",
parameters: {
type: "object",
properties: {
date: {
anyOf: [{ type: "string" }, { type: "integer" }],
},
period: {
oneOf: [{ type: "string" }, { type: "null" }],
},
},
},
execute: () => undefined,
} as never;
const normalized = provider.normalizeToolSchemas?.({
provider: "deepseek",
modelId: "deepseek-v4-pro",
modelApi: "openai-completions",
model: deepSeekV4Model("deepseek-v4-pro"),
tools: [mcpTool],
} as never);
expect(normalized?.[0]?.parameters).toEqual({
type: "object",
properties: {
date: { type: "string" },
period: { type: "string", nullable: true },
},
});
expect(
provider.inspectToolSchemas?.({
provider: "deepseek",
modelId: "deepseek-v4-pro",
modelApi: "openai-completions",
model: deepSeekV4Model("deepseek-v4-pro"),
tools: normalized ?? [],
} as never),
).toStrictEqual([]);
});
it("advertises max thinking levels for DeepSeek V4 models only", async () => {
const provider = await registerSingleProviderPlugin(deepseekPlugin);
const resolveThinkingProfile = requireThinkingProfileResolver(provider);
const expectedV4Levels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
expect(
resolveThinkingProfile({
provider: "deepseek",
modelId: "deepseek-v4-pro",
} as never)?.levels.map((level) => level.id),
).toEqual(expectedV4Levels);
expect(
resolveThinkingProfile({
provider: "deepseek",
modelId: "deepseek-v4-flash",
} as never)?.defaultLevel,
).toBe("high");
expect(
resolveThinkingProfile({
provider: "deepseek",
modelId: "deepseek-v4-flash",
} as never)?.levels.map((level) => level.id),
).toEqual(expectedV4Levels);
expect(
resolveThinkingProfile({ provider: "deepseek", modelId: "deepseek-chat" } as never),
).toBe(undefined);
expect(
resolveThinkingProfile({ provider: "deepseek", modelId: "deepseek-reasoner" } as never),
).toBe(undefined);
});
it("maps thinking levels to DeepSeek V4 payload controls", async () => {
let capturedPayload: Record<string, unknown> | undefined;
const baseStreamFn = (
_model: Model<"openai-completions">,
_context: Context,
options?: { onPayload?: (payload: unknown) => unknown },
) => {
capturedPayload = {
model: "deepseek-v4-pro",
reasoning_effort: "high",
};
options?.onPayload?.(capturedPayload);
const stream = createAssistantMessageEventStream();
queueMicrotask(() => stream.end());
return stream;
};
const wrapThinkingOff = requireThinkingWrapper(
createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "off"),
"off",
);
await wrapThinkingOff(
{
provider: "deepseek",
id: "deepseek-v4-pro",
api: "openai-completions",
} as never,
{ messages: [] } as never,
{},
);
expect(readThinking(capturedPayload)?.type).toBe("disabled");
expect(capturedPayload).not.toHaveProperty("reasoning_effort");
const wrapThinkingXhigh = requireThinkingWrapper(
createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "xhigh"),
"xhigh",
);
await wrapThinkingXhigh(
{
provider: "deepseek",
id: "deepseek-v4-pro",
api: "openai-completions",
} as never,
{ messages: [] } as never,
{},
);
expect(readThinking(capturedPayload)?.type).toBe("enabled");
expect(capturedPayload?.reasoning_effort).toBe("max");
});
it("preserves replayed reasoning_content when DeepSeek V4 thinking is enabled", async () => {
const capture: PayloadCapture = {};
const model = deepSeekV4Model("deepseek-v4-flash");
const context = deepSeekReasoningToolReplayContext();
const baseStreamFn = createPayloadCapturingStream(capture);
const wrapThinkingHigh = requireThinkingWrapper(
createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "high"),
"high",
);
await wrapThinkingHigh(model, context, {});
expect(readThinking(capture.payload)?.type).toBe("enabled");
expect(capture.payload?.reasoning_effort).toBe("high");
const assistantMessage = readPayloadMessage(capture, 1);
expect(assistantMessage?.role).toBe("assistant");
expect(assistantMessage?.reasoning_content).toBe("call reasoning");
const toolCall = readFirstToolCall(assistantMessage);
expect(toolCall?.id).toBe("call_1");
expect(toolCall?.type).toBe("function");
expect(toolCall?.function?.name).toBe("read");
expect(toolCall?.function?.arguments).toBe("{}");
});
it("adds blank reasoning_content for replayed tool calls from non-DeepSeek turns", async () => {
const capture: PayloadCapture = {};
const model = deepSeekV4Model("deepseek-v4-pro");
const context = readToolReplayContext(
replayAssistantMessage({
provider: "openai",
model: "gpt-5.4",
content: [readToolCall],
stopReason: "toolUse",
}),
);
const baseStreamFn = createPayloadCapturingStream(capture);
const wrapThinkingHigh = requireThinkingWrapper(
createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "high"),
"high",
);
await wrapThinkingHigh(model, context, {});
const assistantMessage = readPayloadMessage(capture, 1);
expect(assistantMessage?.role).toBe("assistant");
expect(assistantMessage?.reasoning_content).toBe("");
const toolCall = readFirstToolCall(assistantMessage);
expect(toolCall?.id).toBe("call_1");
expect(toolCall?.type).toBe("function");
expect(toolCall?.function?.name).toBe("read");
expect(toolCall?.function?.arguments).toBe("{}");
});
it("adds blank reasoning_content for replayed plain assistant messages", async () => {
const capture: PayloadCapture = {};
const model = deepSeekV4Model("deepseek-v4-pro");
const context = {
messages: [
{ role: "user", content: "hi", timestamp: 1 },
replayAssistantMessage({
provider: "openai",
model: "gpt-5.4",
content: [{ type: "text", text: "Hello." }],
stopReason: "stop",
}),
{ role: "user", content: "next", timestamp: 3 },
],
} as Context;
const baseStreamFn = createPayloadCapturingStream(capture);
const wrapThinkingHigh = requireThinkingWrapper(
createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "high"),
"high",
);
await wrapThinkingHigh(model, context, {});
const assistantMessage = readPayloadMessage(capture, 1);
expect(assistantMessage?.role).toBe("assistant");
expect(assistantMessage?.content).toBe("Hello.");
expect(assistantMessage?.reasoning_content).toBe("");
});
it("strips replayed reasoning_content when DeepSeek V4 thinking is disabled", async () => {
const capture: PayloadCapture = {};
const model = deepSeekV4Model("deepseek-v4-flash");
const context = deepSeekReasoningToolReplayContext();
const baseStreamFn = createPayloadCapturingStream(capture);
const wrapThinkingNone = requireThinkingWrapper(
createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "none" as never),
"none",
);
await wrapThinkingNone(model, context, {});
expect(readThinking(capture.payload)?.type).toBe("disabled");
expect(capture.payload).not.toHaveProperty("reasoning_effort");
expect((capture.payload!.messages as Array<Record<string, unknown>>)[1]).not.toHaveProperty(
"reasoning_content",
);
});
it("publishes configured DeepSeek models through plugin-owned catalog augmentation", async () => {
const provider = await registerSingleProviderPlugin(deepseekPlugin);
expect(
provider.augmentModelCatalog?.({
config: {
models: {
providers: {
deepseek: {
models: [
{
id: "deepseek-chat",
name: "DeepSeek Chat",
input: ["text"],
reasoning: false,
contextWindow: 65536,
},
],
},
},
},
},
} as never),
).toEqual([
{
provider: "deepseek",
id: "deepseek-chat",
name: "DeepSeek Chat",
input: ["text"],
reasoning: false,
contextWindow: 65536,
},
]);
});
});

View File

@@ -0,0 +1,68 @@
// Deepseek plugin entrypoint registers its OpenClaw integration.
import { readConfiguredProviderCatalogEntries } from "openclaw/plugin-sdk/provider-catalog-shared";
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools";
import { fetchDeepSeekUsage } from "openclaw/plugin-sdk/provider-usage";
import { applyDeepSeekConfig, DEEPSEEK_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildDeepSeekProvider } from "./provider-catalog.js";
import { createDeepSeekV4ThinkingWrapper } from "./stream.js";
import { resolveDeepSeekV4ThinkingProfile } from "./thinking.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,
},
augmentModelCatalog: ({ config }) =>
readConfiguredProviderCatalogEntries({
config,
providerId: PROVIDER_ID,
}),
matchesContextOverflowError: ({ errorMessage }) =>
/\bdeepseek\b.*(?:input.*too long|context.*exceed)/i.test(errorMessage),
...buildProviderReplayFamilyHooks({
family: "openai-compatible",
dropReasoningFromHistory: false,
}),
...buildProviderToolCompatFamilyHooks("deepseek"),
wrapStreamFn: (ctx) => createDeepSeekV4ThinkingWrapper(ctx.streamFn, ctx.thinkingLevel),
resolveThinkingProfile: ({ modelId }) => resolveDeepSeekV4ThinkingProfile(modelId),
isModernModelRef: ({ modelId }) => Boolean(resolveDeepSeekV4ThinkingProfile(modelId)),
resolveUsageAuth: async (ctx) => {
const apiKey = ctx.resolveApiKeyFromConfigAndStore({
envDirect: [ctx.env.DEEPSEEK_API_KEY],
});
return apiKey ? { token: apiKey } : null;
},
fetchUsageSnapshot: async (ctx) =>
await fetchDeepSeekUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn),
},
});

View File

@@ -0,0 +1,34 @@
// Deepseek plugin module implements models behavior.
import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared";
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import manifest from "./openclaw.plugin.json" with { type: "json" };
const DEEPSEEK_MANIFEST_PROVIDER = buildManifestModelProviderConfig({
providerId: "deepseek",
catalog: manifest.modelCatalog.providers.deepseek,
});
export const DEEPSEEK_BASE_URL = DEEPSEEK_MANIFEST_PROVIDER.baseUrl;
export const DEEPSEEK_MODEL_CATALOG: ModelDefinitionConfig[] = DEEPSEEK_MANIFEST_PROVIDER.models;
export function buildDeepSeekModelDefinition(
model: (typeof DEEPSEEK_MODEL_CATALOG)[number],
): ModelDefinitionConfig {
return {
...model,
api: "openai-completions",
};
}
const DEEPSEEK_V4_MODEL_IDS = new Set(["deepseek-v4-flash", "deepseek-v4-pro"]);
export function isDeepSeekV4ModelId(modelId: string): boolean {
return DEEPSEEK_V4_MODEL_IDS.has(modelId.toLowerCase());
}
export function isDeepSeekV4ModelRef(model: { provider?: string; id?: unknown }): boolean {
return (
model.provider === "deepseek" && typeof model.id === "string" && isDeepSeekV4ModelId(model.id)
);
}

12
extensions/deepseek/npm-shrinkwrap.json generated Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "@openclaw/deepseek-provider",
"version": "2026.6.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/deepseek-provider",
"version": "2026.6.11"
}
}
}

View File

@@ -0,0 +1,32 @@
// Deepseek setup module handles plugin onboarding behavior.
import {
applyAgentDefaultModelPrimary,
applyProviderConfigWithModelCatalog,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import { buildDeepSeekModelDefinition, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL_CATALOG } from "./api.js";
export const DEEPSEEK_DEFAULT_MODEL_REF = "deepseek/deepseek-v4-flash";
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,
);
}

View File

@@ -0,0 +1,138 @@
{
"id": "deepseek",
"icon": "https://cdn.simpleicons.org/deepseek",
"activation": {
"onStartup": false
},
"enabledByDefault": true,
"providerCatalogEntry": "./provider-discovery.ts",
"providers": ["deepseek"],
"providerEndpoints": [
{
"endpointClass": "deepseek-native",
"hosts": ["api.deepseek.com"]
}
],
"providerRequest": {
"providers": {
"deepseek": {
"family": "deepseek"
}
}
},
"modelCatalog": {
"providers": {
"deepseek": {
"baseUrl": "https://api.deepseek.com",
"api": "openai-completions",
"models": [
{
"id": "deepseek-v4-flash",
"name": "DeepSeek V4 Flash",
"reasoning": true,
"input": ["text"],
"contextWindow": 1000000,
"maxTokens": 384000,
"cost": {
"input": 0.14,
"output": 0.28,
"cacheRead": 0.028,
"cacheWrite": 0
},
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": true,
"maxTokensField": "max_tokens"
}
},
{
"id": "deepseek-v4-pro",
"name": "DeepSeek V4 Pro",
"reasoning": true,
"input": ["text"],
"contextWindow": 1000000,
"maxTokens": 384000,
"cost": {
"input": 1.74,
"output": 3.48,
"cacheRead": 0.145,
"cacheWrite": 0
},
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": true,
"maxTokensField": "max_tokens"
}
},
{
"id": "deepseek-chat",
"name": "DeepSeek Chat",
"input": ["text"],
"contextWindow": 131072,
"maxTokens": 8192,
"cost": {
"input": 0.28,
"output": 0.42,
"cacheRead": 0.028,
"cacheWrite": 0
},
"compat": {
"supportsUsageInStreaming": true,
"maxTokensField": "max_tokens"
}
},
{
"id": "deepseek-reasoner",
"name": "DeepSeek Reasoner",
"reasoning": true,
"input": ["text"],
"contextWindow": 131072,
"maxTokens": 65536,
"cost": {
"input": 0.28,
"output": 0.42,
"cacheRead": 0.028,
"cacheWrite": 0
},
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": false,
"maxTokensField": "max_tokens"
}
}
]
}
},
"discovery": {
"deepseek": "static"
}
},
"setup": {
"providers": [
{
"id": "deepseek",
"envVars": ["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 <key>",
"cliDescription": "DeepSeek API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}

View File

@@ -0,0 +1,35 @@
{
"name": "@openclaw/deepseek-provider",
"version": "2026.6.11",
"description": "OpenClaw DeepSeek provider plugin.",
"repository": {
"type": "git",
"url": "https://github.com/openclaw/openclaw"
},
"type": "module",
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"extensions": [
"./index.ts"
],
"install": {
"clawhubSpec": "clawhub:@openclaw/deepseek-provider",
"npmSpec": "@openclaw/deepseek-provider",
"defaultChoice": "npm",
"minHostVersion": ">=2026.6.8"
},
"compat": {
"pluginApi": ">=2026.6.11"
},
"build": {
"openclawVersion": "2026.6.11",
"bundledDist": false
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
}
}

View File

@@ -0,0 +1,15 @@
// Deepseek provider module implements model/runtime integration.
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import {
buildDeepSeekModelDefinition,
DEEPSEEK_BASE_URL,
DEEPSEEK_MODEL_CATALOG,
} from "./models.js";
export function buildDeepSeekProvider(): ModelProviderConfig {
return {
baseUrl: DEEPSEEK_BASE_URL,
api: "openai-completions",
models: DEEPSEEK_MODEL_CATALOG.map(buildDeepSeekModelDefinition),
};
}

View File

@@ -0,0 +1,18 @@
// Deepseek provider module implements model/runtime integration.
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
import { buildDeepSeekProvider } from "./provider-catalog.js";
const deepSeekProviderDiscovery: ProviderPlugin = {
id: "deepseek",
label: "DeepSeek",
docsPath: "/providers/deepseek",
auth: [],
staticCatalog: {
order: "simple",
run: async () => ({
provider: buildDeepSeekProvider(),
}),
},
};
export default deepSeekProviderDiscovery;

View File

@@ -0,0 +1,265 @@
// Deepseek tests cover provider policy api plugin behavior.
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types";
import { describe, expect, it } from "vitest";
import { normalizeConfig, resolveThinkingProfile } from "./provider-policy-api.js";
describe("deepseek provider-policy-api", () => {
it("advertises max thinking levels for DeepSeek V4 models", () => {
const expectedV4Levels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
expect(
resolveThinkingProfile({
provider: "deepseek",
modelId: "deepseek-v4-pro",
})?.levels.map((level) => level.id),
).toEqual(expectedV4Levels);
expect(
resolveThinkingProfile({
provider: "deepseek",
modelId: "deepseek-v4-flash",
})?.defaultLevel,
).toBe("high");
expect(
resolveThinkingProfile({
provider: "deepseek",
modelId: "deepseek-chat",
}),
).toBe(undefined);
expect(
resolveThinkingProfile({
provider: "openrouter",
modelId: "deepseek-v4-pro",
}),
).toBe(null);
});
it("hydrates contextWindow and cost from catalog for known models", () => {
const providerConfig: ModelProviderConfig = {
baseUrl: "https://api.deepseek.com",
api: "openai-completions",
models: [
{
id: "deepseek-v4-flash",
name: "DeepSeek V4 Flash",
reasoning: true,
input: ["text"],
} as never,
],
};
const result = normalizeConfig({ provider: "deepseek", providerConfig });
expect(result).not.toBe(providerConfig);
const model = result.models[0];
expect(model.contextWindow).toBe(1_000_000);
expect(model.maxTokens).toBe(384_000);
expect(model.cost).toEqual({
input: 0.14,
output: 0.28,
cacheRead: 0.028,
cacheWrite: 0,
});
});
it("hydrates deepseek-v4-pro with correct metadata", () => {
const providerConfig: ModelProviderConfig = {
baseUrl: "https://api.deepseek.com",
api: "openai-completions",
models: [
{
id: "deepseek-v4-pro",
name: "DeepSeek V4 Pro",
reasoning: true,
input: ["text"],
} as never,
],
};
const result = normalizeConfig({ provider: "deepseek", providerConfig });
const model = result.models[0];
expect(model.contextWindow).toBe(1_000_000);
expect(model.maxTokens).toBe(384_000);
expect(model.cost).toEqual({
input: 1.74,
output: 3.48,
cacheRead: 0.145,
cacheWrite: 0,
});
});
it("hydrates deepseek-chat with 131k context", () => {
const providerConfig: ModelProviderConfig = {
baseUrl: "https://api.deepseek.com",
api: "openai-completions",
models: [
{
id: "deepseek-chat",
name: "DeepSeek Chat",
reasoning: false,
input: ["text"],
} as never,
],
};
const result = normalizeConfig({ provider: "deepseek", providerConfig });
const model = result.models[0];
expect(model.contextWindow).toBe(131_072);
});
it("preserves explicit user contextWindow override", () => {
const providerConfig: ModelProviderConfig = {
baseUrl: "https://api.deepseek.com",
api: "openai-completions",
models: [
{
id: "deepseek-v4-flash",
name: "DeepSeek V4 Flash",
reasoning: true,
input: ["text"],
contextWindow: 500_000,
} as never,
],
};
const result = normalizeConfig({ provider: "deepseek", providerConfig });
const model = result.models[0];
expect(model.contextWindow).toBe(500_000);
// cost should still be hydrated since it was missing
expect(model.cost).toEqual({
input: 0.14,
output: 0.28,
cacheRead: 0.028,
cacheWrite: 0,
});
});
it("preserves explicit user cost override", () => {
const userCost = { input: 99, output: 99, cacheRead: 99, cacheWrite: 99 };
const providerConfig: ModelProviderConfig = {
baseUrl: "https://api.deepseek.com",
api: "openai-completions",
models: [
{
id: "deepseek-v4-flash",
name: "DeepSeek V4 Flash",
reasoning: true,
input: ["text"],
cost: userCost,
} as never,
],
};
const result = normalizeConfig({ provider: "deepseek", providerConfig });
const model = result.models[0];
expect(model.cost).toEqual(userCost);
// contextWindow should still be hydrated since it was missing
expect(model.contextWindow).toBe(1_000_000);
});
it("preserves explicit user maxTokens override", () => {
const providerConfig: ModelProviderConfig = {
baseUrl: "https://api.deepseek.com",
api: "openai-completions",
models: [
{
id: "deepseek-v4-flash",
name: "DeepSeek V4 Flash",
reasoning: true,
input: ["text"],
maxTokens: 100_000,
} as never,
],
};
const result = normalizeConfig({ provider: "deepseek", providerConfig });
const model = result.models[0];
expect(model.maxTokens).toBe(100_000);
});
it("returns providerConfig unchanged when all models already have metadata", () => {
const providerConfig: ModelProviderConfig = {
baseUrl: "https://api.deepseek.com",
api: "openai-completions",
models: [
{
id: "deepseek-v4-flash",
name: "DeepSeek V4 Flash",
reasoning: true,
input: ["text"],
contextWindow: 1_000_000,
maxTokens: 384_000,
cost: { input: 0.14, output: 0.28, cacheRead: 0.028, cacheWrite: 0 },
} as never,
],
};
const result = normalizeConfig({ provider: "deepseek", providerConfig });
expect(result).toBe(providerConfig);
});
it("passes through unknown model ids unchanged", () => {
const providerConfig: ModelProviderConfig = {
baseUrl: "https://api.deepseek.com",
api: "openai-completions",
models: [
{
id: "deepseek-custom-finetune",
name: "Custom Fine-tune",
reasoning: false,
input: ["text"],
} as never,
],
};
const result = normalizeConfig({ provider: "deepseek", providerConfig });
expect(result).toBe(providerConfig);
});
it("returns providerConfig unchanged when models array is empty", () => {
const providerConfig: ModelProviderConfig = {
baseUrl: "https://api.deepseek.com",
api: "openai-completions",
models: [],
};
const result = normalizeConfig({ provider: "deepseek", providerConfig });
expect(result).toBe(providerConfig);
});
it("hydrates only the models that need it in a mixed list", () => {
const providerConfig: ModelProviderConfig = {
baseUrl: "https://api.deepseek.com",
api: "openai-completions",
models: [
{
id: "deepseek-v4-flash",
name: "DeepSeek V4 Flash",
reasoning: true,
input: ["text"],
contextWindow: 1_000_000,
maxTokens: 384_000,
cost: { input: 0.14, output: 0.28, cacheRead: 0.028, cacheWrite: 0 },
} as never,
{
id: "deepseek-v4-pro",
name: "DeepSeek V4 Pro",
reasoning: true,
input: ["text"],
} as never,
],
};
const result = normalizeConfig({ provider: "deepseek", providerConfig });
expect(result).not.toBe(providerConfig);
// First model should be unchanged (same reference)
expect(result.models[0]).toBe(providerConfig.models[0]);
// Second model should be hydrated
expect(result.models[1].contextWindow).toBe(1_000_000);
expect(result.models[1].cost).toEqual({
input: 1.74,
output: 3.48,
cacheRead: 0.145,
cacheWrite: 0,
});
});
});

View File

@@ -0,0 +1,105 @@
// Deepseek API module exposes the plugin public contract.
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types";
import { DEEPSEEK_MODEL_CATALOG } from "./models.js";
import { resolveDeepSeekV4ThinkingProfile } from "./thinking.js";
type ModelDefinitionDraft = Partial<ModelDefinitionConfig> &
Pick<ModelDefinitionConfig, "id" | "name">;
/**
* Build a lookup from the bundled DeepSeek model catalog so we can hydrate
* missing metadata (contextWindow, cost, maxTokens) into user-configured
* model rows without overwriting explicit overrides.
*/
function buildCatalogIndex(): Map<string, ModelDefinitionConfig> {
const index = new Map<string, ModelDefinitionConfig>();
for (const model of DEEPSEEK_MODEL_CATALOG) {
index.set(model.id, model);
}
return index;
}
function isPositiveNumber(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value) && value > 0;
}
function hasCostValues(cost: unknown): cost is ModelDefinitionConfig["cost"] {
if (!cost || typeof cost !== "object") {
return false;
}
const c = cost as Record<string, unknown>;
return (
typeof c.input === "number" ||
typeof c.output === "number" ||
typeof c.cacheRead === "number" ||
typeof c.cacheWrite === "number"
);
}
/**
* Provider policy surface for DeepSeek.
*
* Hydrates missing `contextWindow`, `cost`, and `maxTokens` from the bundled
* catalog for matching model ids. Explicit user overrides are preserved.
*/
export function normalizeConfig(params: {
provider: string;
providerConfig: ModelProviderConfig;
}): ModelProviderConfig {
const { providerConfig } = params;
if (!Array.isArray(providerConfig.models) || providerConfig.models.length === 0) {
return providerConfig;
}
const catalog = buildCatalogIndex();
let mutated = false;
const nextModels = providerConfig.models.map((model) => {
const raw = model as ModelDefinitionDraft;
const catalogEntry = catalog.get(raw.id);
if (!catalogEntry) {
return model;
}
let modelMutated = false;
const patched: Record<string, unknown> = {};
// Hydrate contextWindow from catalog when missing or not a positive number.
if (!isPositiveNumber(raw.contextWindow) && isPositiveNumber(catalogEntry.contextWindow)) {
patched.contextWindow = catalogEntry.contextWindow;
modelMutated = true;
}
// Hydrate maxTokens from catalog when missing or not a positive number.
if (!isPositiveNumber(raw.maxTokens) && isPositiveNumber(catalogEntry.maxTokens)) {
patched.maxTokens = catalogEntry.maxTokens;
modelMutated = true;
}
// Hydrate cost from catalog when missing or when all fields are zero/absent.
if (!hasCostValues(raw.cost) && hasCostValues(catalogEntry.cost)) {
patched.cost = catalogEntry.cost;
modelMutated = true;
}
if (!modelMutated) {
return model;
}
mutated = true;
return { ...raw, ...patched };
});
if (!mutated) {
return providerConfig;
}
return { ...providerConfig, models: nextModels as ModelDefinitionConfig[] };
}
export function resolveThinkingProfile(params: { provider: string; modelId: string }) {
return params.provider.trim().toLowerCase() === "deepseek"
? resolveDeepSeekV4ThinkingProfile(params.modelId)
: null;
}

View File

@@ -0,0 +1,15 @@
// Deepseek plugin module implements stream behavior.
import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry";
import { createDeepSeekV4OpenAICompatibleThinkingWrapper } from "openclaw/plugin-sdk/provider-stream-shared";
import { isDeepSeekV4ModelRef } from "./models.js";
export function createDeepSeekV4ThinkingWrapper(
baseStreamFn: ProviderWrapStreamFnContext["streamFn"],
thinkingLevel: ProviderWrapStreamFnContext["thinkingLevel"],
): ProviderWrapStreamFnContext["streamFn"] {
return createDeepSeekV4OpenAICompatibleThinkingWrapper({
baseStreamFn,
thinkingLevel,
shouldPatchModel: isDeepSeekV4ModelRef,
});
}

View File

@@ -0,0 +1,20 @@
// Deepseek plugin module implements thinking behavior.
import type { ProviderThinkingProfile } from "openclaw/plugin-sdk/plugin-entry";
import { isDeepSeekV4ModelId } from "./models.js";
const V4_THINKING_LEVEL_IDS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
function buildDeepSeekV4ThinkingLevel(id: (typeof V4_THINKING_LEVEL_IDS)[number]) {
return { id };
}
const DEEPSEEK_V4_THINKING_PROFILE = {
levels: V4_THINKING_LEVEL_IDS.map(buildDeepSeekV4ThinkingLevel),
defaultLevel: "high",
} satisfies ProviderThinkingProfile;
export function resolveDeepSeekV4ThinkingProfile(
modelId: string,
): ProviderThinkingProfile | undefined {
return isDeepSeekV4ModelId(modelId) ? DEEPSEEK_V4_THINKING_PROFILE : undefined;
}

View File

@@ -0,0 +1,16 @@
{
"extends": "../tsconfig.package-boundary.base.json",
"compilerOptions": {
"rootDir": "."
},
"include": ["./*.ts", "./src/**/*.ts"],
"exclude": [
"./**/*.test.ts",
"./dist/**",
"./node_modules/**",
"./src/test-support/**",
"./src/**/*test-helpers.ts",
"./src/**/*test-harness.ts",
"./src/**/*test-support.ts"
]
}