共享库,不是同一个 harness
Pi 是独立 coding harness;pi-ai 是它的模型层;DSH 只复用 pi-ai,不引入 Pi 的 CLI/TUI/工具。
DSH 为什么把 pi-ai 放进自己的 LLM seam?两者之间到底传了什么?为什么不是 LiteLLM、OpenRouter 或 Vercel AI SDK?这份报告从源码、仓库设计说明和同类框架的边界出发,把“数据怎么流、对象怎么映射、为什么这样选”讲清楚。
Pi 是独立 coding harness;pi-ai 是它的模型层;DSH 只复用 pi-ai,不引入 Pi 的 CLI/TUI/工具。
DSH 的 provider-neutral 请求,经 pi-ai 的原生 provider 流,再翻译回 DSH 的块事件。
DSH 的治理在自己的 credential/session/retry 层;它需要的是可嵌入的 TS provider 库。
先把最常见的误解拆掉:dsh-llm-pi-ai 不是 Pi harness,也不是“DSH 内置了 Pi”。它只是 DSH 的一个 adapter,把 pi-ai 这个多 provider LLM 库接进 DSH 的 dsh-llm seam。
@earendil-works/pi-coding-agent@earendil-works/pi-ai@deepseek-ai/dshPi 是 Earendil Works 的 minimal/extensible coding agent;monorepo 里还有 pi-agent-core、pi-tui、pi-durable 等。
不是 harness,也不是网关;它是被 Pi 和 DSH 共同复用的 provider/LLM 库。
DSH 只依赖 pi-ai,不依赖 pi-coding-agent/pi-agent-core;Pi 的 CLI/TUI/工具不会进入 DSH。
Pi 和 DSH 是并列的 harness;它们共享 pi-ai 这个模型层,但不是 fork,也不是同一个运行时。 dsh-llm-pi-ai 是 DSH 侧的 adapter,负责把 pi-ai 的 Context / AssistantMessageEvent / Model 转成 DSH 的 GenerateOptions / StreamChunk / ReplayEnvelope。
证据:DSH 的 packages/llm/llm-pi-ai/package.json 只声明 @earendil-works/pi-ai,没有 pi-coding-agent 或 pi-agent-core;其 description 明确写的是 “design-verification twin of dsh-llm-deepseek”。
DSH 的请求不是“把 messages 直接丢给 pi-ai”。中间有 provider-neutral 预处理、能力校验、凭据解析、图片投影、Context 转换、事件回译和 replay 存储。
DSH 的 dsh-llm 后面不是只有一个 adapter:dsh-llm-deepseek 直连 DeepSeek Messages(默认 route:deepseek-official / deepseek-account);dsh-llm-pi-ai 通过 pi-ai catalog 接三方 provider(OpenAI / Anthropic / Google / …)。本文的 §02–§04 以 dsh-llm-pi-ai 为主线,DeepSeek 直连分支在同一个 seam 下走另一套 adapter。
GenerateOptions,deep-freezeContextAssistantMessageEvent → StreamChunkreplayState 存进 source,下一轮恢复签名/ID图中的 5–8 步是 dsh-llm-pi-ai 的路径;dsh-llm-deepseek 走 DeepSeek Messages,不经过 toPiContext/toStreamChunks,但共享同一个 dsh-llm seam 和 StreamChunk 契约。
Agent loop 从 session 里取出历史,构造“模型将看到的精确输入”。DSH 的 GenerateOptions 同时包含请求身份、消息、工具和采样控制:
interface GenerateOptions {
provider: string
model: string
reasoningEffort?: ReasoningEffortId
messages: RequestMessage[] // system / developer / user / assistant / tool
system?: string // one-shot 调用
tools?: ToolSchema[]
toolHistory?: ToolHistory
temperature?: number
maxTokens?: number
stop?: string[]
sessionId?: SessionId
purpose?: 'compaction' | 'session-title'
signal?: AbortSignal
}
messages 里的内容块是 DSH 的 canonical ContentBlock:text、reasoning、image、file、tool-call、tool-addition、tool-removal。这份输入会被 deep-freeze,adapter 只读。
在进入 adapter 前,LlmRuntime.adapterStream() 做四类统一处理:
file block 永远不直接发给 provider,转成带路径/大小的文本句柄;toolUpdate 能力选择 in-history / addition-only / 完整列表;source.provider 不是当前 adapter 拥有的 provider,就剥掉 replayState,避免跨 adapter 使用 provider 签名。这一步是 DSH 的“provider-neutral 边界”:所有 provider 看到的输入先被规范化,但 reasoning/cache 等 provider 语义不被强行翻译。
每个请求会取当前 profiles 的不可变 snapshot(profiles + pi-ai Models collection)。配置变更会构建新 snapshot,不会修改飞行中的请求;这样 prepareCall() 冻结的配置能一直保持到 dispatch。
// 简化自 adapter.ts
const profile = snapshot.profiles.get(options.provider)
const model = snapshot.models.getModel(options.provider, options.model)
const reasoning = resolveReasoningLevel(model, options.reasoningEffort ?? profile.reasoning)
const apiKey = await this.config.resolveApiKey(options.provider, profile)
const events = snapshot.models.streamSimple(model, context, {
apiKey,
reasoning,
...options.temperature !== undefined ? { temperature: options.temperature } : {},
...options.maxTokens !== undefined ? { maxTokens: options.maxTokens } : {},
...options.sessionId !== undefined ? { sessionId: String(options.sessionId) } : {},
signal: watchdog.signal,
headers,
maxRetries: 0, // 重试归 DSH
})
凭据解析是“fail loud”的:如果 profile 明确写了 apiKeyEnv,解析不到就抛 MISSING_CREDENTIAL,不会让 pi-ai 去捡一个不相关的 OPENAI_API_KEY。只有完全没配凭据的 route 才交给 pi-ai 的 provider-native ambient auth / OAuth store。
toPiContext() 把 DSH 的 role/content 结构翻译成 pi-ai 的 Context:
system + 开头连续的 system 历史 → Context.systemPrompt;messages;tool-call → pi-ai toolCall(arguments 从 DSH 的原始 JSON 字符串解析成对象);tool → pi-ai toolResult,toolName 从前一条 assistant 的 tool call 建立 id→name 映射补上;developer 的 tool-addition/tool-removal → pi-ai system 的 toolsAdded/toolsRemoved(或模型不支持时降级);ImageContent;文件永远只以文本句柄出现。models.streamSimple() 是 pi-ai 的统一简化入口。它内部按模型的 api 选择协议实现:anthropic-messages、openai-responses、openai-completions、google-generative-ai、bedrock-converse-stream 等。协议差异由 pi-ai 的 provider 层处理,DSH 只看到统一的 AssistantMessageEvent。
// 简化映射
text_start → { type: 'block-start', blockType: 'text' }
text_delta → { type: 'text-delta', text }
thinking_start → { type: 'block-start', blockType: 'reasoning' }
thinking_delta → { type: 'reasoning-delta', text }
toolcall_start → { type: 'block-start', blockType: 'tool-call' }
toolcall_delta → { type: 'tool-call-delta', argumentsDelta }
toolcall_end → { type: 'block-end', block: { type:'tool-call', arguments: JSON.stringify(...) } }
done → { type: 'usage' } + { type: 'finish', replayState }
error → { type: 'usage' } + { type: 'finish', reason: error/aborted }
这里有一个重要方向差异:pi-ai 的 tool arguments 是已解析对象,DSH 的 tool-call 保留原始 JSON 字符串。所以 toolcall_end 会重新 JSON.stringify;参数非法时 DSH 会报 MALFORMED_RESPONSE(除非是 length 截断)。
BlockAssembler 把 StreamChunk 拼成 DSH 的 content blocks;finish.replayState 被存进 assistant message 的 source.replayState。下一次请求只有在同一个 adapter 实例同时拥有历史 provider 和目标 provider 时,才会把 replay state 传给 adapter;否则剥离并降级为 provider-neutral 历史。
DSH 和 pi-ai 各有一套完整的类型系统。理解 adapter 的关键,是看清两边的对象如何一一对应,以及哪些字段是“语义损失点”。
provider / model / messages / system / tools / toolHistory / reasoningEffort / temperature / maxTokens / stop / sessionId / purpose / signal
role: system / developer / user / assistant / tool;block: text / reasoning / image / file / tool-call / tool-addition / tool-removal
block-start / text-delta / reasoning-delta / tool-call-delta / block-end / usage / finish
response + blocks:adapter-private 的原生回放元数据,DSH 只存不解释
systemPrompt / messages / tools
user / assistant / toolResult / system
content: text / thinking / toolCall;usage、stopReason、responseId、responseModel、providerThinkingLevel、签名
start / text_* / thinking_* / toolcall_* / done / error
provider/id/api/baseUrl/reasoning/input/contextWindow/maxTokens/thinkingLevelMap/compat;Models 提供 streamSimple()
| DSH | pi-ai | 转换要点 |
|---|---|---|
| GenerateOptions | Context + SimpleStreamOptions | 消息进 Context,采样/凭据/传输进 options |
| Message / ContentBlock | Context.messages / ContentBlock | role 和 block 语义基本对齐,但 toolResult 需要补 name |
| ToolSchema | Tool | name/description/parameters 直接映射 |
| StreamChunk | AssistantMessageEvent | 事件一一对应,但 tool arguments 类型不同 |
| TokenUsage | Usage | pi-ai 把 reasoning 折进 output;cache 只在非零时出现 |
| FinishReason | AssistantMessage.stopReason | stop/toolUse/length/error/aborted 映射;deferred → error |
| ReplayEnvelope | AssistantMessage 原生字段 | 只保存 responseId / signatures / thoughtSignature / namespace |
| LlmResolvedModelInfo | Model | contextWindow、input、reasoning、thinkingLevelMap、compat |
DSH 保留模型产出的 raw JSON string;pi-ai 的 toolCall.arguments 是 parsed object。adapter 在 toolcall_end 重新 stringify。非法 JSON 在 DSH 侧是 MALFORMED_RESPONSE。
DSH 的 tool 消息只有 toolCallId;pi-ai 的 toolResult 需要 toolName。adapter 从前一条 assistant 的 tool call 建立 id→name 映射补上。
pi-ai 把底层 Error 压平成 error.message,丢掉 cause。DSH 的 stream.ts 里有 XXX(pi-ai upstream) 注释,承认只能正则识别 AUTH / RATE_LIMIT / TIMEOUT / TRANSPORT。
DSH 对 pi-ai 打 patch,去掉每个 delta 对整个累计 JSON 的重复解析;否则多 MB 参数流会卡事件循环。这是库级集成的真实代价。
DSH 的 durable 内容(text / reasoning / tool-call)是 source of truth;pi-ai 的 replay metadata 只负责恢复“原生保真度”,不是重新存一份 assistant message。
AssistantMessage + usage + stopReasonassistant.source.replayStatetoPiAssistant 校验并恢复原生 AssistantMessageReplay envelope 有两半:
api、provider、model、responseId、responseModel、providerThinkingLevel、stopReason;textSignature、thinkingSignature、redacted、toolCall.thoughtSignature、namespace。如果 replay state 缺失、版本不匹配、provider/model 不匹配或 block 数量/类型不一致,DSH 会把它降级成 provider-neutral 历史,并记 warning,而不是让请求失败。这样既保住了“能跑”,又尽量保住“原生签名”。
这也解释了 DSH 为什么不把“所有 provider 都归一化成 Chat Completions”:一旦归一化,thinking signature / responseId / thoughtSignature 就没有地方存,长时程工具循环的质量会下降。
这一节把“仓库明说的”与“我基于代码结构的推断”分开。前者可以在 DSH 的 README、Agent Note、源码注释里找到;后者是工程判断,不是官方声明。
DSH 同时有 dsh-llm-deepseek(直连 fetch)和 dsh-llm-pi-ai(库实现)。两套不同内部实现跑同一个 StreamChunk 契约,用来验证“provider-neutral”不是只适配了 DeepSeek 的假抽象。
catalog route 继承 pi-ai 的 endpoint、protocol、model catalog;OAuth 登录、credential store、环境变量发现都不需要 DSH 自己为每个 provider 重写。
openai-completions / openai-responses / anthropic-messages。Bedrock/Vertex/Azure/Codex 的认证流程无法用“key + baseURL + models”完整表达,所以走各自 catalog provider。
dormant 的 dsh-llm-pi-ai 不 import pi-ai aggregate entry,避免把 TypeBox 和数百个模块带进每次启动;协议实现走 api/*.lazy。
pi-ai 调用设 maxRetries: 0;DSH 的 dsh-llm-retry 在 agent step 边界重试。adapter 一次调用就是一次 SDK attempt。
pi-ai 对不支持 mid-conversation system 的模型会把后续 system 拼到初始 prompt,破坏 prefix cache;DSH 改成 <system-reminder> user 消息,保留原生 system 的模型则不动。
DSH 仓库对 pi-ai 的流式 tool-call 参数解析打 patch,去掉每个 delta 的整段 JSON 重复解析(O(n²))。
DSH 只存 pi-ai 的原生签名/ID,不存完整 assistant message;跨 adapter 剥离 replay,失效时降级而不是失败。
prepareCall() 要冻结一次调用的 adapter generation;pi-ai 的 Models collection 可以整体替换,adapter 用不可变 snapshot 包住,正好匹配。pi-ai 升级要重新验证 patch、错误文本分类、provider 行为;DSH 的 patch 是集成成本的一部分。
pi-ai 把底层 Error 压平成 message,DSH 只能靠正则识别 401/429/5xx/timeout;provider 改文案就会误分类。
没有 load balancing、fallback、central quota、guardrails;DSH 只做单次调用,路由/重试在更上层。
pi-ai 的模型目录/协议支持变化会影响 DSH 的模型选择;DSH 用 patch/版本 pin 控制。
pi-ai 既不是 LiteLLM Proxy,也不是 OpenRouter,更不是 LangChain。先把“库 / 网关 / 托管 / agent 框架”分开,再比较会清楚很多。
| 类别 | 代表 | 形态 | 适合 |
|---|---|---|---|
| 进程内多 provider 库 | pi-ai、Vercel AI SDK、LiteLLM SDK、aisuite、any-llm | 代码依赖,直接调用 provider | 嵌入 harness/应用,低延迟,自己控制运行时 |
| 自托管 HTTP 网关 | LiteLLM Proxy、Portkey、Bifrost、TensorZero | 独立服务,统一 API | 团队级 key/配额/路由/观测/guardrails |
| 托管网关 | OpenRouter、Vercel AI Gateway、Cloudflare AI Gateway | SaaS 代理 | 快速接大量模型,免运维 |
| Agent 框架 | LangChain、LlamaIndex、Pydantic AI、Mastra | 编排/agent 抽象 | 构建应用逻辑,不是模型传输层 |
DSH 是本地优先的 coding harness,自己的治理边界在 credential / session / approval / retry 层;它不需要再套一个中心网关。pi-ai 作为 in-process 库,正好补“多 provider 接入”,又不和 DSH 的运行时抢职责。
| 框架 | 形态/语言 | 协议策略 | 优点 | 代价/限制 | DSH 匹配 |
|---|---|---|---|---|---|
| pi-ai | TS 库 | 每个 provider 走 native protocol;统一 Context / event | provider 目录全、OAuth/认证/模型元数据、类型好、可嵌进程、MIT | 不是网关;无路由/LB/guardrails;错误 cause 丢失;需维护 patch | 最匹配 |
| LiteLLM | Python SDK + Proxy(新版 Rust core) | 主要 OpenAI 格式,支持 native passthrough | 100+ provider、成本追踪、路由/fallback、guardrails、企业功能 | 独立 Python 服务;翻译层可能丢 reasoning/cache;运维复杂 | 服务端可选 |
| Vercel AI SDK | TS 库 | provider packages 走 native API;统一 LanguageModel / stream parts | UI/agent 生态强、provider 多、TS 类型好、Apache-2.0 | 更偏应用/UI;默认 Vercel AI Gateway;不是 provider catalog/auth 库 | 可选替代 |
| OpenRouter | 托管网关 | 主 API 是 OpenAI Chat;另有 Anthropic Messages / Responses(stateless) | 400+ 模型、路由/fallback、免运维、统一账单 | 托管、额外网络跳、数据合规、无本地/离线 | 可当 route |
| Portkey | 自托管/托管网关(MIT) | OpenAI-compatible,1600+ 模型 | guardrails、缓存、路由、观测、企业部署 | 独立服务;翻译层;不是 TS 库 | 团队网关 |
| Bifrost | Go 网关(Apache-2.0) | OpenAI-compatible,10k+ 模型 | 高性能(<200µs)、failover、语义缓存、MCP | 独立服务;Go 运维;翻译层 | 团队网关 |
| TensorZero | Rust 网关 + LLMOps(Apache-2.0) | OpenAI-compatible | 观测/评估/优化/实验一体化 | 偏 LLMOps 平台;独立服务 | 团队实验 |
| aisuite | Python 轻量库(MIT) | 统一 Chat Completions + Agents | 简单、易学、provider 覆盖 | 功能少、无 gateway/路由;Python | Python 应用 |
| any-llm | Python 库(Apache-2.0) | 单一接口 | 轻量、provider 覆盖、Mozilla 生态 | 无 gateway/路由;Python | Python 应用 |
| LangChain / LlamaIndex | Python/TS 编排框架 | 各自 model wrapper | 生态、RAG/agent 组件 | 不是 wire-level 传输层;抽象重 | 不是同类 |
pi-ai:TS 库,嵌进程;provider catalog / auth / model metadata / 原生流事件;无服务、无网关治理。
LiteLLM:Python SDK + Proxy;统一网关、路由、成本、guardrails、企业治理;以 OpenAI 格式为主,支持 native passthrough。
选择:本地/桌面 harness → pi-ai;团队统一入口/配额/审计 → LiteLLM。
两者都是 TS 进程内库。AI SDK 更上层,偏应用/UI/agent,默认 Vercel AI Gateway,provider 包很多;pi-ai 更底层,重点是 provider catalog、认证/OAuth、模型元数据、原生流事件。
选择:DSH 已有自己的 agent loop/UI/session,pi-ai 的边界更窄、更贴合 dsh-llm;如果是 Next.js 应用,AI SDK 更顺手。
这些是网关/服务,不是库:多一跳网络、多一套运维、可能翻译协议;适合团队统一治理,不适合本地优先的 DSH 内核。
选择:DSH 可以把它们当成一个自定义 OpenAI-compatible route,但不会把它们嵌进进程。
这两者都是 Python 轻量统一接口,适合 Python 应用快速接多家 provider;但没有 DSH 需要的 TS 类型、provider catalog、OAuth、原生 replay 元数据和进程内插件语义。
选择:Python 应用 → aisuite/any-llm;TS harness → pi-ai/AI SDK。
| 需求 | 首选 | 次选 | 不要选 |
|---|---|---|---|
| 本地/桌面 coding harness,TS,进程内 | pi-ai | Vercel AI SDK | LiteLLM Proxy / OpenRouter |
| 团队统一 key、配额、审计、路由 | LiteLLM Proxy / Portkey | Bifrost | pi-ai(没有治理层) |
| 快速接 400+ 模型、免运维 | OpenRouter | Vercel AI Gateway | 自建 provider 适配 |
| Python 应用快速统一接口 | LiteLLM SDK / aisuite | any-llm | pi-ai(TS) |
| LLM 实验/评估/优化 | TensorZero | LiteLLM + 自建观测 | 纯库 |
| Next.js/React AI 应用 | Vercel AI SDK | AI Gateway | pi-ai(缺少 UI 层) |
dsh-llm,DSH 自己保留 agent loop、工具、session、审批、重试。ReplayEnvelope 存回 DSH,下一轮再恢复。要“把模型接进自己的 harness”——选 pi-ai/AI SDK;要“把模型接给整个团队”——选 LiteLLM/Portkey/Bifrost;要“最快接最多模型”——选 OpenRouter;要“做 agent 应用”——选 AI SDK/LangChain。DSH 属于第一种。
版本提示:DSH 仍是开发预览;本文按当前安装的 @deepseek-ai/dsh 0.2.1-rc.2 与 @earendil-works/pi-ai 1.4.0 的包文档/代码核对。框架对比中的“模型数量/性能”含厂商自述,生产前请按锁定版本复核。