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
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:
12
extensions/deepseek/README.md
Normal file
12
extensions/deepseek/README.md
Normal 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.
|
||||
8
extensions/deepseek/api.ts
Normal file
8
extensions/deepseek/api.ts
Normal 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";
|
||||
233
extensions/deepseek/deepseek.live.test.ts
Normal file
233
extensions/deepseek/deepseek.live.test.ts
Normal 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);
|
||||
});
|
||||
527
extensions/deepseek/index.test.ts
Normal file
527
extensions/deepseek/index.test.ts
Normal 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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
68
extensions/deepseek/index.ts
Normal file
68
extensions/deepseek/index.ts
Normal 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),
|
||||
},
|
||||
});
|
||||
34
extensions/deepseek/models.ts
Normal file
34
extensions/deepseek/models.ts
Normal 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
12
extensions/deepseek/npm-shrinkwrap.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
32
extensions/deepseek/onboard.ts
Normal file
32
extensions/deepseek/onboard.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
138
extensions/deepseek/openclaw.plugin.json
Normal file
138
extensions/deepseek/openclaw.plugin.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
35
extensions/deepseek/package.json
Normal file
35
extensions/deepseek/package.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
15
extensions/deepseek/provider-catalog.ts
Normal file
15
extensions/deepseek/provider-catalog.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
18
extensions/deepseek/provider-discovery.ts
Normal file
18
extensions/deepseek/provider-discovery.ts
Normal 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;
|
||||
265
extensions/deepseek/provider-policy-api.test.ts
Normal file
265
extensions/deepseek/provider-policy-api.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
105
extensions/deepseek/provider-policy-api.ts
Normal file
105
extensions/deepseek/provider-policy-api.ts
Normal 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;
|
||||
}
|
||||
15
extensions/deepseek/stream.ts
Normal file
15
extensions/deepseek/stream.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
20
extensions/deepseek/thinking.ts
Normal file
20
extensions/deepseek/thinking.ts
Normal 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;
|
||||
}
|
||||
16
extensions/deepseek/tsconfig.json
Normal file
16
extensions/deepseek/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user