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

12 KiB
Raw Blame History

Key 准入与账户路由模块

1. 定位

该模块回答两个实时问题:

  1. 当前下游 Key 是否允许发起请求;
  2. 如果允许,这个 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 返回 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 只能表达 AuthenticatedPrincipal 和 metadata。宿主适配器会把插件 RPC 错误或 Authenticated=false 都转换为 NotHandled;在本插件是唯一生效的 exclusive provider 时,最终响应是宿主统一的 401 no_credentials。因此第一版不能承诺 invalid_credentialcredential_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 errorHandled=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. 与计费模块协作

  • 计费额度属于 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.gokeys.goplan.go
  • internal/plugin/intercept.go

验收场景包括:枚举全部 CPA 已认证路由并验证 allowlist、未知 Key、禁用、过期、余额不足、严格绑定可用/不可用、preferred fallback、重试、模型限制、并发、LKG reconfigure、重复 completion、interceptor/scheduler error 与 panic、插件 fuse/缺失、Home 模式启动拒绝以及 CPA 内置 scheduler 委托。