# Provider Onboarding Playbook

日期：2026-05-21

## 目的

这份文档面向两类场景：

1. 需要把一个新的 OpenAI-compatible 国产模型中转接入到 `sub2api-cn-relay-manager`
2. `sub2api` 宿主版本发生变化后，需要快速确认现有导入链路是否仍然兼容，并稳定完成重新导入与验收

它不是当前 gate 文档，也不替代真实宿主验收步骤文档。

分工如下：

- `docs/SOURCE_OF_TRUTH.md`
  - 回答“当前最新真相是什么”
- `docs/PROVIDER_VALIDATION_MATRIX.md`
  - 回答“哪些 provider 已有模板、哪些已有官方 key、哪些已经完成 live 验收”
- `docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md`
  - 回答“标准真实宿主验收怎么跑”
- `docs/REAL_HOST_ACCEPTANCE_LEARNINGS.md`
  - 回答“之前踩过哪些坑”
- 本文
  - 回答“以后要新增 provider / 宿主升级后重跑时，最稳妥的工程步骤是什么”

## 适用边界

本文假设：

1. 目标上游是 OpenAI-compatible 接口
2. 目标宿主仍然是 `sub2api`
3. 控制面仍然遵守当前 PRD 首版范围：
   - 不修改宿主源码
   - 不写宿主数据库
   - 通过宿主管理 API 完成 group/channel/account/subscription 等资源编排

如果未来要接入“非 OpenAI-compatible”上游，这份文档只能复用框架，不能直接照抄字段约定。

## 先记住 8 条不变量

后续所有操作都围绕这 8 条不变量展开。只要其中一条被破坏，导入就容易出现“看起来像通了，实际上没通”的假阳性。

1. provider 的 account 视角模型暴露必须正确
   - `credentials.model_mapping` 必须真实落到宿主 account
   - `GET /api/v1/admin/accounts/:id/models` 必须返回目标 provider 模型，而不是 GPT 默认集

2. channel 的网关视角模型约束必须完整
   - `model_mapping`
   - `model_pricing`
   - `restrict_models=true`
   - `billing_model_source=channel_mapped`
   - 这四项缺一不可

3. access ready 不能只看 `/v1/models`
   - 当前代码已经把 ready gate 提升到 completion 层
   - 必须同时通过 `/v1/models` 和 `/v1/chat/completions`

4. `subscription` 和 `self_service` 的 probe key 语义不同
   - `subscription` 最终 probe key 是宿主 managed key
   - `self_service` 最终 probe key 是普通用户 gateway key

5. `self_service` 普通用户 key 的真实认证方式是 `Authorization: Bearer`
   - 不是 `x-api-key`

6. account test 不能默认回退到 `gpt-5.4`
   - 必须显式传 `provider.SmokeTestModel`
   - 否则会把非 GPT provider 错误判成 failed

7. remote/provider 验收要保留 upstream 直探证据
   - upstream `/models`
   - upstream `/chat/completions`
   - `21-summary.json`

8. shared fresh-host 上 `reconcile=drifted` 不能直接覆盖 import/access 成功结论
   - 先看 `05-import.json`
   - 再看 `07-access-status.json`
   - 最后才解释 `09-reconcile.json`

## 一图理解完整链路

把一次稳定 onboarding 看成 5 个阶段：

1. 定义 provider
2. 落 pack
3. 导入宿主
4. 验证 access
5. 固化 artifact 与经验

只有这 5 个阶段都闭环，后续宿主升级或换 key 时才能快速复用。

## 阶段 1：定义 provider

### 1.1 先判断这个上游是否值得接

最先回答 5 个问题：

1. `base_url` 是否稳定，是否明确兼容 OpenAI-style `/models` 与 `/chat/completions`
2. 是否有至少一个可用于真实 completion 的 key
3. 上游返回的模型名是否稳定，是否可能带 `vendor/model` 前缀
4. 这个 provider 目标走 `self_service`、`subscription`，还是两者都要支持
5. 是否需要独立的 host 兼容性结论，而不是直接复用别的 provider 结论

如果连第 2 条都不满足，就不要把“导入失败”先归因为控制面。

### 1.2 provider 清单里必须关注的字段

至少明确这些信息：

1. `provider_id`
2. `platform`
3. `default_model`
4. `smoke_test_model`
5. `channel_template.model_mapping`
6. `channel_template.model_pricing`
7. `account_template.credentials.model_mapping`

经验规则：

- `smoke_test_model` 不要留空
- `default_model` 和 `smoke_test_model` 可以相同，但不要依赖宿主默认值
- 若 upstream 返回模型名带厂商前缀，要在验收脚本侧考虑归一化，而不是强行修改上游

### 1.3 什么时候要新增模型别名归一化

如果你看到的是这类模型名：

- `deepseek-ai/DeepSeek-V4-Pro`
- `vendor-x/model-y`

而 pack 里写的是：

- `deepseek-v4-pro`
- `model-y`

就不要把 `upstream_models_has_expected_model=false` 直接当失败。先补脚本归一化规则，再看真实 chat 是否通。

## 阶段 2：落 pack

### 2.1 最小改动原则

优先只改下面几类文件：

1. `packs/openai-cn-pack/providers/<provider>.json`
2. 必要时更新 `checksums.txt`
3. 若需要测试临时线路，优先复制出临时 pack，不要先污染正式 pack

不要为了一个新 provider 先改控制面逻辑。先让 pack 描述能力足够完整，再判断是否真的需要动 Go 代码。

### 2.2 pack 完成后的本地静态检查

至少确认：

1. pack 能被 loader 成功读取
2. provider schema 通过
3. checksum 对齐
4. `smoke_test_model` 非空

如果 pack 里已经缺字段，后面的导入问题会越来越难解释。

## 阶段 3：导入宿主

### 3.1 导入前先选 access mode

两条链路语义不同，不要混着看。

`self_service`：

1. 目标用户真实存在
2. 普通用户 key 可用
3. key 已绑定目标标准 group
4. 用户有可用余额

`subscription`：

1. 目标用户真实存在
2. 普通用户 key 可用
3. 目标 group 是 `subscription` 类型
4. 用户有 active subscription
5. key 已绑定该 subscription group

### 3.2 导入时最容易漏掉的三件事

1. account `credentials.model_mapping`
   - 漏掉就会回退 GPT 默认模型集

2. channel `model_pricing`
   - 只有 `model_mapping` 不够

3. account test 的 `provider.SmokeTestModel`
   - 不显式传递就可能被宿主默认拿 `gpt-5.4` 去测

### 3.3 既有 channel 与新建 channel 的处理方式不同

对于新建 channel：

- 创建时就要带全量字段

对于既有 channel：

- 不能假设历史字段完整
- 必须走 update 纠偏

如果 live host 上出现“有 `model_mapping` 但没有 `model_pricing`”，先别急着怀疑当前代码。先确认在线 CRM 进程是不是旧版本。

## 阶段 4：验证 access

### 4.1 必须分三层落证据

每次 onboarding 至少保留这三层：

1. account 单体视角
   - `GET /api/v1/admin/accounts/:id`
   - `GET /api/v1/admin/accounts/:id/models`

2. group / 普通用户聚合视角
   - `GET /v1/models`

3. completion 视角
   - `POST /v1/chat/completions`

任何时候都不要用前一层去替代后一层。

### 4.2 当前 completion-gated 判定标准

对外宣称 ready 之前，至少要满足：

1. `/v1/models` 命中目标 `smoke_test_model`
2. `/v1/chat/completions` 返回成功

否则最多只能说：

- 模型暴露对了
- 还不能说真实调用链路对了

### 4.3 upstream 直探的意义

当 host chat 失败时，必须做 upstream 直探，目的是把问题分成两类：

1. `host_compatibility_gap`
   - host `/chat/completions` 失败
   - upstream `/chat/completions` 成功

2. `upstream_key_quota_issue`
   - upstream 自己就失败

这一步非常关键，因为它决定后续是修控制面、提宿主 issue，还是换 key。

## 阶段 5：固化 artifact 与经验

### 5.1 哪些 artifact 算“最终有效证据”

只有这类证据值得长期保留：

1. latest-head
2. fresh-host
3. 同时包含 import/access/status/reconcile/rollback 或至少 import + access + completion
4. 已能解释失败分类或通过结论

中间大量半失败、参数错误、旧进程产物，不要全部纳入版本库。

### 5.2 文档必须同步的三处

每次完成一次新的 provider onboarding 或宿主版本重适配，至少同步：

1. `docs/EXECUTION_BOARD.md`
2. `docs/SOURCE_OF_TRUTH.md`
3. 本文或 `docs/REAL_HOST_ACCEPTANCE_LEARNINGS.md`

同步原则：

- 执行板写当前真相
- 真相索引写当前优先证据
- playbook / learnings 写可复用方法和误判点

## 宿主版本变更时的稳定重验流程

如果 `sub2api` 宿主版本升级了，不要立刻重跑全套试错。按下面顺序来。

### 第一步：先验证宿主契约有没有变

重点确认 6 个点：

1. `channel` create/update 的字段契约有没有变
2. `accounts/:id/test` 是否仍接受显式模型
3. `/v1/models` 的普通用户认证语义有没有变
4. `/v1/chat/completions` 的普通用户认证语义有没有变
5. subscription key/group/balance 前置有没有变
6. admin bearer/api-key 认证入口有没有变

如果这一步没确认，后面所有“导入失败”都没有解释力。

### 第二步：只挑一个最稳定的 provider 先复验

推荐顺序：

1. 先挑一条历史上最稳定、completion 已验证通过的 provider
2. 先跑 `subscription`
3. 再跑 `self_service`

原因很简单：

- 这样可以先区分“宿主整体契约变了”还是“某个 provider 自己有问题”

### 第三步：再放大到 provider matrix

当基线 provider 通过后，再去补：

1. 新 provider
2. 较不稳定中转
3. 多 key / replacement account / reconcile / rollback 场景

## 新增 provider 的推荐最短路径

如果目标是“尽快把一个新 provider 接入并完成真实验收”，建议按这个顺序：

1. 准备 provider JSON
2. 明确 `smoke_test_model`
3. 明确 `model_mapping` 与 `model_pricing`
4. 用临时 pack 跑 dry-run
5. 跑一轮 fresh-host `subscription`
6. 验证三层证据
7. 跑 upstream `/models` 与 `/chat/completions`
8. 若通过，再补 `self_service`
9. 固化 artifact
10. 更新文档板面

不要一开始就：

- 同时改 pack、脚本、Go 逻辑、多个 provider
- 同时在多个宿主环境上跑
- 同时把 self_service 与 subscription 混成一个结论

## 常见失败与最短定位法

### 现象 1：`/accounts/:id/models` 正确，但 `/v1/models` 错

优先检查：

1. key/group 绑定
2. subscription 分配
3. 普通用户余额
4. probe key 语义是否用错

### 现象 2：`/v1/models` 正确，但 `/chat/completions` 失败

优先检查：

1. host `/chat/completions`
2. upstream `/chat/completions`
3. `21-summary.json`

### 现象 3：provider 导入成功，但 account probe 失败

优先检查：

1. `provider.SmokeTestModel` 是否真的传到了 `/accounts/:id/test`
2. SSE `type=error` 是否被正确解析
3. 宿主默认模型是否回退到了 GPT

### 现象 4：CRM 显示 `self_service broken`，但用户 key 直打宿主已经通

优先检查：

1. gateway probe 是否错误用了 `x-api-key`
2. 在线 CRM 进程是否已切到最新修复版本

### 现象 5：刚升级宿主后，最前面的 `create-host/probe-host` 就失败

优先检查：

1. host bearer token 是否过期
2. host admin auth 契约是否变更
3. 不要先把它归因为 provider 本身问题

## 推荐沉淀模板

每新增一个 provider，建议至少补这 6 项记录：

1. provider 名称与 base_url
2. `smoke_test_model`
3. `subscription` 是否通过
4. `self_service` 是否通过
5. upstream `/models` 与 `/chat/completions` 是否稳定
6. 当前已知限制或宿主兼容性差异

## 最后结论

把新增 provider 做稳定，不在于“多快把 key 导进宿主”，而在于这 4 件事是否同时完成：

1. pack 定义完整
2. access gate 真实闭环
3. upstream 失败能分层归因
4. 经验和证据能复用

如果只完成前两项，系统还只是“能跑一次”。
只有四项都完成，后续宿主升级、换 key、补 provider 时才会稳定且快。
