青雲的博客
深入浅出 Pi 第二部:模型请求不是一次 fetch 第 07 章

模型目录与 Provider Registry 各管什么

区分生成的模型元数据、pi-ai 的可变 provider 集合,以及 coding-agent 的 ModelRuntime 和兼容门面,说明模型条目如何获得实际请求行为。

源码版本
v0.83.0
验证日期
Commit
845d6ff1f6643aba440341cce877ce1c43ebbc39

models.generated.ts 很像一张完整注册表:里面有 provider、模型 id、API 类型、上下文窗口和价格。可它开头明确标注为生成文件,MODELS 只是带类型的静态目录。目录能回答“内置元数据里有哪些条目”,不能回答“本次请求由哪个函数执行”。像 radius 这类纯动态 provider,甚至可以没有静态目录条目。

目录提供候选,Provider 提供行为

行为装在 Provider 上。builtinProviders() 会逐个调用 provider factory;builtinModels() 再创建可变 Models 集合,把这些对象按 id 放进去。此时模型元数据才与认证策略、动态枚举及 stream/streamSimple 实现连起来。

flowchart TD
  accTitle: 模型目录与运行时 provider 的关系
  accDescr: 生成目录提供元数据,provider factory 提供行为,ModelRuntime 再叠加扩展与配置后交给请求路径
  G["生成目录 MODELS"] --> F["builtin provider factories"]
  F --> B["pi-ai Models: Map<providerId, Provider>"]
  N["native extension provider"] --> R["ModelRuntime 组合"]
  J["models.json / extension config"] --> R
  B --> R
  R --> Q["按 model.provider 发起请求"]
  R --> C["ModelRegistry 兼容门面"]

ModelsImpl 的事实源很朴素:一个 Map<string, Provider>。模型枚举调用各 provider 的 getModels();刷新则允许支持动态列表的 provider 读缓存、按需联网,再把结果保存。createProvider() 合并动态模型时,同 id 的动态条目覆盖 baseline;真正发请求时,还会按 model.api 选择具体适配器。静态存在与运行时可调用因此是两件事。

动态刷新也不会重写 models.generated.ts。provider 的 baseline 保持在代码中,远端或缓存结果保存在实例内;刷新失败时 ModelsImpl 记录该 provider 的 error,并尝试禁止联网地恢复缓存。枚举还是 best-effort:某个 getModels() 抛错只让该 provider 暂时贡献空列表,不会让全部模型选择界面崩掉。因此“目录里有”“runtime 枚举得到”“当前认证可用”是三种不同状态。

反过来,更新生成目录需要重新运行仓库脚本并发布新代码版本,不会被一次用户侧 refresh 顺带写回。固定 tag 下做源码审计时,应把目录视为该版本的静态快照,把动态列表视为运行时数据;两者的时间戳和可复现条件不同。

可用性快照属于 coding-agent,而不是 pi-ai 的静态目录。ModelRuntime 同时持有凭据、配置和 provider 组合结果,才能把全部模型进一步筛成 available;扩展侧 ModelRegistry.getAvailable() 读取的正是这份快照。仅搜索生成文件,无法证明用户此刻能选中或调用某个模型。

coding-agent 再组合一次

coding-agent 还多一层 ModelRuntime。它把内置 provider、原生扩展 provider、扩展配置和 models.json 的 provider id 合并;没有覆盖时保留内置对象的精确行为,有覆盖时通过 composer 重建 provider,组合失败则记录错误并尽量退回 base。扩展 API 里叫 ModelRegistry 的对象只是同步兼容门面,其读写最终仍委托给 ModelRuntime

一轮请求再次做动态核验。ModelRuntime.prepareRequest() 不信任调用方手里的 model 对象足以执行:它用 model.provider 回查当前组合后的 provider,解析此刻的认证,再生成只属于本次调用的 model/options 副本。provider 若已被扩展注销,即使旧 UI 还握着模型元数据,请求也会以 unknown provider 失败;这正是 registry 作为运行时权威的含义。

只读实验可以同时看到“数据”和“行为”来自不同文件:

repo="${PI_SOURCE_DIR:-/tmp/pi-handbook-qbQTcA}"
git -C "$repo" show v0.83.0:packages/ai/src/models.generated.ts |
  nl -ba | sed -n '1,55p'
git -C "$repo" show v0.83.0:packages/ai/src/providers/all.ts |
  nl -ba | sed -n '86,137p'
git -C "$repo" show v0.83.0:packages/coding-agent/src/core/model-runtime.ts |
  nl -ba | sed -n '193,230p'
Pi 模型认证、Provider 分派与兼容 API 的源码入口定位结果

终端把 Models.applyAuth()Models.stream()、Provider 创建入口和兼容层 API 放在同一屏。它说明凭据合并与 Provider 分派发生在请求边界,而不是由模型目录单独完成;这里没有真实凭据和网络请求,不能证明某个外部 Provider 当前可用。

依赖已安装时,可在 packages/ai 下运行 node ../../node_modules/vitest/dist/cli.js --run test/models-runtime.test.ts,覆盖 provider 的替换、删除、查找和动态刷新。目录与 provider 对上以后,请求仍缺一项只应在最后时刻出现的材料:凭据。