12 KiB
Key 准入与账户路由模块
1. 定位
该模块回答两个实时问题:
- 当前下游 Key 是否允许发起请求;
- 如果允许,这个 Key 应该使用哪个上游 Codex 账户。
它与计费模块及 上游账户模块 协作,但职责不同:计费模块维护金额额度和账本;上游账户模块维护 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 维护内部账户到当前 CPA AuthID 的显式映射,并建立:
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 返回 404;cpa-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 interceptor:CPA 有些经过 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 按固定顺序执行:
- 验证
caller_scope能映射到已认证 credential,并重新检查状态以防认证后的并发变更; - 检查账户是否启用、Credential 是否仍 active;
- 检查允许的 provider/model/endpoint/service tier;
- 查询计费周期、额度和余额;
- 检查并发限制或进行额度预留(若启用);
- 固化 plan/cycle/account 路由快照;
- 允许请求进入 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,配额感知调度以后再做。
数据结构示意:
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. 与计费模块协作
- 计费额度属于 BillingAccount,Key 是 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 委托。