DSHarness 系统拆解 固定基线 47f943859b · 36 已复核 / 0 撰写中 / 36 章
English
核心运行时·第 08 章

LLM Adapter 与会话协议转换

内部消息怎样投影到 DeepSeek、Pi AI 与重放适配器

已复核上游 47f943859b范围: 分析内部消息/流词汇、adapter registry、provider selection、tool call、reasoning、usage 与错误归一化。

结论:Adapter 翻译一次调用,而 Session Log 拥有会话

DeepSeek Harness 没有让任何 Provider SDK 成为对话状态的 owner。内部先形成不可变、带来源的 Message 和封闭的 StreamChunk 协议;Adapter 只负责把一次完整请求投影到某条 wire protocol,再把响应翻译回来。持久化、重试、重放和工具续跑都发生在 Adapter 外。

这个边界的价值不只是“支持多个模型”。它把 Provider 差异限制在 capability resolution、wire serialization、stream translation 与 error classification 四个面,同时让 Agent Loop 能用同一套 Turn/Step、request header 和 SessionEvent 语义验证每次派发。

1. 一次模型调用跨越六个提交边界

1

从日志派生历史

Loop 读取当前 request header 与 Session.deriveMessages(),不向 SDK 询问对话历史。

2

准备 exact call

prepareCall() 解析唯一 route、精确 model capability、adapter defaults、context window 与 retry policy。

3

提交 request facts

Canonical header 与 request context 先写 Session;最终 request 被 deep-freeze。

4

Durability barrier

llm/stream middleware 在构造下游 Adapter stream 前 flush 完整 request prefix。

5

记录 raw stream

每个 chunk 先 append 为 assistant/chunk,再交给规范组装器。

6

提交语义消息

只有成功 finish 才生成一个 assistant/message;失败由 request-error policy 处理。

生产控制流

同一 Step 内的 request、raw chunks 和最终 message 都携带相同 turn/step;Provider 不接触 Session 对象,也不决定何时开始下一 Step。LOOP-STEP-STATELOOP-REQUEST-BUILD

副作用前屏障

模型 I/O 是明确的语义副作用边界:request facts 未成功持久化时,checkpoint listener 不会调用 Adapter。LLM-CHECKPOINT-BARRIER

2. 内部协议刻意比任何单一 Provider 更窄

核心表示关键约束为什么重要
Message{id, role, content, source}创建时 clone + deep-freeze;stable ID 穿过投递、日志与 request没有 SDK-owned history 的第二事实源 LLM-MESSAGE-VALUE
Contenttext / reasoning / image / tool-call / tool-resultTool arguments 保留模型产生的原始 JSON string解析错误留给工具边界;Adapter 不重写原始意图 LLM-CONTENT-PROTOCOL
Provenanceuser / plugin / model / toolAssistant 必须记录 provider、model,可带 replay state跨 Provider 历史不会冒充 native response LLM-MESSAGE-IDENTITY
Streamblock-start / delta / block-end / usage / finishIndex 允许交错 block;usage 在 terminal finish 之前;finish 后无 chunkUI、日志、Assembler 与 Adapter 共用一份流合同 LLM-STREAM-PROTOCOL
Requestconfig + system + tools + messagesProvider/model 精确路由;sessionId 与 purpose 是 model-hidden metadata会话语义与 wire metadata 分离

该协议是 merge-extensible,但内置 Adapter 对未知 block 的行为并不自动对称。扩展一个新 block type 必须同时补 Adapter、UI、compaction 和 replay 支持;仅扩 TypeScript union 不会获得端到端能力。

3. Provider route 是唯一映射,不是 fallback list

Registry 合同

一个 provider string 同时只能映射到一个 Adapter registration。空 route、重复 route 或 metadata ID 不一致均同步失败;一项多 route 注册是 all-or-nothing。Effect teardown 精确删除自己仍拥有的 route,replace() 在一个同步区段内完成旧集合到新集合的切换。LLM-ADAPTER-REGISTRY

Prepared call

prepareCall() 捕获 registration、resolved config、retry policy、model context 和 adapter-default marker,返回只能 dispatch 一次的 frozen handle;调用者若改动 config,派发以 INVALID_PREPARED_CALL 拒绝。LLM-PREPARED-CALL

热替换测试

Route 在 prepare 后被替换,旧 prepared call 仍由旧 Adapter 和旧 retry policy 服务;新的 registry lookup 才看到新 registration。LLM-PREPARED-PIN-TEST

4. BlockAssembler 是成功响应的唯一语义折叠器

组装规则

Assembler 以 block index 维护插入顺序,支持没有 start/end 的 delta-only Adapter,也允许 reasoning、text 与多个 tool call 交错。第一次 block-end 成为权威完整 block;随后同 index 的 delta 或再次 close 被忽略。LLM-BLOCK-ASSEMBLER

  • 没有显式 finish 时 accessor 默认 stop;生产 Adapter 应自己保证 terminal protocol。
  • 未闭合的 text/reasoning/tool-call 可从 delta 合成;未知未闭合 block type 会失败。
  • max-tokens 时所有 tool-call block 被过滤,避免执行截断的 JSON 参数。
  • Usage 采用最后一个 usage chunk;replay state 来自 terminal finish。
  • Raw chunks 已先进入 Session Log,因此语义消息不是唯一可观测层。
设计评价

这是一处重要的“单一折叠算法”选择:Provider Adapter 负责发事实,Agent Loop 不各写一套拼接逻辑。代价是协议容忍 malformed stream 的若干分支;它优先保持已关闭 block 与已展示流一致,而不是对每种违规都 fail-fast。

5. DeepSeek 直连 Adapter:窄协议、宽 model passthrough

真实行为边界/后果
Route固定 deepseek-official默认 catalog 是 V4 Flash/Pro,但未知 model ID 仍原样直通
Capability默认 1,000,000 context、256,000 output、text-onlyCatalog 是 discovery/advisory,不是 allowlist LLM-DEEPSEEK-MODELS
Thinkingoff/high/max;一般默认 high;deployment disabled 时只允许 offsession-title 无条件关闭 thinking
Configuration每次 request 解析 endpoint、credential 和 last-good snapshot坏的 live settings 不替换上一份可服务配置 LLM-DEEPSEEK-CONFIG
Transport原生 fetch POST /chat/completions + SSE发送 attribution、匿名 user ID、session ID;compaction 有单独 header LLM-DEEPSEEK-TRANSPORT
Wire 投影

System 成为首条 wire message;user text 连接;内部 user-role tool-result 被展开成独立 role: tool。Assistant 的 text 与 tool calls 原序投影,但 reasoning_content 只在 tool-call turn 回传;plain-turn reasoning 仍在 durable internal message 中,却不进入后续 DeepSeek wire history。Image 在任何 text flattening 前显式拒绝。LLM-DEEPSEEK-SERIALIZE

SSE 翻译

Translator 在收到字面量 [DONE] 时才关闭所有 block、发最后 usage 和 finish。坏 JSON 为 MALFORMED_RESPONSE,无 DONE 为 STREAM_CLOSED,没有任何 content 的正常 stop 为 EMPTY_RESPONSE。DeepSeek 的 prompt total 包含 cache hit,Adapter 会扣除 hit 形成互斥 input bucket。LLM-DEEPSEEK-TRANSLATE

6. Pi AI Adapter:宽 Provider 面、严格 exact-model 面

两种 provider 构造

已安装 catalog 的 route 且未覆盖 wire API 时,Harness 复用原 pi-ai Provider,保留其 native API implementation 与 ambient auth。手工 route 或显式 protocol override 只允许 openai-completionsopenai-responsesanthropic-messages;Bedrock、Vertex、Azure 与 OAuth-only 路径因当前配置形状无法完整表达而不伪装支持。LLM-PI-PROVIDER

动态 profile

Plugin 默认没有任何 route。每个配置 profile 自己拥有 model catalog/override、endpoint、credential reference、reasoning mapping、thinking budget、cache retention、SSE/WebSocket transport、timeout、header 与 retry policy。LLM-PI-CONFIG

调用行为Pi AI 路径与直连 DeepSeek 的差异
Model resolution必须在当前 profile 精确存在;未知 model 在 I/O 前失败DeepSeek 未列出的 ID 仍 passthrough
Stop sequences显式不支持DeepSeek 直接写 wire stop
ImageModel 声明 image 且 durable attachment service 存在才接纳DeepSeek 路径总是 text-only
Retries传给 SDK 的 maxRetries: 0两条路径都把可见 attempt 交给 Agent recovery
Reasoning按 exact model 枚举,非法 effort 拒绝,不让 SDK clampDeepSeek 固定 off/high/max
Error detail上游库丢失 Error/cause 后以 message regex 分类直连路径可保留 HTTP status、Retry-After 与 request ID
每次 stream snapshot

Pi Adapter 在第一次 await 前固定 profile、model collection 和 descriptor,再解析 credential;中途配置更新不会改变已开始请求,下次调用看到新 snapshot。LLM-PI-STREAM

7. “Replay”有两层,不能混为一谈

Provider-native replay metadata

Pi AI 的 text、reasoning 和 tool-call 内容仍以 Harness Message 为权威;PiAiReplayState v1 只持久化 API、source provider/model、response identity、stop reason 与各 block signature。恢复时先验证 kind/version,再验证 source 和每个 block 的数量、类型及 metadata。LLM-PI-REPLAY

LlmRuntime 只在历史 provider 与目标 provider 当前由同一个 Adapter instance 拥有时保留这份 private state;否则剥离 state,以普通“foreign assistant”重建。这防止一个 Adapter 读取另一个 Adapter 的私有协议数据。

Keyless test replay

dsh-llm-replay 是测试/快照 Adapter,不是生产会话恢复机制。它从 assistant/chunk 的每个 finish 派生一次 provider call;同一 Turn/Step 内的 retry 被切成多条 script entry。Thrown stream 与 hang 无法仅凭成功日志还原,必须提供 sidecar override。LLM-REPLAY-DERIVATION

边界结论

会话 replay 重建“下一次请求”;Adapter replay state 恢复“历史响应的 provider-native metadata”;测试 replay 重放“预录的 chunk 流”。三者解决不同问题。

8. Retry 是同一 Step 的另一次 Provider attempt

真实 loop

step()while (true) 包住 build request、stream 与 request-error waterfall。失败得到 {kind:'retry'} 后直接 continue,没有关闭 Step 或 Turn;下一 attempt 重新 derive messages 和 build request,但复用该 Step 的 prompt assembly。PROMPT-RETRY-BOUNDARYLLM-RETRY-SAME-STEP-TEST

Policy executor

Normal mode 默认最多两次 retry,只覆盖 EMPTY_RESPONSERATE_LIMITSERVERTIMEOUTTRANSPORT;本地延迟从 500ms 指数增长至 10s,带 10% 对称 jitter。Always mode 对所有 request failure 无限尝试,直到成功、取消或 plugin disposal。LLM-RETRY-POLICY

耐久调度

Plugin 先 append 包含 policy key、失败、retry number 和 delay 的 llm/retry,再进行可取消等待;等待结束后 append llm/retry-started,才把 retry action 交回 Loop。Provider Retry-After 只有不超过 cap 才直接采用。LLM-RETRY-EXECUTOR

Wire 测试合同

真实 mock HTTP 测试要求失败与成功 attempt 的 body 完全相同;失败 partial chunks 仍在 raw log,但不进入 surface,最终只提交一个 assistant message。干净 EOF 分类为 STREAM_CLOSED,当前默认 policy 不重试它。依赖未安装,本研究只审阅了测试源码。LLM-RETRY-WIRE-TEST

9. 上游 Retry 文档已经与实现漂移

确认的文档/源码冲突

llm-retry README 写着“每次 retry 打开新编号 Turn”“Loop 关闭 failed Turn 后再开 retry Turn”,并把 Agent Turn 称为唯一 retry boundary;当前生产源码和两套测试都证明 attempt 共享同一 turn/step。LLM-RETRY-DOC-DRIFTLOOP-REQUEST-ERROR-TEST

这不是措辞小错。它影响事件解释、UI grouping、成本归因、replay script 分段和所有以 (turn, step) 当作 request identity 的 projection。分析和下游实现必须以当前 loop 与测试为准,而不是复制 README 的“新 Turn”叙事。

10. Error normalization 只包住 Adapter-owned failure

精确边界

Provider route 选择、Adapter stream() 调用、iterator 构造和 next() 抛错都会变成一个 terminal finish {error|aborted, failure}。Failure 是可序列化快照,可保留 Harness-owned code、HTTP status、provider delay 和 request ID。LLM-FAILURE-BOUNDARY

  • llm/stream middleware 自己抛错:继续 throw,不伪装成 Provider failure。
  • 下游 consumer 在 yield 后抛错:继续 throw。
  • Consumer 提前关闭时 Adapter iterator 的 cleanup failure:继续 throw。
  • Adapter 抛任意非 Error 值:归一为 UNKNOWN,不让 hostile coercion 逃出。
  • AbortSignal 已取消或 failure code 为 ABORTED:terminal reason 为 aborted。
设计评价

该区分保留责任归属:Provider/transport failure 可以进入统一 retry policy;插件 bug、consumer bug 与 cleanup bug 不能被自动重试吞掉。Pi AI 因上游 flatten Error 而只能用文本分类,是 capability 退化,不是核心协议的要求。

11. Token、cache 与一个高置信度计费缺口

来源映射保真边界
DeepSeekinput = prompt_tokens - cache_hit;output 原样;可带 reasoning没有 cache-write 字段;reasoning 是 output 的子维度,不另加总
Pi AIinput/output/cacheRead/cacheWrite 映射;零 cache bucket 省略pi-ai 已把 reasoning 计入 output,不再单独上报
Harness projection累计 uncached input、output、cache read/write以最新同 turn/step sample 替换前 sample
原设计意图

Usage chunk 会早于最终 message;成功时 message 又携带同一 usage。因此 projection 用 (turn, step) 识别“同一次调用”的早期与最终样本,扣掉旧值再加入新值,避免双计。LLM-TOKEN-USAGE-PROJECTION

源码推导的缺陷

Retry 让多个 Provider attempt 共享同一 (turn, step),而 replay 测试明确把这些 attempt 当作多个 call。若失败 attempt 已发 usage,成功 attempt 的 usage 会替换而非累加它;累计投影因此低估真实 Provider 消耗。LLM-MULTI-ATTEMPT-REPLAY-TESTLLM-RETRY-SAME-STEP-TEST

12. Prepared registration 并未固定 Adapter 的动态配置代际

源码推导的竞态窗口

prepareCall() 固定 Adapter registration 与 resolved model config;随后 checkpoint 可能异步 flush;真正开始迭代时,DeepSeek Adapter 再调用 options(),Pi Adapter 再调用 current()。若 settings 恰在两者之间更新,日志中的 model defaults/context 可能来自代际 A,而 endpoint、profile 或 credential 来自代际 B。LLM-PREPARED-CALLLLM-CHECKPOINT-BARRIERLLM-DEEPSEEK-TRANSPORTLLM-PI-STREAM

现有 Pi snapshot 测试覆盖的是 Adapter stream 已开始 后,credential await 期间发生更新;它证明开始后的 snapshot 稳定,却没有覆盖 prepare 与首次 iterator 之间的窗口。更进一步,Loop reconstruction invariant 比较 model/system/temperature/maxTokens/stop/tools/messages,但没有比较 provider 或 reasoningEffort。LLM-REQUEST-INVARIANT-SCOPE

13. 两条生产 Adapter 的能力矩阵

维度DeepSeek directPi AI架构含义
Provider breadth一个官方 route多 profile、多 catalog provider窄而保真 vs 宽而受上游抽象约束
Wire control完整 fetch/SSE ownership委托 pi-ai provider APIs直连能保留更多错误与 header 事实
Model admissionCatalog 外 passthroughExact catalog membershipForward compatibility vs 配置闭包
Images拒绝按 capability + attachment storeDurable binary ownership是额外依赖
Reasoning固定 off/high/max每 model 映射Provider-neutral ID 仍需要 Adapter 验证
History fidelityTool turn passback;plain reasoning 不回传版本化 signatures/replay metadata同一内部 Message 可有不同 wire 保真度
Usage含 cache read 与可选 reasoning含 cache read/write,reasoning folded统一 bucket 不意味着源数据同等丰富
RetryAdapter 每次只尝试一次;Agent recovery 拥有可见 retry统一成本、事件和取消边界

14. 优势、代价与当前未知

选择收益代价/未知
Provider-neutral immutable messages日志、UI、replay 与 request 共用事实每个新 block 必须跨所有消费者落地
Raw chunks + semantic message 双层日志兼顾流式观察与最终语义Projection 必须正确识别 attempt 与 replacement
Prepared one-shot call防 registration HMR 与 request config drift尚未封闭 Adapter-internal config generation
Retry 位于 Agent Loop失败 attempt、等待、取消和成本可记录Always mode可无限消耗;同 Step identity 已暴露计费缺口
Direct + library-backed 双 Adapter同时拥有一条高保真路径和广覆盖路径错误细节、reasoning、usage、stop/image capability 不对称
没有隐式 failoverProvider 切换不会悄悄改变语义或成本需要部署显式组合自己的 fallback policy
  • 尚未验证安装版 pi-ai 0.82.1 的完整内置 provider/model catalog;依赖目录缺失。
  • 尚未用真实 API 比较 DeepSeek 与 Pi 路径在同一 model 上的 reasoning/cache/error fidelity。
  • 尚未证明 dynamic-config generation window 能在真实 settings HMR 时稳定复现。
  • 尚未有自动 provider failover 的标准事件、幂等或成本协议。
  • 上游 Retry 文档需要以同 Step attempt 语义修正。
验证状态

本章逐段审阅了生产源码、类型与测试源码,并把测试结论明确标为“测试定义的合同”。仓库没有安装依赖,本研究没有执行上游测试,也没有把 source inference 冒充运行时观察。

我的学习体会

内容仅自动保存到当前浏览器,不上传、不进入仓库。你可以导出 Markdown 自行归档。