Provider-routed LLM adapters and a generic pi-ai backend
基于提供方路由的 LLM 适配器与通用 pi-ai 后端
`dsh-llm` registered adapters by exact model name. A plugin supplied a model list at Cordis startup, `LlmRuntime` stored one adapter per listed string, and `GenerateOptions.model` selected the adapter and the provider model at once. This worked while both shipping adapters targeted the same two DeepSeek models, but it conflated two independent decisions: which upstream provider owns a request, and which model that pr
English
Problem
dsh-llm registered adapters by exact model name. A plugin supplied a model list at Cordis startup, LlmRuntime stored one adapter per listed string, and GenerateOptions.model selected the adapter and the provider model at once. This worked while both shipping adapters targeted the same two DeepSeek models, but it conflated two independent decisions: which upstream provider owns a request, and which model that provider should run.
The conflation prevents a provider gateway from serving an open-ended model catalog. OpenRouter, for example, is one provider with many model ids, while a private OpenAI-compatible endpoint may add models without changing the Harness plugin tree. Every newly selected model currently needs to have been registered during plugin startup. The same model id can also exist at multiple providers, so model-only registration cannot state which provider the caller intended.
dsh-llm-pi-ai exposed none of pi-ai's provider abstraction. It constructed an inline DeepSeek openai-completions model, applied DeepSeek-specific payload patches, and stamped every replayed assistant message as DeepSeek. pi-ai itself has a provider/model catalog, selects APIs such as openai-responses, anthropic-messages, and google-generative-ai, and preserves provider-specific response ids and reasoning/tool signatures for later turns. The Harness conversion dropped the provider/model route and provider response fields, so simply replacing the inline model with a catalog lookup would have made same-model replay and cross-provider handoff incomplete.
The adapter configuration also assumes one DeepSeek API key and endpoint. A generic backend needs independent credentials and endpoint overrides per provider while leaving AWS, Google ADC, OAuth, and other ambient authentication mechanisms to pi-ai.
Decision
Provider is the adapter registration key
GenerateOptions and LlmCallConfig carry provider: string beside model: string; AgentOptions carries the corresponding optional creation field. A loop request is valid only after both values are non-empty, and both values are part of the logged request header. agent/request may return a replacement pair on any step, so a session can switch providers and models without changing the Cordis plugin lifecycle.
LlmRuntime registers and resolves adapters by provider. registerAdapter(providers, adapter) checks the entire provider list before mutating the registry, rejects a duplicate with DUPLICATE_ADAPTER, and disposes the whole registration as one effect. Model ids are not registration keys; the selected adapter still validates or forwards them. The later LLM catalog and ACP selection Agent Note added advisory listProviders() / listModels() discovery without turning model membership into request validation.
A provider has exactly one adapter owner in a Cordis context. dsh-llm-deepseek registers deepseek; dsh-llm-pi-ai may also register deepseek, but loading both owners is a configuration error rather than an ordering rule or fallback. A deployment that wants the hand-rolled DeepSeek implementation excludes deepseek from the pi-ai profiles. A deployment that wants pi-ai's DeepSeek implementation does not mount dsh-llm-deepseek.
dsh-llm-deepseek removes its model registration list and accepts any model string routed through provider deepseek. Its request serialization, /chat/completions endpoint, thinking options, SSE parsing, and error behavior remain unchanged; options.model is still sent verbatim.
Explicit pi-ai provider profiles
dsh-llm-pi-ai takes one non-empty list of provider profiles. Provider names must be unique within the list and present in pi-ai's getProviders() result. Each profile contains the provider name plus optional apiKey, baseURL, headers, reasoning level and budgets, cache retention, transport, SDK timeouts, a Harness stream-idle timeout, and a provider-owned retryPolicy. The adapter forces pi-ai's maxRetries to zero so one stream() call makes one visible provider attempt, while dsh-llm-retry executes the resolved policy at the agent failed-step extension point. Credentials are never global: an explicit key applies only to its profile, while an absent key lets pi-ai resolve its standard environment variable, OAuth token, AWS credential chain, Google ADC, or other provider-native ambient authentication. An explicitly empty key is invalid configuration rather than an environment fallback.
The plugin registers all configured provider names against one PiAiAdapter in one all-or-nothing call. A request uses its provider to select the matching profile and finds its model in getModels(provider) to obtain the catalog descriptor. An unknown provider fails at plugin load; an unknown model fails before network I/O with UNKNOWN_MODEL. The catalog object is never mutated. When a profile supplies baseURL, the adapter clones the selected descriptor and overrides only baseUrl, so a private endpoint can retain pi-ai's API, capabilities, compatibility flags, context limits, and reasoning map. The private endpoint must implement the selected provider's protocol, and the model id must still exist in the installed pi-ai catalog.
The adapter calls pi-ai's streamSimple() so each catalog model chooses its registered API implementation, including OpenAI Responses instead of Chat Completions where the descriptor says openai-responses. Harness temperature, maximum tokens, signal, session id, and the profile's common stream options flow through directly. Profile headers merge with the mandatory Harness attribution headers, with Harness attribution winning its reserved names. The adapter no longer maintains DeepSeek-specific payload rewrites or a provider-protocol matrix.
pi-ai's common stream options do not expose stop sequences. dsh-llm-pi-ai rejects a defined Harness stop option with UNSUPPORTED_OPTION rather than silently ignoring it or growing a second provider-specific payload implementation. dsh-llm-deepseek continues to support stop through its native request serializer.
Recorded assistant route and replay state
Assistant messages carry the request's provider and model, plus an optional JSON-serializable adapter replay state. A successful assistant/message session event records those fields and deriveMessages() returns them with the assistant message. User, system, context, and tool-result messages carry no assistant route fields. The provider/model fields are authoritative loop data; an adapter owns only its opaque replay-state payload.
A terminal successful finish chunk may carry replay state as a ReplayEnvelope: opaque response-level metadata plus optional per-block entries aligned with the emitted block sequence. BlockAssembler makes one keep/drop decision for content and metadata — when max-token assembly drops a tool call, the envelope loses the entry at the same position — so the state the loop attaches to the assembled assistant message's model source always describes the stored blocks, per the max-token replay-state alignment decision. The loop exposes no response-rewrite hook. Error and aborted responses do not produce a normal assistant message and therefore do not enter future model history.
The pi-ai replay state fills that envelope with a versioned, minimal projection of its successful AssistantMessage: a response half (source API/provider/model, response id/model, stop reason) and per-block text, thinking, and tool-call signatures. It does not duplicate text or tool arguments already carried by Harness content blocks, and it omits diagnostics, timestamps, usage, and errors. On a later request, LlmRuntime gives replay state to the target adapter only when the historical provider and target provider are currently owned by the same adapter instance. That adapter combines the logged Harness content with replay state when it can restore the historical response, and owns any required cross-model or cross-provider conversion. Durable content stays authoritative: an adapter receiving replay state it cannot use — an unknown kind or version, malformed metadata, or a block shape that no longer matches the content — degrades that message to provider-neutral conversion with a diagnostic; a different adapter receives only provider-neutral content plus provider/model fields.
This state is model-visible replay input and therefore follows the existing reconstructable-request rule: it is present in both the terminal finish chunk and the assembled assistant/message model source that drives derivation. Resume and fork preserve it verbatim. Compaction that shadows the assistant message also removes its replay state from the active surface; the summary is ordinary provider-neutral content.
Propagate the target through every request producer
Every model-selection path carries provider and model together: declarative agents, ACP and stdio app config, the JSON-RPC initialize request, subagent overrides and inheritance, workflow child overrides, and direct compaction summarization. Subagents inherit both fields from their parent before applying request overrides. The system-prompt variable set gains provider beside model.
Compaction configuration gains summarizationProvider beside summarizationModel. Both are empty to inherit, or both are non-empty to select an explicit target; a half-configured pair fails load. Inheritance uses the last logged request target when one exists and falls back to the agent's creation options. compaction/summary records both fields with the existing model-call envelope.
The JSON-RPC runtime receives provider and model explicitly. Its convenience fallback mounts dsh-llm-deepseek only for provider deepseek when that provider has no registered owner; other missing providers fail without guessing an adapter.
The on-disk session format remains the pre-release pinned version 0, with no compatibility promise. Seed/load validation rejects request headers and assistant messages that omit required provider/model fields instead of accepting an old shape that can no longer reconstruct the request.
Alternatives considered
Keep model names as registry keys and add wildcard adapters. A wildcard introduces fallback ordering between exact registrations and catch-all plugins, makes duplicate ownership dependent on listener order, and still cannot distinguish the same model id at two providers without another convention.
Encode provider and model into one string. Values such as OpenRouter's openai/gpt-* already contain provider-like prefixes and slashes. A delimiter convention would leak routing syntax into every model selector and require escaping rules; two explicit fields are unambiguous and independently loggable.
Add backend + provider + model. A backend key would allow dsh-llm-deepseek and pi-ai's DeepSeek implementation to coexist and switch per request. The accepted deployment rule is instead one adapter owner per provider: implementations of the same upstream are alternatives selected by plugin composition. A third routing dimension would burden every request and configuration for a capability with no current consumer.
Let dsh-llm-pi-ai automatically register every pi-ai provider. This would claim ambient credentials and provider names the deployment never intended to expose, and would conflict with native adapters such as dsh-llm-deepseek. Explicit profiles make capability and credential scope reviewable.
Mount one pi-ai plugin instance per provider. Separate instances isolate config but repeat plugin declarations and cannot make profile registration atomic. One adapter already receives provider on every request, so a validated profile map is the smaller lifecycle API.
Accept arbitrary inline pi-ai model descriptors. This would support catalog-external private model ids, but it exposes pi-ai's model and compatibility schema as Harness configuration and makes the adapter responsible for validating protocol-specific combinations. The first version supports custom endpoints by overriding baseURL on catalog models; custom descriptors require a separate decision after a real catalog-external deployment is identified.
Consequences
- Provider names are deployment-wide route ownership keys: two providers may use the same model string, but mounting two adapters for one provider fails at load instead of creating fallback order.
- Model selection no longer changes the Cordis plugin graph. Catalog-backed adapters can accept any installed catalog model selected after startup, while the native DeepSeek adapter forwards arbitrary DeepSeek model ids.
- A custom
baseURLpreserves the selected catalog model's protocol and capabilities; it does not make catalog-external model ids valid. Private endpoints must implement that catalog entry's protocol. - pi-ai credentials, transport knobs, SDK timeouts, and the five-minute-default
streamIdleTimeoutMswatchdog are scoped per provider profile. Hidden provider retries are disabled; bounded retries belong to the separately composed agent recovery policy. dsh-llm-pi-airejects stop sequences because pi-ai's common stream API cannot express them; the native DeepSeek adapter retains its stop support.- Replay state is portable only within the adapter instance that owns both the historical and target providers. Cross-provider and cross-model restoration is an adapter responsibility, and another adapter receives provider-neutral history without the opaque state.
- Current pre-release session JSONL requires provider/model on request headers and assistant messages. Older shapes remain version
0but are rejected rather than migrated.
Testing
- Unit coverage exercises registry conflicts, request reconstruction, session validation, profile resolution, single-attempt option forwarding, native API selection including OpenAI Responses, conversion, replay validation, error mapping, caller cancellation, idle-timeout transport termination, content rewrites, and same-instance versus different-instance replay dispatch.
- Keyless loop/session tests and ACP snapshots exercise durable provider/model metadata, resume and fork propagation, workflow/subagent overrides, and unchanged user-visible transcripts; the key-gated DeepSeek e2e retains real provider streaming and tool follow-up coverage.
- Public JSDoc, package READMEs, architecture and subsystem docs, generated catalogs, examples, session fixtures, and Python SDK pairs use provider/model targets consistently and are checked by the repository documentation and type-equivalence gates.
Risks
This is a repo-wide pre-release API break: model-only request construction, adapter registration, app protocols, fixtures, and persisted version-0 event shapes all change together, with no compatibility aliases. The provider exclusivity rule deliberately prevents two implementations of the same upstream from coexisting in one context. A pi-ai dependency update can change the accepted provider/model catalog, so the lockfile and adapter e2e matrix define the tested set. Custom baseURL endpoints inherit the chosen catalog model's protocol assumptions and cannot repair an incompatible proxy. Catalog-external model descriptors and multimodal content remain unsupported. pi-ai replay state may contain opaque encrypted reasoning signatures; it is persisted because the provider requires it for continuity, but it is never rendered or logged outside the existing session record.
中文
问题
dsh-llm 按精确模型名称注册适配器。插件在 Cordis 启动时提供模型列表,LlmRuntime 为列表中的每个字符串保存一个适配器,GenerateOptions.model 同时选择适配器与提供方模型。两个随附的适配器都只面向相同的两个 DeepSeek 模型时,这种方式可以工作,但它混淆了两个独立决策:由哪个上游提供方承接请求,以及该提供方应运行哪个模型。
这种混淆使提供方网关无法提供开放的模型目录。例如,OpenRouter 是一个包含大量模型 ID 的提供方,私有 OpenAI 兼容端点也可能在不修改 Harness 插件树的情况下增加模型。目前,每个新选择的模型都必须在插件启动期间完成注册。同一个模型 ID 还可能存在于多个提供方中,因此仅按模型注册无法表达调用方预期使用的提供方。
dsh-llm-pi-ai 没有暴露 pi-ai 的提供方抽象。它以内联方式构造 DeepSeek openai-completions 模型,应用 DeepSeek 专用的 payload 补丁,并将每条回放的助手消息标记为 DeepSeek。pi-ai 自身提供提供方/模型目录,能够选择 openai-responses、anthropic-messages、google-generative-ai 等 API,并保留提供方专用的响应 ID,以及后续轮次所需的推理和工具签名。Harness 转换丢弃了提供方/模型路由和提供方响应字段,因此仅将内联模型替换为目录查询,会导致同模型回放与跨提供方移交不完整。
适配器配置同样假定只存在一个 DeepSeek API 密钥和端点。通用后端需要为各提供方分别配置凭据和端点覆盖,同时继续由 pi-ai 处理 AWS、Google ADC、OAuth 等环境认证机制。
决策
提供方作为适配器注册键
GenerateOptions 与 LlmCallConfig 在 model: string 之外携带 provider: string,AgentOptions 则携带对应的可选创建字段。只有两个值都非空时,agent loop(智能体循环)请求才有效;两个值也都会写入请求头日志。agent/request 可以在任意步骤返回替换后的字段组合,因此会话可以切换提供方与模型,无需改变 Cordis 插件生命周期。
LlmRuntime 按提供方注册和解析适配器。registerAdapter(providers, adapter) 在修改注册表前检查整个提供方列表,遇到重复项时返回 DUPLICATE_ADAPTER,并以一个 effect 为单位整体 dispose(资源释放)。模型 ID 不作为注册键;仍由选中的适配器负责验证或转发。后续的 LLM 目录与 ACP 模型选择 Agent Note 增加了建议性的 listProviders() / listModels() 发现接口,但不会把目录成员关系变成请求校验规则。
在一个 Cordis 上下文中,一个提供方只能有一个适配器所有者。dsh-llm-deepseek 注册 deepseek;dsh-llm-pi-ai 也可以注册 deepseek,但同时加载两个所有者属于配置错误,不采用顺序规则或回退行为。若部署选择手写的 DeepSeek 实现,需从 pi-ai 配置中排除 deepseek;若部署选择 pi-ai 的 DeepSeek 实现,则不挂载 dsh-llm-deepseek。
dsh-llm-deepseek 移除模型注册列表,接受通过 deepseek 提供方路由的任意模型字符串。其请求序列化、/chat/completions 端点、thinking 选项、SSE(Server-Sent Events)解析和错误行为保持不变;options.model 仍会原样发送。
显式 pi-ai 提供方配置
dsh-llm-pi-ai 接受一个非空的提供方配置列表。列表内的提供方名称必须唯一,并且存在于 pi-ai 的 getProviders() 结果中。每项配置包含提供方名称,以及可选的 apiKey、baseURL、headers、推理级别和预算、缓存保留设置、传输方式、SDK 超时、Harness 流空闲超时,以及由提供方拥有的 retryPolicy。适配器强制将 pi-ai 的 maxRetries 设为零,使一次 stream() 调用只发起一次可见的提供方请求;dsh-llm-retry 则在 agent 失败步骤扩展点上执行解析后的策略。凭据不设全局值:显式密钥仅对所属配置生效;未提供密钥时,pi-ai 使用标准环境变量、OAuth token、AWS 凭据链、Google ADC 或其他提供方原生环境认证。显式空密钥属于无效配置,不会回退到环境认证。
插件通过一次全有或全无调用,将所有已配置的提供方名称注册到同一个 PiAiAdapter。请求按 provider 选择对应配置,并在 getModels(provider) 中查找模型以取得目录描述符。未知提供方会在插件加载时失败;未知模型会在网络 I/O 前以 UNKNOWN_MODEL 失败。适配器不会修改目录对象。当配置提供 baseURL 时,适配器复制选中的描述符,仅覆盖 baseUrl,使私有端点保留 pi-ai 的 API、能力、兼容标志、上下文限制与推理映射。私有端点必须实现所选提供方的协议,模型 ID 也仍须存在于已安装的 pi-ai 目录中。
适配器调用 pi-ai 的 streamSimple(),因此每个目录模型会选择其注册的 API 实现;描述符为 openai-responses 时使用 OpenAI Responses,而非 Chat Completions。Harness 的 temperature、最大 token 数、signal、session ID,以及提供方配置中的通用流选项均直接传递。配置 headers 与 Harness 强制归因 headers 合并;发生保留名称冲突时,以 Harness 归因为准。适配器不再维护 DeepSeek 专用 payload 重写或提供方协议矩阵。
pi-ai 的通用流选项不支持停止序列。若 Harness stop 选项已定义,dsh-llm-pi-ai 会以 UNSUPPORTED_OPTION 拒绝请求,不会静默忽略,也不会增加第二套提供方专用 payload 实现。dsh-llm-deepseek 继续通过原生请求序列化器支持 stop。
已记录的助手路由与回放状态
助手消息携带请求的 provider 和 model,以及可选的 JSON 可序列化适配器回放状态。成功的 assistant/message 会话事件记录这些字段,deriveMessages() 返回助手消息时也会包含它们。用户、系统、上下文与工具结果消息不携带助手路由字段。提供方/模型字段是 agent loop 的权威数据;适配器仅拥有其不透明回放状态 payload。
成功的终止 finish 分片可以以 ReplayEnvelope 形式携带回放状态:不透明的响应级元数据,加上与发射块序列对齐的可选逐块条目。BlockAssembler 对内容与元数据只做一次保留/丢弃决定——max-token 组装丢弃工具调用时,数据同一位置的条目一并丢弃——因此 agent loop 附加到已组装助手消息模型来源中的状态始终描述存储的块,见 max-token 回放状态对齐决定。agent loop 不公开响应改写钩子。错误或中止响应不会生成正常助手消息,因此不会进入后续模型历史。
pi-ai 回放状态用其成功 AssistantMessage 的带版本最小投影填充该结构:一个响应半区(源 API/提供方/模型、响应 ID/模型、停止原因),以及逐块的文本签名、thinking 签名和工具调用签名。它不会重复 Harness 内容块中已有的文本或工具参数,也不包含诊断信息、时间戳、用量或错误。后续请求中,只有历史提供方和目标提供方当前归同一个适配器实例所有时,LlmRuntime 才会把回放状态交给目标适配器。适配器在能够恢复历史响应时,将 Harness 记录的内容与回放状态组合,并负责所需的跨模型或跨提供方转换。持久化内容保持权威:适配器收到无法使用的回放状态——未知 kind 或版本、格式错误的元数据、或与内容不再匹配的块结构——会把该消息降级为提供方无关转换并带出诊断;其他适配器只能收到提供方无关的内容以及提供方/模型字段。
该状态属于模型可见的回放输入,因此遵循现有的请求可重建规则:它同时存在于终止 finish 分片和驱动派生的已组装 assistant/message 模型来源中。恢复和 fork 会原样保留该状态。压缩(compaction)遮蔽助手消息时,也会从活动 surface 中移除其回放状态;摘要属于普通的提供方无关内容。
在所有请求生产方中传播目标
每条模型选择路径都同时携带 provider 与 model:声明式 agent、ACP(Agent Client Protocol)和 stdio 应用配置、JSON-RPC initialize 请求、subagent 覆盖与继承、工作流子 agent 覆盖,以及直接压缩摘要。subagent 先从父 agent 继承两个字段,再应用请求覆盖。系统提示词变量集合在 model 之外增加 provider。
压缩配置在 summarizationModel 之外增加 summarizationProvider。两个值均为空时继承,均非空时选择显式目标;只配置其中一个会导致加载失败。继承优先使用最近一次记录的请求目标,没有时回退到 agent 创建选项。compaction/summary 使用现有模型调用 envelope 记录两个字段。
JSON-RPC 运行时显式接收提供方与模型。仅当 deepseek 提供方没有注册所有者时,其便利回退才会挂载 dsh-llm-deepseek;其他缺失的提供方会直接失败,不会猜测适配器。
磁盘会话格式仍使用预发布阶段固定的版本 0,且不承诺兼容性。seed/load 验证会拒绝省略必需提供方/模型字段的请求头和助手消息,不会接受已无法重建请求的旧格式。
考虑过的替代方案
继续以模型名称作为注册表键,并增加通配适配器。 通配机制会在精确注册与兜底插件之间引入回退顺序,使重复所有权取决于监听器顺序;若不再增加其他约定,仍无法区分不同提供方中相同的模型 ID。
将提供方与模型编码到一个字符串中。 OpenRouter 的 openai/gpt-* 等值已经包含类似提供方的前缀和斜杠。分隔符约定会把路由语法泄漏到每个模型选择器,并需要转义规则;两个显式字段更清晰,也可以分别记录日志。
增加 backend + provider + model。 backend 键可以让 dsh-llm-deepseek 与 pi-ai 的 DeepSeek 实现共存,并按请求切换。最终采用的部署规则是一个提供方对应一个适配器所有者:同一上游的不同实现属于由插件组合选定的替代项。第三个路由维度会增加每个请求与配置的负担,却没有当前消费方。
让 dsh-llm-pi-ai 自动注册所有 pi-ai 提供方。 这种方式会占用部署无意暴露的环境凭据和提供方名称,并与 dsh-llm-deepseek 等原生适配器冲突。显式配置可以审查能力和凭据范围。
每个提供方挂载一个 pi-ai 插件实例。 独立实例可以隔离配置,但会重复插件声明,也无法实现配置注册的原子性。每个请求本就向同一个适配器提供提供方,因此经过验证的配置映射具有更小的生命周期接口。
接受任意内联 pi-ai 模型描述符。 这种方式可支持目录外的私有模型 ID,但会将 pi-ai 的模型与兼容性 schema 暴露为 Harness 配置,并要求适配器验证协议专用组合。当前版本通过覆盖目录模型的 baseURL 支持自定义端点;只有实际出现目录外部署需求后,才会另行决策是否支持自定义描述符。
影响
- 提供方名称是部署范围内的路由所有权键:两个提供方可以使用相同的模型字符串,但为同一个提供方挂载两个适配器会在加载时失败,不会形成回退顺序。
- 模型选择不再改变 Cordis 插件图。目录型适配器可以接受启动后选择的任意已安装目录模型,原生 DeepSeek 适配器则会转发任意 DeepSeek 模型 ID。
- 自定义
baseURL会保留所选目录模型的协议与能力,但不会让目录外模型 ID 变为有效。私有端点必须实现该目录项对应的协议。 - pi-ai 凭据、传输选项、SDK 超时,以及默认五分钟的
streamIdleTimeoutMs空闲超时机制均按提供方配置隔离。系统禁用隐藏的提供方重试;有界重试由单独组合的 agent 恢复策略负责。 - pi-ai 的通用流 API 无法表达停止序列,因此
dsh-llm-pi-ai会拒绝停止序列;原生 DeepSeek 适配器仍支持停止序列。 - 仅当历史提供方与目标提供方归同一个适配器实例所有时,回放状态才可移植。适配器负责跨提供方和跨模型恢复;其他适配器只接收不含不透明状态的提供方无关历史。
- 当前预发布会话 JSONL 要求请求头和助手消息都包含提供方/模型。旧格式仍使用版本
0,但会被拒绝,不执行迁移。
测试
- 单元测试覆盖注册表冲突、请求重建、会话验证、配置解析、单次请求的选项转发、包括 OpenAI Responses 在内的原生 API 选择、转换、回放验证、错误映射、调用方取消、空闲超时导致的传输终止、内容重写,以及同一实例与不同实例间的回放分发。
- 无密钥的 agent loop/会话测试和 ACP 快照覆盖持久化提供方/模型元数据、恢复与 fork 传播、工作流/subagent 覆盖,以及不变的用户可见 transcript(文本记录);密钥门控的 DeepSeek e2e 测试保留真实提供方的流式输出与工具后续调用覆盖率。
- 公共 JSDoc、package README、架构与子系统文档、生成目录、示例、会话 fixture(测试前置数据)和 Python SDK 配对文档统一使用提供方/模型目标,并由仓库文档与类型等价门禁校验。
风险
这是一次覆盖全仓库的预发布 API 破坏性变更:仅模型的请求构造、适配器注册、应用协议、fixture,以及持久化版本 0 事件格式会同时变化,不提供兼容别名。提供方排他规则有意禁止同一上游的两个实现共存于同一上下文。pi-ai 依赖升级可能改变可接受的提供方/模型目录,因此锁文件与适配器 e2e 矩阵定义已验证集合。自定义 baseURL 端点会继承所选目录模型的协议假设,无法修复不兼容的代理。目录外模型描述符与多模态内容仍不受支持。pi-ai 回放状态可能包含不透明的加密推理签名;提供方需要该信息维持连续性,因此系统会持久化该状态,但不会在现有会话记录之外渲染或记录它。