# Pi Model Doctor

> [English](../README.md) | **简体中文**

Pi Model Doctor 是一个 Pi 扩展，用于管理 `models.json` 中的模型生命周期。普通的 `pi install npm:pi-model-doctor` 会安装到当前用户的 Pi 全局设置；只有显式使用 `-l` 才会安装为项目级包。它从 [models.dev](https://models.dev) 发现 provider 元数据、补全模型能力、检查现有配置，并在不覆盖用户自有设置的前提下执行安全修复。

## 目录

- [安装、更新与卸载](#安装更新与卸载)
- [全局安装与项目级覆盖](#全局安装与项目级覆盖)
- [命令](#命令)
- [安全写入与所有权](#安全写入与所有权)
- [缓存与离线行为](#缓存与离线行为)
- [Provider 能力](#provider-能力)
- [开发](#开发)

## 安装、更新与卸载

从 npm 安装到当前 Pi 用户的全局 Pi 环境（默认且推荐）：

```bash
pi install npm:pi-model-doctor
```

这会把包写入用户 Pi 包目录，并加入 `~/.pi/agent/settings.json`，因此同一用户的其他项目也可以使用。只有需要项目级固定版本时，才安装到当前项目的 `.pi/settings.json`：

```bash
pi install npm:pi-model-doctor -l
```

更新已安装的包：

```bash
pi update npm:pi-model-doctor
```

从安装时对应的作用域卸载：

```bash
# 全局/用户级安装
pi remove npm:pi-model-doctor
# 等价命令
pi uninstall npm:pi-model-doctor

# 项目级安装
pi remove npm:pi-model-doctor -l
```

安装或卸载后，请重启 Pi 或开启新会话，使扩展注册信息重新加载。卸载扩展不会删除已经写入 `models.json` 的模型；如需删除配置条目，请使用 `/model-doctor remove <provider/model>`，并保留时间戳备份以便回滚。

## 全局安装与项目级覆盖

本仓库的 `.pi/settings.json` 只包含本项目的开发工具配置。全局安装 npm 包后，不需要再添加 Model Doctor 的 `../index.ts`。Pi 全局设置会通过包根目录清单加载 npm 包：

```json
{
  "packages": ["npm:pi-model-doctor"]
}
```

只有明确执行 `pi install npm:pi-model-doctor -l` 时，才会把包引用写入当前项目的 `.pi/settings.json`，形成项目级覆盖。不要同时保留全局 npm 包和本地 `../index.ts` 条目，否则扩展可能被加载两次。

## 本地开发检出

用于本仓库开发冒烟测试时，`.pi/settings.json` 中与 Model Doctor 相关的条目是：

```json
{
  "extensions": ["../index.ts"]
}
```

Pi 会以项目的 `.pi/` 目录作为 `.pi/settings.json` 相对路径的基准；因此如果开发时使用 `../index.ts`，它会指向仓库根目录的 `index.ts`。但普通全局 npm 安装不需要这条本地路径。对于其他 Pi 项目，可通过 `pi install npm:pi-model-doctor` 安装已发布包，或将包目录加入该项目的 Pi settings。从 npm/Git 安装时**不会**使用 `../index.ts`；Pi 会读取包根目录清单中的 `pi.extensions: ["./index.ts"]`。

### 为什么实现位于仓库根目录

`.pi/` 是 Pi 的项目级运行时/配置命名空间，而面向 GitHub/npm/pi.dev 发布的包需要独立的包根目录。实现位于仓库根目录，`package.json`、`README.md`、`LICENSE`、`src/`、测试和 Pi 清单可以一起打包。项目级 `.pi/settings.json` 指向 `../index.ts` 仅用于本仓库的开发冒烟测试；从 npm 或 Git 安装时，Pi 会根据包自身的 `pi.extensions` 清单，从包根目录解析 `./index.ts`。

## 命令

```text
/model-doctor add <provider-id> [endpoint-url] [model] [--metadata-provider <models.dev-provider>] [--api <protocol>] [--api-key <reference>] [--allow-literal-api-key] [--dry-run] [--yes]
/model-doctor configure <provider-id> [endpoint-url] [model] [--api <protocol>] [--api-key <reference>] [--allow-literal-api-key] [--dry-run] [--yes]
/model-doctor list [provider]
/model-doctor check [provider/model]
/model-doctor fix [provider/model] [--dry-run] [--yes]
/model-doctor remove <provider/model> [--dry-run] [--yes]
/model-doctor refresh [--force] [--dry-run]
/model-doctor sync <provider-or-url> [--models <id1,id2>] [--metadata-provider <models.dev-provider>] [--api <protocol>] [--api-key <reference>] [--allow-literal-api-key] [--dry-run] [--yes]
/model-doctor migrate <provider/model> [--to <provider/model>] [--dry-run] [--yes] [--remove-source]
/model-doctor cleanup-backups [--keep <count>] [--max-age-ms <milliseconds>] [--dry-run] [--yes]
/model-doctor rollback <models.json.bak-timestamp> [--dry-run] [--yes]
```

`sync` 会在 dry-run、写入及无变更通知中逐模型报告 metadata 来源。channel discovery 失败后仍按精确模型 id 解析官方 publisher 来源；无效的显式 `--metadata-provider` 会报错，绝不会静默改用 publisher 或保守默认值。它从 models.dev 发现 provider/channel 的全部模型，交互式 UI 通过分页、可搜索的多选器选择多个模型：逐个勾选/反选模型，对搜索结果一键全选或反选、清空选择，以及翻页浏览大列表（每页 10 个），最后选择 Done。操作项（全选/反选/清空、搜索、翻页、Done）排在模型列表之前，在没有列表滚动的终端上也能始终可见。任何选择步骤取消都会以 `not-persisted` 结束，不创建 proposal，也不写入配置。非交互模式必须通过 `--models model-a,model-b` 显式指定；不会隐式选择 catalog 首项或全部模型。sync 是一次组合 proposal：一次确认、一次备份、一次原子写入，所有选中模型一起应用。`sync --dry-run` 不写入 models.json、备份或缓存。交互式 `add` 模型选择器和 `migrate` 目标选择器同样使用分页、可搜索的模式。

`add` 始终以 provider id 开始：`add <provider-id> [endpoint-url] [model]`。第二个位置参数只有在它是 HTTP(S) URL 时才会被视为 endpoint，否则视为 model id；不再支持 URL-first 语法。如果省略 endpoint，provider 必须已经存在于 `models.json`，因此现有 endpoint、API 协议、headers、认证和其他传输字段保持不变。交互式 UI 中省略模型时会通过分页、可搜索的候选选择器展示候选列表；非交互模式必须提供显式 model id。新渠道可以用 `add providerA https://gateway.example/v1 <model>` 创建，也可以先用 `add providerA https://gateway.example/v1` 建立 provider-only，再用 `add providerA <model>` 或 `sync providerA` 添加模型。未在 models.dev 登记的第三方渠道可以使用精确匹配的 models.dev 模型作为 metadata-only。metadata 来源优先级为显式 `--metadata-provider`、目标 provider 的直接 metadata、经官方资料核实的精确 publisher 归属，最后才是唯一但未证实发布者的 catalog 来源。publisher 注册表只负责选择 metadata 来源，不硬编码模型参数；context/output 等参数仍动态读取 catalog。目前仅将精确模型 id `glm-5.3` 根据 [Z.AI 官方模型文档](https://docs.z.ai/guides/llm/glm-5.3) 和 [API 文档](https://docs.z.ai/api-reference/introduction) 映射到 Z.AI；SDK 包名不能证明模型作者。dry-run、写入和无变更通知都会报告每个模型的来源、原因及持久化状态。官方 limits 只是参考，不代表该渠道已验证的运行上限；不会将官方价格/cache 执行能力或 endpoint、API、headers、认证当作渠道事实。无法确认官方来源时会明确标注 catalog-only 或保守默认值，并指出缺失的 context/output 使用了 Pi 保守默认值。模型 id 在多个 models.dev provider 间有歧义时，交互模式会通过分页选择器列出并列的 metadata provider 供选择（等价于显式传 `--metadata-provider`），报错信息也会列出候选 provider id 便于 `--metadata-provider <models.dev-provider>` 消歧。`configure providerA [endpoint-url] [model]` 可修改已有 provider 的 endpoint、API 协议和/或 API-key 引用；省略 model 时还会同步 Model Doctor 管理的模型 API，同时保留用户自有模型的 API。省略 `--api` 时，协议推断依次参考现有配置、catalog/provider metadata、endpoint，最后才使用弱模型名提示；无法判断时默认 `openai-completions`。支持的 API 值为 `openai-completions`、`mistral-conversations`、`openai-responses`、`azure-openai-responses`、`openai-codex-responses`、`anthropic-messages`、`bedrock-converse-stream`、`google-generative-ai`、`google-vertex` 和 `pi-messages`。一步添加渠道模型时，根 URL 仅在 `openai-completions` 或 `openai-responses` 下补全 `/v1`；已有路径、查询参数和 fragment 保持不变；provider-only 不补全 `/v1`。Provider 官网数据不会自动抓取。API 凭据应使用 `$OPENAI_API_KEY`、`${OPENAI_API_KEY}`、`!command` 或 `pi-auth:provider` 等引用；`add --api-key` 本身即授权保存明文，并替换 provider 已有 key；省略时保留旧值，引用原样保存，不执行或展开。`--allow-literal-api-key` 继续兼容，但 add 不再需要它；configure 和 sync 保存明文仍需该 opt-in。扩展不会打印 secret。使用 `--dry-run` 可预览而不写入；非交互写入需要 `--yes`，且 dry-run 优先级更高。`migrate` 接受显式 `--to provider/model`，UI 模式可展示目标候选；非交互模式必须提供 `--to`。

`check` 可离线检查本地文件，网络不可用时使用本地 models.dev 缓存；即使没有 catalog 也会报告本地所有权、元数据和 header findings。slash-command `refresh` 强制读取 catalog 并报告完整配置 findings，不应用修复；它从不写入 `models.json`，普通形式可以更新 catalog/policy 缓存。`refresh --dry-run` 完全只读，也不修改 catalog/policy 缓存。`fix` 只修改 Pi Model Doctor 拥有的字段。如果用户显式修改过 endpoint、header、compat 对象或模型能力，会报告为 conflict 且不会覆盖。`remove` 需要精确的 `provider/model` 目标。`migrate` 从当前元数据创建目标模型，保留安全用户字段，报告 endpoint/API/header 冲突但不复制 secret，默认保留源模型，显式 `--remove-source` 才会删除源；deprecated 目标仅作提示，不能自动应用。无操作的迁移报告无变更且不创建备份。每次修改都会验证 proposal 创建后 `models.json` 是否变化，然后使用备份、原子写入、持久化后 read-back 验证；持久化验证失败会自动恢复。当活动 Pi runtime 使用默认 agent models 路径时，命令会刷新并验证模型注册表，报告 `persisted-and-active`、`activation-failed` 或 `persisted-reload-required`；dry-run 和取消报告 `not-persisted`。备份清理是显式操作：`/model-doctor cleanup-backups --keep <count>` 或 `--max-age-ms <milliseconds>` 在授权后预览/删除旧的时间戳备份；从不自动运行。

## 安全写入与所有权

默认文件是 `join(getAgentDir(), "models.json")`（通常是 `~/.pi/agent/models.json`）。修改命令：

1. 读取现有文件。
2. 文件存在时创建时间戳备份 `models.json.bak-<timestamp>`。
3. 只合并请求的 provider/model。
4. 通过临时文件和原子重命名写入。

受管理条目携带 `_piModelDoctor` 元数据，包含来源、最近检查时间、修复策略、受管理字段和最近受管理值。未知 JSON 字段和用户自有的 headers/endpoint 覆盖保持不变。

迁移默认保留源模型，除非传入 `--remove-source`。不会复制 API key、OAuth、授权/secret headers、endpoint 覆盖或用户自有能力覆盖；冲突时目标值优先。回滚已持久化的修改使用 `/model-doctor rollback <models.json.bak-<timestamp>> [--dry-run] [--yes]`。命令会验证备份是常规、可解析的 models 文件，创建当前文件的独立 safety backup，原子恢复已验证的备份，并在支持时刷新/验证活动模型注册表。如果无法验证激活，重新加载 Pi 并运行 `/model-doctor check`；备份包含原始凭据，必须像 `models.json` 一样保护。

## 缓存与离线行为

本地缓存默认位于：

```text
~/.pi/model-doctor/models-cache.json
~/.pi/model-doctor/providers-cache.json
~/.pi/model-doctor/policies-cache.json
```

`policies-cache.json` 包含 reasoning/cache resolver 使用的版本化能力策略目录。其 baseline 记录 Pi 兼容性基线（`0.82.1`）、models.dev schema 标签（`api.json`）、归一化 schema 版本（`1`）、观测日期、PolicyCatalog schema 版本和 `_piModelDoctor` 元数据版本。无效、不兼容、敏感或权限不安全的数据会被忽略并重新生成。`models-cache.json` 保存归一化完整 catalog；`providers-cache.json` 保存 provider 摘要、环境变量名、所选 adapter、reasoning 控制方式和独立的 prompt/context/KV 能力信号。缓存写入拒绝敏感字段；缓存目录 mode 为 `0700`，缓存文件 mode 为 `0600`。能力结果包含 resolved、partial、advisory、unsupported 或 unknown 状态。内部缺失的 prompt/context/KV 能力证据为 `null`（未知），而非不支持；写入运行时 `models.json` 时省略未知诊断，不输出 null/unknown。完全无证据时省略整个 `cacheCapabilities`/`cacheResolution`，部分证据仅输出已知子项，不猜测否定控制、retention 或 affinity；proposal 和 provider 摘要仍独立保留内部三态。显式 `false` 保留为不支持，显式 `true` 即使没有 Pi adapter 控制字段也保留为服务端能力。未知能力不会生成否定缓存控制字段，也不会盲目开启缓存。fallback 默认零成本不是价格证据，置信度为 low；catalog 明确提供的零缓存价格仍是有效证据。provider 摘要缓存 schema 为 `2`，旧版 `1` 被忽略并重新生成，完整 catalog 和 policy schema 不变。`sync`/`fix` 仅在所有权 snapshot 证明 compat 未被修改时更新它；旧 fallback 否定指纹和中间版本生成的 null/unknown 诊断可按精确 snapshot 安全清理，保留其他扩展控制与无关字段，刷新 snapshot 后重复 fix/sync 无额外变更；无 snapshot、非 managed 或手动 true/false 修改均保留。不依赖或自动配置 pi-cache-optimizer 等扩展。价格元数据不视为启用运行时缓存的授权。每个结果都保留来源和置信度，避免把 provider 事实误认为已验证的 Pi runtime 行为。

可用 `PI_MODEL_DOCTOR_DIR` 覆盖位置；用 `PI_MODEL_DOCTOR_MODELS_PATH` 覆盖配置目标；用 `PI_MODEL_DOCTOR_MODELS_DEV_URL` 覆盖 models.dev endpoint。自定义 endpoint 必须通过相同的 HTTPS/私网策略；显式信任的私有测试基础设施需要 `PI_MODEL_DOCTOR_TRUSTED_ENDPOINT=1`。条件刷新在成功响应未提供新值时保留已有 `ETag` 或 `Last-Modified`，并在写入缓存前拒绝类似凭据或包含换行的响应 validator。刷新是本地优先、故障安全的：网络刷新失败时保留有效旧缓存并报告其已过期。会话级后台刷新默认每 24 小时运行一次，带有限随机抖动以减少多会话刷新风暴；设置 `PI_MODEL_DOCTOR_REFRESH_INTERVAL_MS=0` 可禁用。`PI_MODEL_DOCTOR_REFRESH_JITTER_MS` 可覆盖抖动上限。后台刷新只更新 catalog 缓存并报告 warning；从不修改 `models.json`。

## Provider 能力

缓存和 reasoning 引擎由元数据驱动，而不是有限模型表。它们为 OpenAI-compatible、OpenAI Responses、Anthropic Messages、Google 和未知 provider fallback adapter 归一化 prompt/context/KV-cache 信号以及 reasoning toggle/effort/budget/adaptive 选项。Provider 专属兼容性元数据记录 reasoning 预算、thinking 配置、独立 cache 能力和不支持的 runtime 行为。预算值与输出 token 上限保持区分；未知/建议性能力不会静默呈现为已启用。因此无需修改扩展即可发现新的 models.dev provider。对于第三方渠道，这些只是模型事实和建议性能力数据；不代表第三方传输实现了相同的 runtime 行为。

## 开发

在仓库根目录：

```bash
npm install
npm run typecheck
npm test
npm pack --dry-run
```

本仓库不会把 `@earendil-works/pi-coding-agent` 安装或固定在 `devDependencies` 中，而是将 Pi 声明为可选的宿主 peer。开发脚本会解析机器上已经安装的 Pi，并使用该版本执行类型检查和测试；如果无法自动找到 Pi，可设置 `PI_HOST_PACKAGE` 为已安装的 `@earendil-works/pi-coding-agent` 包目录。这样本地检查使用的就是实际加载扩展的 Pi 版本，开发辅助脚本也不会进入发布包。

安装到其他 Pi 项目：

```bash
pi install npm:pi-model-doctor
```

或从本地检出测试：`pi install /absolute/path/to/pi-model-doctor`。

领域模块刻意保持轻依赖，可用临时目录和 mocked `fetch` 实现测试。
