Files
cpa-plugin/docs/modules/access-routing.md
T

200 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Key 准入与账户路由模块
## 1. 定位
该模块回答两个实时问题:
1. 当前下游 Key 是否允许发起请求;
2. 如果允许,这个 Key 应该使用哪个上游 Codex 账户。
它与计费模块及 [上游账户模块](upstream-accounts.md) 协作,但职责不同:计费模块维护金额额度和账本;上游账户模块维护 CPA Auth、priority、配额和 bindability 快照;本模块读取这些状态和路由策略,产出允许、拒绝或选中上游账户的决定。
## 2. 能否指定 Key 使用某个账户
可以,当前 CPA 已提供原生 `scheduler` capability,不需要修改 CPA。
`scheduler.pick` 能取得:
- request metadata 中的 `caller_scope`
- provider、model、stream
- CPA 已过滤出的当前可用 auth candidates
- candidate 的稳定 `AuthID`、provider、priority、status 和安全 attributes。
插件通过 [upstream-accounts.md](upstream-accounts.md) 维护内部账户到当前 CPA AuthID 的显式映射,并建立:
```text
caller_scope → DownstreamCredential → RoutePolicy → UpstreamIdentity.AuthID
```
当目标 `AuthID` 出现在 candidates 中,scheduler 返回该 ID,CPA 使用指定账户。候选已经经过 disabled、provider、model compatibility、cooldown、tried 等过滤,因此插件不能也不需要强行选择不可用账户。
重要限制:
- CPA 当前只启用最高优先级的一个 scheduler plugin;本插件会占用该能力,必须在内部组合所有调度规则;
- CPA `home.enabled` 会跳过 plugin scheduler,并让 plugin Management/resource routes 返回 404cpa-ext 第一版整体拒绝 Home 模式,不只是禁用账户绑定;
- scheduler 接收的是 `AuthID`,管理 UI 可展示稳定 `AuthIndex`/别名,但落库必须能解析到当前 `AuthID`
- `request_interceptor` 不能修改 metadata,因此不能依靠它注入 `pinned_auth_id`;正确入口是 scheduler
- scheduler candidate metadata 在当前构造中基本为空,路由应依赖 ID、provider、priority、status、attributes 和本地身份目录。
- plugin scheduler 当前只看到 CPA 已选出的最高可用 priority tier,而不是所有健康 Auth;绑定到较低 priority 的账号即使健康也不会出现在 candidates。MVP 要求所有可绑定 Codex Auth 使用相同 CPA priority,管理 UI 明确显示 `scheduler_visible/bindable`;需要跨 priority 绑定时必须先扩展宿主候选契约。
## 3. 准入决策顺序
插件自管 Key 主路径先由 exclusive `frontend_auth_provider` 校验 method/path allowlist、Key 的密码学有效性及 active 状态。未知入口或未知、畸形、pending、disabled、expired、revoked Key 对外统一认证失败,不向未认证调用方枚举具体状态。
当前 CPA revision 的 frontend-auth wire response 只能表达 `Authenticated``Principal` 和 metadata。宿主适配器会把插件 RPC 错误或 `Authenticated=false` 都转换为 `NotHandled`;在本插件是唯一生效的 exclusive provider 时,最终响应是宿主统一的 `401 no_credentials`。因此第一版不能承诺 `invalid_credential``credential_disabled` 或认证依赖故障的自定义 HTTP 状态;如需区分,必须先扩展 CPA 的 frontend-auth 错误契约与适配器。
路径 allowlist 必须在 frontend auth 完成,而不能只依赖 request interceptorCPA 有些经过 frontend authentication 的 Alpha Search、Live、Realtime/client-secret 路由并不进入普通 interceptor/scheduler/usage/lifecycle 链。MVP 支持矩阵如下,默认拒绝未列入口:
| 入口 | MVP | 准入/路由/结算要求 |
| --- | --- | --- |
| `POST /v1/responses` | 允许 | 必须通过完整宿主集成测试 |
| `POST /v1/responses/compact` | 允许 | 必须验证 Usage 与 completion |
| `POST /backend-api/codex/responses` | 允许 | 同 Responses 链 |
| `POST /backend-api/codex/responses/compact` | 允许 | 同 Compact 链 |
| `GET /v1/models` | 条件允许、零金额 | 仅验证无可计费执行后开启 |
| Responses WebSocket、Alpha Search、Live、Realtime/client secret | 拒绝 | 当前链路覆盖不足,存在二级凭证/绕过风险 |
| chat/completions、messages、图片、视频、Gemini/其他未知入口 | 拒绝 | 非初期 Codex 范围或尚未验证 |
认证成功后,`request.intercept_before` 按固定顺序执行:
1. 验证 `caller_scope` 能映射到已认证 credential,并重新检查状态以防认证后的并发变更;
2. 检查账户是否启用、Credential 是否仍 active
3. 检查允许的 provider/model/endpoint/service tier
4. 查询计费周期、额度和余额;
5. 检查并发限制或进行额度预留(若启用);
6. 固化 plan/cycle/account 路由快照;
7. 允许请求进入 CPA auth selection。
拒绝应返回稳定错误码:
| 场景 | HTTP | 错误码 |
| --- | --- | --- |
| 缺少/未知/无效/非 active Key(当前 CPA 认证阶段) | 401 | `no_credentials`(宿主统一返回) |
| 已认证但未绑定本地账户/策略 | 403 | `access_key_unbound` |
| 认证后状态被并发改为不可用 | 403 | `access_credential_inactive` |
| 模型/endpoint 不允许 | 403 | `access_scope_denied` |
| 余额不足 | 429 | `billing_quota_exhausted` |
| 并发已满 | 429 | `access_concurrency_exceeded` |
| 严格绑定账户不可用 | 503 | `upstream_binding_unavailable` |
| 未找到可信价格 | 503 | `billing_price_unavailable` |
只有身份验证失败使用不泄露细节的统一 401;身份已经确认后的策略拒绝、额度耗尽和上游不可用必须使用各自稳定错误码。认证数据库故障同样会被当前适配器折叠为 `401 no_credentials`:系统仍然 fail closed,但必须写内部诊断和告警,不能把该响应误判成真实坏 Key。认证后的 Core/账本依赖故障仍可由 interceptor 返回明确的 `503`
interceptor 的业务拒绝必须返回正常 RPC success envelope,并设置 `Terminate=true`、合法 `StatusCode` 和安全响应体;返回 RPC error 或让 panic 越过插件边界会被当前 CPA 忽略并继续请求。scheduler 的 strict 拒绝必须返回 RPC error`Handled=false`、空值、未知 AuthID 和宿主边界 panic都会回退内置 scheduler,不能用来表达 fail closed。
## 4. Key 状态
Credential 状态固定为:
- `active`:正常使用;
- `disabled`:管理员禁止;
- `expired`:超过有效期;
- `revoked`:永久撤销,仅保留历史;
- `pending`:已创建但尚未启用。
`unbound` 是账户/套餐/路由绑定状态,不是 Credential 认证状态。active Credential 可以先通过身份认证,再由 interceptor 返回 `403 access_key_unbound`;不能把它和未知 Key 一样藏进凭证状态后丢失可管理性。
人工禁用优先于所有自动策略。删除/轮换 Key 不删除账户余额和历史账本;一个账户可以拥有多个 Key。
## 5. 上游路由策略
每个 credential/account 可配置以下策略:
### 5.1 `default`
不指定账户,scheduler 返回 unhandled 或委托 CPA `round-robin` / `fill-first`。适合普通共享池。
### 5.2 `strict`
只允许指定上游账户。目标不在 candidates 时返回 scheduler 错误,不允许 CPA 自动切换其他账号。
dispatcher 必须在插件内部 recover scheduler panic 并转为明确 RPC error。after-auth interceptor 还要在执行前复核实际 `selected_auth_id`;不属于允许集合时 `Terminate=true`。不过如果插件已在宿主边界被 fuse,当前 CPA 仍可能跳过这两层并回退,生产部署必须依赖 dev 模块定义的 required-plugin/startup 门禁,而不能声称纯插件具备绝对隔离。
用途:成本归属、专属账户、隐私隔离、测试账号。
### 5.3 `preferred`
优先指定账户;不可用时按明确 fallback pool 或 CPA 内置策略选择。
必须记录:目标账户、实际账户、是否 fallback、fallback 原因。
### 5.4 `pool`
绑定到一组上游账户,在组内 round-robin、fill-first、最低使用率或健康优先。第一版建议只实现有确定语义的 round-robin/fill-first,配额感知调度以后再做。
数据结构示意:
```go
type RoutePolicy struct {
CredentialID string
Mode string // default, strict, preferred, pool
PrimaryUpstreamID string
FallbackPoolID string
BuiltinDelegate string // round-robin, fill-first
AllowedModels []string
AllowedTiers []string
}
```
## 6. 重试语义
CPA 每次重试都会重新构造候选,并排除已经 tried 的账户:
- strict:第一次指定账户失败后,下一次 pick 中它通常不再是 candidate;插件应拒绝,保证绝不串到别的账户;
- preferred:指定账户不在 candidate 时选择 fallback,并创建新的 Execution
- default/pool:按剩余 candidates 继续选择。
用户视角仍是一条 Request,底层每次账户选择是一条 Execution。每个能够可靠观察和关联的 Execution Token 都必须保留,不能主动用最后一次尝试覆盖前面的事实。当前 CPA `usage.handle` 缺少 RequestID/AttemptID,无法安全关联的失败尝试必须标为 `unmeasured`,不能按时间或 Auth 猜配收费。
## 7. 与计费模块协作
- 计费额度属于 BillingAccountKey 是 Credential;每个 Credential 必须绑定一个 BillingAccount,多个 Key 是否共享额度由绑定关系决定;
- 准入时读取余额,但结算使用实际完成后的金额;
- 准入快照锁定 account、plan、cycle,Key 在执行中被重新绑定不改变本次费用归属;
- 并发请求可能产生有限负余额,后续请求立即拒绝;
- 若未来使用预授权,预留与最终金额差额通过账本释放/补扣;
- 失败/取消只要上游报告用量仍可结算;本地直接拒绝且无上游执行不收费。
这是请求结束后结算的**软金额额度**,不能对用户承诺绝不超额。MVP 每个 BillingAccount 默认并发上限为 1,把最坏超额限制在单个在途请求;管理员可以明确调高。只有加入输入估算、最大输出预占并确认上游硬输出限制后,才可以提供更接近硬上限的模式。
## 8. 管理能力
管理员需要:
- 创建、启用、禁用、撤销和轮换 Key;
- 设置 Key 标签、有效期、模型/tier 范围;
- 绑定账户和金额套餐;
- 选择 default/strict/preferred/pool
- 选择上游账户/池并看到其状态、订阅和配额;
- 看到当前最高 priority tier、`scheduler_visible/bindable`,并阻止绑定宿主永远不会暴露给 scheduler 的 Auth
- 查看“期望账户 vs 实际账户”和 fallback 记录;
- 批量禁用、批量绑定和同步 CPA Key;
- 在修改前预览会影响哪些 Key。
## 9. 安全与故障策略
- Key 明文只在创建时显示一次;事件和日志仅存 credential/public ID 与 preview,不记录明文或 HMAC digest;兼容 CPA Key 只允许记录不可逆 scope
- scheduler 超时、panic 或数据存储不可用时,已受控 Key 应 fail closed,不能绕过额度或账户隔离;
- 配置热更新使用不可变快照原子替换;已注册插件的失败按 dev 文档返回 LKG success registration 并保留上一个有效配置,不能让宿主撤下 exclusive capability
- 准入与 completion 清理必须幂等;
- scheduler 不进行网络请求或慢查询,路由目录保存在内存快照并由数据库更新驱动刷新;
- 管理员紧急禁用应快速进入内存快照,新请求立即生效,已执行请求仍完成账务归属。
## 10. 参考路径与验收
CPA
- `sdk/pluginapi/types.go` 的 Scheduler 类型;
- `internal/pluginhost/scheduler.go`
- `sdk/cliproxy/auth/conductor_selection.go`
- `examples/plugin/scheduler/`
- `examples/plugin/request-lifecycle/`
下游参考:
- `cpa-plugin-key-billing/internal/billing/enforce.go`
- `pending.go``keys.go``plan.go`
- `internal/plugin/intercept.go`
验收场景包括:枚举全部 CPA 已认证路由并验证 allowlist、未知 Key、禁用、过期、余额不足、严格绑定可用/不可用、preferred fallback、重试、模型限制、并发、LKG reconfigure、重复 completion、interceptor/scheduler error 与 panic、插件 fuse/缺失、Home 模式启动拒绝以及 CPA 内置 scheduler 委托。