前言¶
已经用 Vercel AI SDK 搭好应用的人,编排入口通常是 generateText / streamText:换模型只换 provider,工具、流式输出和取消信号都走同一套接口。DeepSeek Harness(命令名 dsh)走的是另一条路——它是 DeepSeek AI 开源的智能体运行时,官方仓库 deepseek-ai/deepseek-harness 写明核心理念是「一切皆插件」:agent 循环、工具、技能、会话都可以在配置层组合。两边各管各的,常见结果是:要么把业务迁进 dsh,要么自己再写一层进程封装。
社区包 ai-sdk-provider-dsh 做的事情比较直接:把一份 dsh 运行时藏到 AI SDK 的 LanguageModel 后面。调用方式和普通语言模型一样,实际跑起来的是 harness 里的 agent 循环,工具也在 harness 内部执行。仓库 README 把它类比为 ai-sdk-provider-claude-code 驱动 Claude Code 的方式,编排面仍留在 AI SDK。
社区里还有一份独立的插件目录 deepseek-harness-plugin.com。它和 DeepSeek / 幻方没有官方从属关系,不能当成官方应用商店。本文按该目录详情页、GitHub 仓库 README / package.json / 源码,以及 npm 页面交叉核对后整理。
这是什么¶
ai-sdk-provider-dsh 是一款社区开源的 AI SDK provider,由 krislavten 维护,仓库是 krislavten/ai-sdk-provider-dsh,许可证 MIT,主要语言 TypeScript。目录页把它归在「模型与提供方」,收录日期 2026-08-14。npm 当前版本是 0.2.0(CHANGELOG 记在 2026-08-14;npm 发布时间 2026-08-13)。截至本文查阅时,GitHub 星标为 2(目录页当时显示 1,以仓库为准)。
它解决的是这一类任务:
- 现有应用已经用 AI SDK 编排,但希望调用 dsh 的完整 agent,而不是只打一发 chat completion
- 需要 bash、读写文件、subagent、会话持久化这些 harness 能力,同时不想换掉
streamText这一层 - 希望同一份构建同时跑在 AI SDK v6 和 v7 上
实现上,每个 provider 实例会惰性拉起一个 dsh 子进程,双方走 stdio JSON-RPC(dsh SDK 协议)。doGenerate / doStream 把 AI SDK 的 LanguageModelV3CallOptions 转成 dsh prompt,再把运行时的 session.event 映射回 AI SDK 的流式分片。
有两点需要先说清楚。目录页给出的安装命令是 dsh plugin add github:krislavten/ai-sdk-provider-dsh;核对 package.json 后,这个包没有声明 dsh.bundle。README 的主路径是 npm install ai-sdk-provider-dsh:在 Node 应用里拉起捆绑好的运行时,而不是往现有 dsh 进程里再挂一张模型卡片。下文两套命令都会写,用法以仓库 README 为准。
核心功能¶
一份构建覆盖 AI SDK v6 与 v7¶
源码里的语言模型实现 specificationVersion: 'v3',走的是 AI SDK 的 LanguageModelV3 接口。README 给出的兼容矩阵如下:
| AI SDK | @ai-sdk/provider |
状态 |
|---|---|---|
ai@^6 |
@ai-sdk/provider@^3 |
支持 |
ai@^7 |
@ai-sdk/provider@^4 |
支持(v7 把 V3 模型当作一等公民) |
package.json 的 peer 依赖与上表一致:ai 为 ^6.0.0 || ^7.0.0,@ai-sdk/provider 为 ^3.0.0 || ^4.0.0。运行时要求 Node.js >=22.19,模块格式只有 ESM。
运行时打进包里,版本钉死¶
dsh 目前仍是开发者预览(0.1.0-rc.x),官方 README 写明会有破坏性变更。这个 provider 把 harness 家族精确钉在 0.1.0-rc.6,依赖列表里是 @deepseek-ai/dsh-sdk-jsonrpc-demo、@deepseek-ai/dsh-bash-local、@deepseek-ai/dsh-tool-fs 这类包的精确版本,而不是版本范围。README 的态度是:升级 pin 必须是显式决定,不能靠 range 漂移。
包内自带默认组合 runtime/cordis.yml,以及 dsh-jsonrpc-agent 可执行入口。创建一个 provider 实例就会拉起一份能工作的运行时,不必再单独安装 dsh CLI。
默认组合暴露的模型侧工具是:
- bash(前台执行,默认组合里关闭了后台 run)
- read / write / edit(本地文件系统)
- subagent
- todo_write
另外还有 JSONL 会话持久化,以及自动上下文压缩。工具在 harness 内部执行:流里会出现 providerExecuted: true 的 tool 分片,AI SDK 不会再执行一遍。这也是它和普通「模型 provider」最大的差别——你调的是一个带工具的 agent,不是裸模型。
默认 cordis.yml 里 skills.enabled 为 false。技能若要启用,走的是 dsh 原生机制:从 .dsh/skills、.agents/skills、$DSH_HOME/skills 发现 SKILL.md;README 写明 不会套用 reskill 的 skills.json / skills.lock。目录简介里提到的 MCP,在这份默认组合里没有对应插件,不能当成开箱即用。
多轮会话、元数据与错误分类¶
同一个 provider 实例对应一个运行时子进程。sessionId 默认是新 UUID;写成固定值后,后续调用会继续同一条 harness 会话(运行时持久化 session log)。仓库 examples/multi-turn.ts 用「第一轮写入暗号、第二轮再问」做过端到端验证。
每次响应会在 providerMetadata['dsh'] 下带:
| 字段 | 含义 |
|---|---|
sessionId |
这次调用跑在哪条 harness 会话上 |
turnId |
最后观察到的轮次号(可选) |
terminalReason |
结束类型不是 completed 时的原因:aborted、error、max-tokens、blocked、interrupted |
取元数据的位置因大版本而异:AI SDK v7 看 result.finalStep.providerMetadata(streamText 则 await stream.finalStep);v6 看 result.providerMetadata。
运行时边界上的失败会归类成 AI SDK 的 APICallError,消息里会附一段清洗过的 stderr 尾部。传输断开、请求超时可重试;协议违例、运行时拒绝请求不可重试。选 deepseek-official 却没有 DEEPSEEK_API_KEY 时,会尽快抛出映射到 LoadAPIKeyError 的错误,而不是默默吐空输出(回放模式 DSH_SNAPSHOT_FILE 除外)。
安装与启用¶
目录页上的安装命令原文如下,在 DeepSeek Harness 终端里运行:
dsh plugin add github:krislavten/ai-sdk-provider-dsh
如需可复现安装,目录页给出的写法是固定 commit 哈希:
dsh plugin add github:krislavten/ai-sdk-provider-dsh#commit
把 commit 换成实际哈希即可。插件以当前 dsh 进程的权限运行,安装时可能执行代码。安装前请检查源代码仓库和许可证。
对「在 AI SDK 应用里调用 dsh」这条主路径,仓库 README 写的是 npm:
npm install ai-sdk-provider-dsh
凭据走运行时环境变量:
export DEEPSEEK_API_KEY=sk-...
export DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_API_KEY 必填(走 deepseek-official 时)。DEEPSEEK_BASE_URL 可选,指向任意 OpenAI 兼容网关都可以。
Linux 上默认组合依赖 node-pty,安装时现场编译,没有预构建包。最小容器、缺少 libc6-dev 的 WSL 等环境编不过去时,改用包内的 runtime/cordis.minimal.yml(无 bash)。package.json 还把 node-pty 和 @deepseek-ai/node-addon-landlock-run 列进了 onlyBuiltDependencies,安装阶段会跑原生构建,需要同样谨慎。
典型用法¶
下面的示例均来自仓库 README 与 examples/,把 import 路径换成已发布包名即可。用完后调用 await dsh.close() 拆掉子进程(幂等);拆卸顺序是 EOF → SIGTERM → SIGKILL。
AI SDK v7:streamText¶
v7 用 instructions 写系统说明:
import { streamText } from "ai";
import { createDsh } from "ai-sdk-provider-dsh";
const dsh = createDsh({
runtime: { provider: "deepseek-official", model: "deepseek-v4-flash" },
});
const result = streamText({
model: dsh.languageModel("deepseek-v4-flash"),
instructions: "You are a coding agent.",
prompt: "run the tests",
});
const text = await result.text;
console.log(text);
createDsh 返回的对象可以 dsh.languageModel("deepseek-v4-flash"),也可以直接 dsh("deepseek-v4-flash"),这是 AI SDK provider 的惯例别名。runtime.provider 与 runtime.model 都会在运行时握手时传给 dsh;provider 可以是 deepseek-official,也可以是 pi-ai 目录里的路由。
AI SDK v6:字段名不同¶
v6 没有 instructions,系统说明字段叫 system:
import { streamText } from "ai";
import { createDsh } from "ai-sdk-provider-dsh";
const dsh = createDsh({
runtime: { provider: "deepseek-official", model: "deepseek-v4-flash" },
});
const result = streamText({
model: dsh.languageModel("deepseek-v4-flash"),
system: "You are a coding agent.",
prompt: "run the tests",
});
generateText¶
一次性取完整文本:
import { generateText } from "ai";
import { createDsh } from "ai-sdk-provider-dsh";
const dsh = createDsh({
runtime: { provider: "deepseek-official", model: "deepseek-v4-flash" },
});
const { text } = await generateText({
model: dsh.languageModel("deepseek-v4-flash"),
prompt: "say hello",
});
固定 sessionId 做多轮¶
仓库示例 examples/multi-turn.ts 的核心是:同一个 provider、同一个 sessionId,第二轮能读到第一轮写入的上下文。
import { streamText } from "ai";
import { createDsh } from "ai-sdk-provider-dsh";
const dsh = createDsh({
runtime: {
provider: "deepseek-official",
model: "deepseek-v4-flash",
sessionId: "example-session-1",
},
});
async function ask(prompt: string): Promise<string> {
const result = streamText({
model: dsh.languageModel("deepseek-v4-flash"),
prompt,
});
return (await result.text).trim();
}
const t1 = await ask("The secret code is X7Q9. Reply: stored");
const t2 = await ask("What is the secret code? Reply with only the code.");
不传 sessionId 时,每个 provider 实例仍会复用自己的子进程,但会话 id 是新的。需要跨调用记住上下文时,显式传入固定 id。
换一份最小运行时¶
不能编 node-pty 时,把 configPath 指到包内的最小组合:
runtime: {
provider: "deepseek-official",
model: "deepseek-v4-flash",
configPath: require.resolve("ai-sdk-provider-dsh/runtime/cordis.minimal.yml"),
}
package.json 的 exports 里有 ./runtime/* 子路径,所以 require.resolve 这一写法是包正式支持的。常用覆盖项还有:cwd(默认 process.cwd())、env(默认继承 process.env,可传 DSH_CWD、DSH_SESSION_ROOT)、maxTokens(根 agent 每次请求的输出 token 上限)。
读错误里的 stderr¶
import { generateText } from "ai";
import { createDsh, getErrorMetadata, isAPICallError } from "ai-sdk-provider-dsh";
try {
await generateText({
model: dsh.languageModel("deepseek-v4-flash"),
prompt: "Hello!",
});
} catch (error) {
if (isAPICallError(error)) {
console.error(getErrorMetadata(error)?.stderr);
console.error("retryable:", error.isRetryable);
}
}
适用场景与注意事项¶
比较适合:
- 已经用 AI SDK 做编排,想把 dsh 当作「会跑工具的语言模型」嵌进去
- 需要 harness 侧的 bash / 文件编辑 / subagent / 会话日志,同时保留
generateText/streamText的调用面 - 同一套代码要同时兼容 AI SDK v6 与 v7
不适合当成「把任意 AI SDK tools 丢给 dsh 执行」的胶水。README 写明:AI SDK 的 tools / toolChoice 不会由 AI SDK 执行;工具要改 cordis.yml 或 $DSH_* 环境变量。temperature、topP、topK、stopSequences、seed 这些采样参数会被接口收下,但不会转发给 harness,采样由 harness 自己管。
其他已经写进 README 的边界:
- 只有 ESM,Node.js 必须
>=22.19 - SDK 线上没有「回合中途取消」:abort 会拒绝当前这次调用,子进程和 session log 还在,后续轮次可以继续;要拆进程请
dsh.close() - dsh 仍是开发者预览,破坏性变更是发布政策的一部分;provider 版本和 harness 家族(当前
0.1.0-rc.6)都应该显式钉住 - 默认运行时在 Linux 上需要能编译
node-pty;不行就改cordis.minimal.yml(没有 bash)
无论走目录里的 dsh plugin add,还是走 npm,安装阶段都可能执行构建脚本和原生插件。装之前看源码和许可证;要可复现,把 git 安装钉到 commit,把 npm 依赖钉到具体版本。
小结¶
ai-sdk-provider-dsh 把 dsh 运行时封装成 LanguageModelV3:AI SDK 这一侧还是熟悉的 generateText / streamText,另一侧是钉在 0.1.0-rc.6 的完整 agent 循环。工具在 harness 里执行,会话可以靠固定 sessionId 续上,错误会收成 AI SDK 认识的 APICallError。
它解决的是编排面统一,不是给 dsh Web UI 再加一个模型提供方。默认组合能用 bash 和文件工具,技能和 MCP 都不是开箱状态。dsh 还在预览期,版本要自己盯。
目录页:https://deepseek-harness-plugin.com/zh-CN/plugins/ai-sdk-provider-dsh/
GitHub:https://github.com/krislavten/ai-sdk-provider-dsh
npm:https://www.npmjs.com/package/ai-sdk-provider-dsh