# 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 返回 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` 按固定顺序执行: 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. 与计费模块协作 - 计费额度属于 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 委托。