Chat Completions
兼容层与最大公约数。简单、生态最广,但没有 reasoning 连续性、hosted tools 和 server state。
一份合并版报告:先用五层框架讲清 Chat Completions、Responses、Anthropic Messages 的能力差异,再用同一条任务走完三次 trace;最后把 DSH × pi-ai 的数据流、核心对象、同类框架与选型依据合并进来。
兼容层与最大公约数。简单、生态最广,但没有 reasoning 连续性、hosted tools 和 server state。
OpenAI 的 agent runtime。Items、hosted tools、server state、encrypted reasoning;Codex 的唯一协议。
Claude 的强类型 agent 协议。Content blocks、signed thinking、cache_control;Claude Code 的唯一协议。
不是三家厂商随意发明三套 JSON,而是 agent 的需求把“聊天 API”推向了“agent runtime”。Chat Completions 解决的是“给模型一段消息、拿回一段文本”;Responses 和 Messages 解决的是“让模型连续地思考、调工具、保持状态、被审计”。
messages 数组 + function calling + SSE delta。简单、无状态,迅速成为事实标准,几乎所有模型和网关都兼容它。
顶层 system、content blocks、tool_use/tool_result、stop_reason。Claude Code 后来把这套协议变成 coding agent 的事实接口。
Items、previous_response_id、hosted tools、encrypted reasoning、semantic SSE。OpenAI 明确建议新项目用 Responses,Codex 随后只支持 Responses。
Codex 只认 Responses,Claude Code 只认 Messages;第三方 harness 转向 provider-native + OpenAI-compatible 兜底;Gemini Interactions GA。
Chat Completions 是 RPC,Responses 是 OpenAI 的 agent runtime,Messages 是 Claude 的 agent runtime。前者把“模型动作”压回 message;后两者把 tool call、reasoning、state、hosted tools 做成一等公民。这个分层决定了它们的能力上限,而不只是字段命名不同。
Chat Completions 被 OpenAI、Anthropic、Gemini 以及无数网关实现。它的价值是“谁都能接”,代价是只能表达所有模型的最小公倍数。
工具循环变长、reasoning 需要跨请求保持、hosted tools 出现、上下文需要压缩。Responses 和 Messages 各自把这些需求写进 wire format。
OpenAI 的 Codex 只支持 Responses,Anthropic 的 Claude Code 只支持 Messages。协议不再是中立标准,而是 harness 与模型生态的绑定面。
背景判断基于 2026-10-25 的官方文档:OpenAI 的 Migrate to Responses 明确“Chat Completions 仍支持,但新项目推荐 Responses”;Claude Code 的 gateway 文档只列 Anthropic Messages / Bedrock InvokeModel / Vertex rawPredict 三种格式。
先看结构差异,再看能力差异。表中“Chat”指 OpenAI Chat Completions;“Resp”指 OpenAI Responses;“Msg”指 Anthropic Messages。所有结论以 2026-10 的官方文档为准。
| 维度 | Chat Completions | Responses | Messages |
|---|---|---|---|
| 定位 | 通用聊天 RPC / 兼容层 | OpenAI agent runtime | Claude 强类型 agent 协议 |
| 端点 | /v1/chat/completions | /v1/responses | /v1/messages |
| 请求模型 | messages[],role 扁平 | input 为 string 或 Item[] | messages[] + 顶层 system,content blocks |
| 工具调用 | assistant tool_calls → role:"tool" | function_call Item → function_call_output Item | tool_use block → tool_result block |
| 终止信号 | finish_reason(tool_calls / stop) | output 含 function_call;status | stop_reason(tool_use / end_turn / pause_turn) |
| 状态 | 无状态,客户端重发全部历史 | 可选 server state(previous_response_id / Conversations) | 无状态,客户端重发;beta compaction |
| Reasoning | 无正式 round-trip | encrypted reasoning Items | thinking block + signature |
| 流式 | 统一 chunk delta + [DONE] | 语义化 response.* 事件 | 命名 SSE:message_start / content_block_* / message_stop |
| 缓存 | 隐式;GPT-5.6+ 可显式 breakpoint | 隐式 + 显式 breakpoint / key | cache_control 自动或最多 4 个显式 breakpoint |
| 内置工具 | 无 hosted tools | 最丰富:search / file / code / computer / MCP / shell 等 | 丰富:web / code / computer / editor / MCP connector 等 |
| 结构化输出 | response_format | text.format | output_config.format |
| 多模态 | 图片 / 音频 / 文件 | 图片 / 文件(音频按模型核实) | 图片 / PDF / 文档 |
| 长任务 | 无 | background + WebSocket | 流式 + pause_turn;无 background |
| 兼容性 | 事实标准,几乎人人支持 | OpenAI/Codex 系 + 部分网关 | Claude 系 + 网关 + 自托管 |
| 版本策略 | 稳定但 agent 能力冻结;新项目不推荐 | 快速演进;部分能力按模型/账号灰度 | anthropic-version + beta headers;2026-10-21 有破坏性变更 |
| 最大风险 | 能力上限低,前沿 OpenAI 模型工具循环受限 | OpenAI 绑定强,默认存储/复杂度高 | 前缀/thinking 约束硬,beta 多,版本变更快 |
Chat Completions 不能把模型上一轮的 thinking 原样带回;Responses 可以带 encrypted reasoning item;Messages 可以带签名 thinking block。对短对话影响不大,对长时程 coding agent 是质量分水岭。
能力差异不是一个表能讲完的。按五层看:L1 表面形状、L2 控制流、L3 状态、L4 质量、L5 系统。越往下,越接近真正的选型。
这是最容易看到的一层,也是最多人停的一层。三种协议都能“发一条消息、拿回一段文本”,但它们的“基本单位”不同:Chat 是 message,Responses 是 Item,Messages 是 content block。
| 维度 | Chat Completions | Responses | Messages |
|---|---|---|---|
| 请求单位 | messages[],role 扁平 | input:string 或 Item[] | messages[] + 顶层 system,content blocks |
| 响应单位 | choices[].message | output: Item[] | content: Block[] |
| 模型动作 | assistant message 里的 tool_calls | 单独的 function_call Item | 单独的 tool_use block |
| 状态表达 | 历史全在 messages 里 | Items + 可选 response id | messages + thinking signature |
看懂字段不代表能写 agent。真正的差异在下一层:工具循环由谁驱动、终止信号是什么、结果放在哪里。
三种协议都是“模型发起调用 → 客户端执行 → 结果回传 → 模型继续”,但每一步的载体和终止信号不同。写错终止条件,就会死循环或提前结束。
| 环节 | Chat Completions | Responses | Messages |
|---|---|---|---|
| 模型发起 | assistant tool_calls[];finish_reason:"tool_calls" | function_call Item;call_id 关联 | tool_use block;stop_reason:"tool_use" |
| 执行 | 客户端 | 客户端函数 + 可选 hosted tools | 客户端函数 + 可选 server tools |
| 回传 | role:"tool" + tool_call_id | function_call_output Item + call_id | user message 里的 tool_result block + tool_use_id |
| 继续/终止 | finish_reason != "tool_calls" | output 里不再出现 function_call;status | stop_reason != "tool_use";end_turn |
关键差异:Chat 把“调用”和“结果”压进 message,循环最简单;Responses 把模型动作拆成 Item,能表达 hosted tools 和 reasoning;Messages 把 assistant turn 拆成 block,要求整个 turn 原样回传,thinking 不能被丢掉。
失败模式:Chat 把 finish_reason 当文本结束、Responses 把 output[0] 当文本、Messages 只回传 tool_result 而丢掉 thinking —— 三种都会让循环“看起来能跑,实际不对”。
这是从“能跑”到“能长跑”的分水岭。状态归属决定了你能不能压缩、分支、重写历史、做 ZDR,也决定了缓存和 reasoning 连续性。
| 维度 | Chat Completions | Responses | Messages |
|---|---|---|---|
| 默认状态 | 客户端 | 服务端(可 store:false) | 客户端 |
| 续接方式 | 重发全部 messages | previous_response_id / Conversations / 手工回放 | 重发全部 messages |
| 历史编辑 | 完全可控 | stateful 模式受限;stateless 可控 | 可控,但 preserved thinking 要求 append-only |
| ZDR/驻留 | 最容易 | 需显式 store:false + 加密 reasoning | 最容易 |
| failover | 最容易,但丢 reasoning | 需回放 Item;reasoning 不可跨 provider | 需处理 thinking 签名,跨 provider 不可用 |
非显然的判断:server state 不是“更先进”,而是“把复杂度从客户端搬到服务端”。它简化回放、提高缓存,但带来 retention/ZDR/数据驻留/历史不可编辑的约束。客户端状态更可控,但压缩、分支、重写历史的代码要自己写。
这一层开始解释“为什么同样模型,换协议效果会变”。决定性因素是 reasoning 能否跨轮回传、缓存能否稳定命中、流式事件是否足以驱动工具进度。
| 协议 | Reasoning 载体 | 回传要求 | 典型失败 |
|---|---|---|---|
| Chat | 无正式载体(仅 usage 计数) | 无 | 多轮后推理不连续;新 OpenAI 模型工具调用受限 |
| Responses | reasoning Item,含 encrypted_content / summary | 与 function_call 一起回传;不要漏、不要改 | 工具循环质量下降;与手工回放混用导致上下文重复 |
| Messages | thinking block + signature | 工具回合内完整原样回传;跨回合建议全部回传 | 修改/重排 400;prefix 变化导致 preserved thinking 失效 |
OpenAI Chat Completions 核心只支持 reasoning 模型与 reasoning_effort:reasoning token 会产生、会计费,但不会作为内容返回,也没有跨轮状态回传;reasoning summary 只在 Responses 里提供。OpenAI-compatible 厂商(DeepSeek、OpenRouter、vLLM 等)常在 Chat 形状上增加 reasoning_content / reasoning_details 扩展,这才让 Chat 能“看到”推理内容;但字段名、流式 delta、是否必须回传都因厂商而异,不是 OpenAI 标准。
为什么这是胜负手:reasoning 是 provider-scoped、model-family-scoped 的不透明状态。它不能翻译成另一家的字段,也不能在 fallback 时复用。短对话看不出来,长工具循环会暴露质量差距。
| 协议 | 机制 | 控制粒度 | 典型 TTL |
|---|---|---|---|
| Chat | 隐式;GPT-5.6+ 支持显式 breakpoint | prompt_cache_key / prompt_cache_options | 30m(新模型);旧模型 in_memory/24h |
| Responses | 隐式 + 显式 breakpoint | prompt_cache_key / prompt_cache_options | 30m/12h(模型相关) |
| Messages | 自动或 block 级 cache_control | 最多 4 个显式 breakpoint | 5m/1h |
非显然的判断:缓存不是“省钱技巧”,而是“历史不可变”的约束。你一旦用显式缓存,就必须保证 tools/system/thinking 配置稳定、历史 append-only;否则命中率会掉,甚至 prefix 失效。
| 协议 | 事件模型 | 文本 | 工具参数 | Reasoning |
|---|---|---|---|---|
| Chat | 统一 chunk | delta.content | tool_calls[].function.arguments 片段 | 无 |
| Responses | 语义 response.* 事件 | response.output_text.delta | response.function_call_arguments.delta | reasoning_summary_text.delta |
| Messages | 命名 block 事件 | text_delta | input_json_delta | thinking_delta + signature_delta |
非显然的判断:流式不是 UI 细节。语义事件决定你能不能做工具进度、reasoning 展示、断线恢复;chunk 最好写,但信息最少。
最后一层是架构:协议不仅是“怎么调模型”,还决定你能不能把 search/code/computer/MCP 交给厂商、能不能做后台任务、能不能跨 provider 迁移。
| 能力 | Chat Completions | Responses | Messages |
|---|---|---|---|
| Hosted tools | 无 | 最丰富:search/file/code/computer/MCP/shell | 丰富:web/code/computer/editor/MCP connector |
| 后台/长任务 | 无 | background + WebSocket | 流式 + pause_turn;无 background |
| 可移植性 | 最高 | 较低(OpenAI 绑定) | 中等(Claude 生态强) |
| 治理边界 | harness 自己 | provider 可代跑工具,信任边界外移 | provider 可代跑 server tools |
| 能力 | Chat | Responses | Messages |
|---|---|---|---|
| Reasoning 连续性 | 弱 | 强 | 强 |
| 工具循环保真 | 中 | 强 | 强 |
| 状态/长任务 | 弱 | 强 | 中 |
| 缓存控制 | 中 | 强 | 强 |
| Hosted tools | 弱 | 强 | 强 |
| 可移植性 | 强 | 弱 | 中 |
| 实现复杂度 | 低 | 高 | 高 |
抽象讨论容易失真。这里用一个最小任务把三种协议各跑一遍:用户说“读一下 a.py”,模型调用 read_file,工具返回内容,模型继续回答。重点看每一步的载体、终止信号和状态。
{
"model": "gpt-...",
"messages": [
{ "role": "user",
"content": "Read a.py" }
],
"tools": [{
"type": "function",
"function": {
"name": "read_file",
"parameters": { "type": "object" }
}
}]
}
{
"model": "gpt-...",
"input": [
{ "role": "user",
"content": "Read a.py" }
],
"tools": [{
"type": "function",
"name": "read_file",
"parameters": { "type": "object" }
}]
}
{
"model": "claude-...",
"max_tokens": 1024,
"system": "You are a coding agent.",
"messages": [{
"role": "user",
"content": "Read a.py"
}],
"tools": [{
"name": "read_file",
"input_schema": { "type": "object" }
}]
}
| 协议 | 完整响应里的调用 | 终止信号 | 流式事件 |
|---|---|---|---|
| Chat | choices[0].message.tool_calls[0]:{id, function:{name, arguments}} | finish_reason:"tool_calls" | delta.tool_calls[].function.arguments 片段 |
| Responses | output[] 中的 function_call:{call_id, name, arguments} | output 中出现 function_call | response.function_call_arguments.delta |
| Messages | content[] 中的 tool_use:{id, name, input} | stop_reason:"tool_use" | input_json_delta(partial JSON) |
细节:Chat 的 arguments 是 JSON 字符串,Responses 也是字符串,Messages 的 input 是对象;但流式时三者都可能只给出 partial JSON。不要在第一个 delta 就解析。
| 协议 | 回传消息 | 关联键 | 注意 |
|---|---|---|---|
| Chat | 新增 {"role":"tool","tool_call_id":"call_1","content":"..."} | tool_call_id | 一条 tool message 对应一个调用 |
| Responses | 新增 {"type":"function_call_output","call_id":"call_1","output":"..."} | call_id | reasoning Item 要一起回传 |
| Messages | user message 里加 {"type":"tool_result","tool_use_id":"toolu_1","content":"..."} | tool_use_id | assistant 的 thinking + tool_use 原样保留 |
| 协议 | 继续生成 | 最终响应 | 状态变化 |
|---|---|---|---|
| Chat | 追加 tool message 后再次请求 | choices[0].message.content | finish_reason:"stop" |
| Responses | previous_response_id 或回放完整 Item 历史 | output[] 里的 message / output_text | status:"completed" |
| Messages | 把 assistant content + tool_result 回传 | content[] 里的 text | stop_reason:"end_turn" |
| 步骤 | Chat | Responses | Messages |
|---|---|---|---|
| 模型动作 | message.tool_calls | function_call Item | tool_use block |
| 工具结果 | role:"tool" | function_call_output Item | tool_result block |
| 终止信号 | finish_reason | status / output | stop_reason |
| Reasoning | 没有 | reasoning Item | thinking block + signature |
| 历史编辑 | 完全可控 | stateful 受限 / stateless 可控 | append-only 最稳 |
| 翻译 | 通常能保留 | 通常丢失/降级 |
|---|---|---|
| Chat → Responses | 文本、工具调用/结果、大部分 schema | signed/encrypted reasoning、hosted tools、server state、语义事件 |
| Responses → Chat | 文本、函数调用 | reasoning Item、built-in tools、previous_response_id、compaction |
| Messages → Responses | 文本、工具调用/结果 | thinking signature、cache_control、server tools、beta 字段 |
| 任意 → Chat | 文本、函数调用 | reasoning 状态、缓存控制、hosted tools、长任务语义 |
不要只测“最终文本对不对”。至少测:工具调用 ID 是否稳定、partial JSON 是否能拼完整、thinking/reasoning 是否原样回传、终止事件是否识别、缓存 prefix 是否被意外改写、失败后重试是否重复执行工具。
这一节只留判断,不重复事实;完整 harness/版本事实在附录 A 和 B。真正难的不是“知道字段”,而是知道在什么约束下放弃什么。
协议不是自由变量。Codex 只能 Responses、Claude Code 只能 Messages;选错 harness,协议就固定了。反过来,选模型路线也会排除一批 harness。
行动:先列“必须支持的模型 + 必须用的 harness”,再看协议;不要先画一个“统一协议”架构。
Chat 适合简单聊天、分类、RAG、旧应用、自托管入口。对“推理模型 + 长工具循环”,它的能力上限已经被 OpenAI 自己的新模型限制封死。
行动:把 Chat 放在边界,不要放在 agent 核心;需要 agent 时优先 Responses/Messages。
server state 简化回放、提升缓存,但带来 retention/ZDR/数据驻留/历史不可编辑。client state 更可控,但要自己写压缩、分支、回写。
行动:先问“历史能不能编辑/删除/驻留”,再决定 state 归属;不要默认 server state。
跨协议翻译能保留文本和工具调用,但很难保留 signed/encrypted reasoning、cache 前缀和 hosted tools。短对话看不出,长任务会暴露。
行动:网关优先 native passthrough;翻译只用于非推理/短对话;fallback 不要把 reasoning 当可复用状态。
同时服务 Codex + Claude Code 的网关,必须原生提供 /v1/responses 和 /v1/messages;只做 Chat 翻译会同时丢两端的能力。
行动:入口按客户端协议分流,出口按 provider native;把“是否翻译”作为可观测字段。
| 你的约束 | 优先选择 | 避免 |
|---|---|---|
| OpenAI 前沿模型 + 长工具循环 | Responses | Chat 作为主协议 |
| Claude 生态 + thinking/cache | Messages | Chat 翻译层接 Claude Code |
| 多 provider、简单聊天/RAG | Chat 兼容层 | 用 Chat 统一所有 agent |
| 同时服务 Codex + Claude Code | 原生 Responses + Messages 双入口 | 只做 Chat 网关 |
| ZDR / 历史可编辑 | 客户端状态 / store:false | 默认 server state |
| 长上下文/成本敏感 | 支持 prefix cache 且 append-only | 每轮重写 system/tools |
选择 = 模型路线 × harness 协议墙 × 工作负载 × 合规/成本。四个约束里先排除不可选项,再在剩下的里选“保真度最高、翻译最少”的方案。
完整 20 行 harness 事实表在附录 A。正文只留三条规则、架构分类和“为什么”,避免把“事实”当“分析”。
wire_api 只接受 "responses";只实现 Chat Completions 的网关无法服务 Codex。这不是“偏好”,是硬约束。
Claude Code / Agent SDK 只接受 Anthropic Messages、Bedrock InvokeModel、Vertex rawPredict;没有 OpenAI 格式模式。
同时服务 Codex 和 Claude Code 的网关,必须原生提供 /v1/responses 和 /v1/messages。只做 Chat 翻译,Codex 直接 404,Claude Code 拿不到完整 thinking/cache 语义。
| 架构 | 代表 | 默认协议 | 代价 |
|---|---|---|---|
| 厂商自有 | Codex、Claude Code、Gemini CLI、Antigravity | 自家 native(Responses / Messages / generateContent) | 协议墙、模型锁定 |
| 多 provider native | Cline、OpenCode、Kilo、Zed、Crush、Goose | 按 provider native;自定义端点 Chat | 适配代码多;Gemini Interactions 支持薄 |
| Chat-normalizing | Aider、OpenHands、SWE-agent(LiteLLM) | harness 入口 Chat | reasoning/cache/hosted tools 受翻译层限制 |
| provider-neutral | DSH | DeepSeek route Messages;其他 provider 经 pi-ai native | adapter 多;依赖 pi-ai |
这一章是合并进来的 DSH 深潜。先纠正一个常见画法:DSH 的 LLM 流不是“HTTP Chat/Responses/Messages 进来”,也不是“adapter = DeepSeek Messages / pi-ai”。正确结构是:agent loop → dsh-llm → adapter 选择 → 两条分支。
dsh-llm-deepseek直连 DeepSeek Messages(https://api.deepseek.com/anthropic + /v1/messages);默认 route deepseek-official / deepseek-account。baseURL 也可指向 Messages-compatible 网关。
dsh-llm-pi-ai通过 pi-ai catalog 接三方 provider 的 native protocol(OpenAI Responses / Anthropic Messages / Google generateContent 等);自定义 route 只开 3 种协议。
下面 §7.1–§7.7 以 dsh-llm-pi-ai 为主线;dsh-llm-deepseek 共享同一个 dsh-llm seam 和 StreamChunk 契约,但不经过 toPiContext / toStreamChunks。
先把最常见的误解拆掉: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 / …)。本章 §7.2–§7.4 以 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 的包文档/代码核对。框架对比中的“模型数量/性能”含厂商自述,生产前请按锁定版本复核。
把前面的分析压缩成可执行的判断。先按“模型路线 / harness 协议墙 / 工作负载”分流,再看合规和成本。
/v1/responses 和 /v1/messages;只做 Chat 翻译服务不了两堵协议墙。
/v1/responses;想跑 Claude Code 就提供 /v1/messages;注意本地实现多为 stateless/兼容子集。
store:false + 加密 reasoning,或自托管网关;不要把 server state 当默认。
/v1/responses;读 output: Item[],不要读 choices[0]。response_format → text.format;工具定义扁平化为 {type:"function", name, parameters, strict}。role:"tool" 改为 function_call_output Item,tool_call_id → call_id。response.* 语义事件。previous_response_id / Conversations;ZDR 用 store:false 并保留 encrypted reasoning。AgentEvent/ToolCall/ToolResult/ReasoningState,但保留原始 opaque payload。encodeRequest、decodeStreamEvent、stateMode、capabilities。previous_response_id 就不接受 stateful 请求;不支持 cache_control 就明确降级。正文只留判断,事实放这里。包括完整 harness 表、版本事实、Gemini Interactions、术语和来源。
2026-10 快照;闭源产品协议不可观测,表中标注了证据边界。此表是事实,不是选型建议。
| Harness | 默认 / 硬性协议 | 其他协议与说明 | 选择要点 |
|---|---|---|---|
| 厂商自有 harness | |||
| OpenAI Codex CLI | Responses only | 内置 OpenAI/Ollama/LM Studio 也只能走 Responses 端点(推断;本地服务需提供 /v1/responses) | 协议墙;Chat-only 网关不可用 |
| Claude Code / Agent SDK | Messages only | Bedrock InvokeModel、Vertex rawPredict、Foundry/AWS;无 OpenAI 格式 | 协议墙;网关必须原生 Messages |
| Gemini CLI | generateContent | Gemini API / Vertex / Code Assist;无 OpenAI 兼容、无 Interactions;2026-06 起免费/Google One 层由 Antigravity CLI 接替 | Google 自有栈;旧版参考 |
| Antigravity CLI | Google 托管后端 | Gemini + 通过 Google 提供的 partner Claude;无 BYOK base URL | 账号锁定;不是通用 BYO 网关客户端 |
| GitHub Copilot CLI | GitHub 托管 | BYOK:openai(Chat,默认)、azure、anthropic(Messages);有 COPILOT_PROVIDER_WIRE_API | 托管默认 + 有限 BYOK |
| Cursor / Amp / Warp | 厂商后端 | BYOK 仅部分 provider;wire protocol 不公开 | 闭源,协议不可观测 |
| 多模型 harness / 网关客户端 | |||
| Cline | OpenAI→Resp Anthropic→Msg Google→Gen | OpenAI-compatible(Chat)、Bedrock/Vertex/Ollama、Codex/Claude 订阅 | provider-native + 兼容端点 |
| Kilo Code | 未知端点默认 Chat | 显式 Provider API:Chat / Responses / Messages;Gemini 走 Google AI | 把协议做成显式选项 |
| OpenCode | OpenAI→Resp | AI SDK + Models.dev:Responses/Chat、Messages、Gemini、Google Interactions;自定义 baseURL | AI SDK 抽象,默认随 provider |
| Continue | 多数 Chat | OpenAI Responses 只对 o-series/GPT-5+ 做“minimal draft”;Anthropic Messages;Gemini generateContent | 兼容优先,Responses 部分支持 |
| Goose | OpenAI Chat Anthropic Msg Gemini Gen | ChatGPT/Codex 订阅走 Responses;Bedrock/Vertex/Azure/LiteLLM/兼容端点 | Rust provider crates 按厂商实现 |
| Crush | 按 provider 类型 | provider 类型:openai(可 Responses;ChatGPT OAuth 走 Responses)/ openai-compat(Chat)/ anthropic(Messages)/ google(Gen) | “openai”和“openai-compat”明确区分 |
| Zed Agent | 自定义默认 openai_chat | per-model protocol:anthropic / openai_responses / openai_chat / google | 少数四协议都可选的编辑器 |
| Aider / OpenHands / SWE-agent | harness 侧 Chat | 通过 LiteLLM 出站到 Messages/generateContent;自定义端点默认 Chat | LiteLLM 翻译,能力取决于翻译层 |
| Factory Droid | openai→Resp anthropic→Msg generic→Chat | 还有 bedrock-converse;provider 类型显式 | 闭源但公开协议映射 |
| DeepSeek Harness (DSH) | DeepSeek route→Messages | 其他 provider 经 pi-ai 按 catalog native;自定义 route 仅 3 种;无 Interactions | provider-neutral + native adapter |
| Agent SDK / 框架 | |||
| OpenAI Agents SDK | Responses | Chat 通过 OpenAIChatCompletionsModel / set_default_openai_api("chat_completions") | Responses 优先;Chat 能力受限 |
| Claude Agent SDK | Messages | Bedrock/Vertex/Foundry 变体;ANTHROPIC_BASE_URL 可接网关 | 包装 Claude Code 运行时 |
| Google ADK | generateContent | Interactions 通过 use_interactions_api=True opt-in;Claude/OpenAI/LiteLLM 连接器 | 默认 Gen,Interactions 可选 |
| Vercel AI SDK | OpenAI→Resp Anthropic→Msg Google→Gen | openai.chat、google.interactions、OpenAI-compatible(Chat) | 统一抽象,默认随 provider/SDK 版本 |
| 事件 | 事实 | 影响 |
|---|---|---|
| Anthropic API 2026-10-21 | 移除 temperature/top_p/top_k;SDK 默认改用 Bearer;API 仍接受 x-api-key;Foundry SDK 例外 | 旧 SDK/harness 可能不兼容;锁定 anthropic-version |
| Gemini Interactions | 2026-06 GA;Google 新能力优先在 Interactions;generateContent legacy 但继续支持 | Gemini-first 新项目优先 Interactions;第三方 harness 支持仍薄 |
| Codex wire API | wire_api 只接受 "responses";当前版本不再支持 "chat" | Chat-only 网关无法服务 Codex |
| DSH | 0.2.1-rc.2;dsh-llm-deepseek 走 Messages;dsh-llm-pi-ai 依赖 pi-ai ^1.1.0(当前 1.4.0) | 开发预览;协议/包版本变化快 |
Google 在 2026-06 将 Interactions GA,并明确新模型/agentic 能力优先在 Interactions 上线;generateContent 转为 legacy 但继续支持。它用 Interaction + typed steps(thought、function_call、function_result、model_output),默认 server state(previous_interaction_id),支持 background 和托管 agent。
当前第三方 harness 对 Interactions 的支持仍薄:AI SDK、ADK(opt-in)、OpenCode 是少数先行者;Gemini CLI、Cline、Kilo、Continue、Goose、Crush、DSH 等仍以 generateContent 为主。
Wire protocol:模型 API 的 HTTP/JSON/SSE 形状;本文比较的对象。
Harness:驱动模型、执行工具、管理上下文的 agent 运行时(CLI/IDE/SDK)。
Hosted tools:由模型厂商在服务端执行的内置工具(web search、code execution 等)。
Opaque reasoning:加密/签名的 reasoning 状态;客户端不可读,但必须原样回传。
Native passthrough:网关不翻译,直接把客户端协议转发到同协议上游。
OpenAI-compatible:通常只意味着 /chat/completions 兼容,不等于 Responses 兼容。
MCP:Model Context Protocol,工具/上下文接入协议,不是模型 API 协议。
ACP:Agent Client Protocol,agent 与编辑器/harness 之间的协议,不是模型 API 协议。
ReplayEnvelope:DSH 存储的 adapter-private 原生回放元数据;内容仍是 durable message。
资料与代码快照为 2026-10-25。Anthropic API 刚在 2026-10-21 做了破坏性变更;Codex、Claude Code、harness 和 DSH 版本也在快速变化。生产落地请锁定 SDK/API 版本,并建立自己的协议回归测试。