16 KiB
上游账户与配额模块
1. 定位
本模块把 CPA 管理的 Codex OAuth/Auth 记录抽象成 cpa-ext 可查询、可绑定、可审计的上游账户目录。
它负责:
- 同步 CPA Auth 的稳定标识、provider、状态、priority 和展示元数据;
- 维护插件内部
UpstreamAccount与 CPA Auth 引用的对应关系; - 记录 scheduler 实际可见性、模型可用性和最近选择结果;
- 按需刷新 Codex 配额、订阅和 reset credits,并标记新鲜度;
- 生成路由模块使用的不可变账户快照;
- 管理上游账户池的成员目录;
- 处理 Auth 新增、替换、消失和重新出现时的 reconciliation。
它不负责:
- 创建或保存 OAuth Token,CPA 仍是上游凭证权威所有者;
- 决定某个下游 Key 最终选择哪个账户,该职责属于 access-routing.md;
- 按上游订阅剩余量给用户计费,用户计费只使用 billing.md 定义的金额;
- 把管理员看到的 Token/百分比/credits 暴露为用户余额;
- 在 scheduler 热路径执行 SQL、网络或配额刷新。
2. 三种身份不能混用
2.1 UpstreamAccountID
cpa-ext 自己生成的内部稳定 ID。所有 Key binding、账户池、统计和审计引用它,而不是文件名、邮箱或数组下标。
2.2 CPAAuthID
CPA auth.Auth.ID,scheduler candidate 使用该值选择账户。当前 CPA 源码说明它应跨重启稳定,但 cpa-ext 仍将它当作外部引用,不直接作为数据库主键。
2.3 CPAAuthIndex
CPA 从 Auth 文件/凭证身份派生的稳定 runtime index。host.auth.list、host.auth.get 和 Management /auth-files 使用该值。它适合同步和配额查询,但 scheduler 返回的是 CPAAuthID,两者必须同时保存并显式映射。
路径、文件名、email、label、account ID 都不是安全主键。OAuth 重新登录、文件迁移或用户改名不能让历史费用转移到另一账户。
3. 数据来源
3.1 CPA host auth callbacks
当前 CPA v7.2.130 提供:
host.auth.list:返回HostAuthFileEntry[];host.auth.get_runtime:按auth_index返回最新 runtime 元数据;host.auth.get:返回物理 Auth JSON,包含秘密,只能用于受限 provider adapter;host.auth.save:写入 Auth 文件,本项目 MVP 不使用。
HostAuthFileEntry 可提供:
id、auth_index、name、type、provider、label;- status、status_message、disabled、unavailable、runtime_only;
- source、updated/refresh/retry 时间;
- email、project/account 展示元数据;
- priority、note、websockets;
- recent success/failed buckets。
主目录同步优先使用 host.auth.list,不读取原始 Auth JSON。
3.2 Scheduler candidates
scheduler.pick 每次只提供当前请求可选的候选:
- Auth ID;
- provider;
- priority;
- host-visible status;
- 经过宿主过滤的非敏感 attributes。
这不是全量账户目录。更关键的是,CPA 只把最高可用 priority tier 交给 plugin scheduler。较低 priority 的健康账户不会出现在 candidates 中,不能因为目录显示“active”就声称 strict binding 可用。
每次 scheduler 调用应更新有界的可见性观察:
(upstream_account_id, provider, model, priority, observed_at, candidate=true)
它用于诊断和 bindability 投影,不在请求热路径写大对象或同步刷新全目录。
3.3 After-auth 与 Usage
- request after-auth 提供实际选中的 Auth ID,用来验证 strict/pool 约束;
UsageRecord.AuthID/AuthIndex/AuthType用于费用和统计归属交叉校验;- Request、Execution 和 Usage 仍按 data.md 的关联规则持久化。
3.4 Codex 配额与订阅
参考 usage-keeper 可取得:
plan_type/ subscription plan;allowed、limit_reached;- primary/secondary window;
- used percent、window seconds、reset after/reset at;
- window usage tokens/cost(若上游返回);
- additional rate limits;
- reset credits available count、到期时间。
这些是上游运营信息,不是 cpa-ext 用户计费事实。缺失字段必须保持 unknown,不能转换为 0。
4. 核心领域对象
4.1 UpstreamAccount
UpstreamAccount
├─ upstream_account_id
├─ provider # MVP: codex
├─ display_name
├─ identity_fingerprint # 不可逆、非秘密
├─ lifecycle_status
├─ operator_enabled
├─ created_at / updated_at
└─ metadata_quality
display_name 可以来自 label 或脱敏 email,但修改它不改变身份。identity_fingerprint 仅用于人工比对,不能由可枚举的短邮箱直接做无密钥 hash。
4.2 CPAAuthRef
CPAAuthRef
├─ upstream_account_id
├─ cpa_auth_id
├─ cpa_auth_index
├─ provider
├─ source_kind # file/runtime
├─ source_name_preview
├─ priority
├─ host_status
├─ disabled / unavailable
├─ next_retry_after
├─ first_seen_at / last_seen_at
├─ ref_status # active/missing/replaced
└─ observation_revision
不能持久化 Auth 文件绝对路径、access token、cookie、api_key 或完整物理 JSON。
4.3 QuotaSnapshot
QuotaSnapshot
├─ upstream_account_id
├─ snapshot_id / revision
├─ provider
├─ subscription
├─ windows[]
├─ reset_credits
├─ observed_at
├─ expires_at
├─ source
├─ quality # fresh/stale/partial/error/unknown
└─ error_class # 脱敏
每个窗口保存原始单位和规范化展示值。上游百分比不能用 cpa-ext 自己统计的金额反推。
4.4 SchedulerVisibility
SchedulerVisibility
├─ upstream_account_id
├─ provider / model
├─ visible_priority
├─ last_candidate_at
├─ last_selected_at
├─ scheduler_visible
├─ bindable
└─ reason
常见 reason:
active;priority_shadowed;disabled;host_unavailable;model_unavailable;quota_exhausted;not_seen_for_model;auth_ref_missing;snapshot_stale。
4.5 UpstreamPool
账户池只保存内部账户成员关系和展示信息:
UpstreamPool
├─ pool_id
├─ name
├─ enabled
├─ members[] # UpstreamAccountID
└─ revision
池内 round-robin/fill-first、fallback 和 strict 语义仍由 Access/Route 执行。本模块只确保成员存在并给出 bindability。
5. 账户同步与 reconciliation
同步流程:
host.auth.list
→ adapter 删除敏感/路径字段
→ normalize CPAAuthRef observations
→ match existing exact CPAAuthID/AuthIndex refs
→ produce add/update/missing candidates
→ transactional reconcile
→ publish immutable directory snapshot
→ emit audit/diagnostic events
匹配规则:
- 同一 provider 下 exact
CPAAuthID命中现有 active ref; - 否则 exact
CPAAuthIndex命中,记录 ID 变化诊断并等待规则确认; - 仅 email/label/文件名相同不得自动合并;
- 新引用创建新
UpstreamAccount或进入人工关联候选; - 一次扫描未出现只标
missing_candidate,经过配置的连续次数/时间后才转missing; - missing 账户历史和 bindings 保留,不能级联删除;
- 替换 Auth 必须由管理员显式把旧账户绑定迁到新 ref,并写审计原因。
同步写入后一次性替换内存目录 Snapshot。scheduler 只读 Snapshot;不得在 Pick 内触发同步。
6. 状态、健康和可绑定性
账户状态至少拆成四层:
| 层 | 回答的问题 |
|---|---|
| lifecycle | 这个内部账户是否 active/missing/retired |
| host eligibility | CPA 是否 disabled/unavailable/cooldown |
| scheduler visibility | 目标模型当前最高 priority candidates 是否包含它 |
| quota freshness | 配额是否 fresh/stale/unknown/exhausted |
bindable=true 至少需要:
- internal account/operator enabled;
- active CPA Auth ref;
- provider/model 符合 Codex MVP allowlist;
- host 未 disabled/unavailable;
- 对目标模型最近可见,且不处于较低 priority shadow;
- 若策略要求配额保护,quota fresh 且未明确 exhausted。
配额 unknown/stale 的默认行为:
- strict binding:如果 CPA candidate 仍存在可以继续选择,但 UI 显示 quota unknown;
- quota-aware pool(后续能力):不得把 stale 当作“余额充足”;
- host 已明确 unavailable/limit reached:不可绑定/不可选择;
- 任何情况下 after-auth 都要验证实际 Auth,不能只信目录快照。
MVP 不修改 CPA Auth 的 disabled 状态。管理员禁用上游账户只影响 cpa-ext 路由目录;真正停用 OAuth 凭证仍在 CPA 管理中心完成,避免两个系统争夺同一权威字段。
7. Priority 的产品规则
当前宿主只向 plugin scheduler 提供最高可用 priority tier,因此第一版固定:
- 所有希望被 cpa-ext 互相路由的 Codex Auth 应配置为相同 CPA priority;
- UI 显示每个账户的 priority、
scheduler_visible和bindable; - strict/preferred/pool 保存前执行模型维度的 bindability 检查;
- 较低 priority 账户返回
priority_shadowed,不允许管理员误以为 strict binding 会生效; - priority 变化触发目录 revision 和绑定影响预览;
- 若未来 CPA 向 scheduler 暴露跨 priority candidates,再通过 capability/version gate 开启跨层路由,不能靠插件猜测隐藏候选。
8. 配额刷新方式
8.1 触发
MVP 支持:
- 管理员手动刷新单个或小批账户;
- management/callback-driven maintenance 在有预算时刷新到期项;
- sidecar 拓扑下的有界低频自动刷新。
双 Go runtime soak gate 通过前,c-shared 动态库不启动长期 quota worker。刷新必须有:
- 全局和 per-account 并发上限;
- 超时、冷却和短期错误缓存;
- 同一 AuthIndex 去重;
- 最大批量;
- 明确状态 queued/running/completed/failed;
- shutdown/配置切换时不遗留永久 queued 状态。
可参考 usage-keeper internal/quota/refresh.go 的任务去重、worker token、错误 TTL 和批量限制,但后台 goroutine 拓扑不能原样搬入 DLL。
8.2 凭证处理
目录同步永远不需要 raw Auth JSON。只有 provider-specific quota adapter 在一次受保护的刷新命令内可能调用 host.auth.get:
- 由已通过 CPA Management 鉴权的固定 route 触发;
- 读取指定
auth_index,不允许调用方传 URL 或 Header; - 在 adapter 内提取最小 Token/Account 字段;
- 仅调用代码内 allowlist 的 HTTPS provider endpoint;
- 通过
host.http.do使用宿主网络策略; - 立即覆盖/释放 raw JSON 和 Authorization 临时缓冲;
- 只把规范化 quota DTO 交给领域层。
禁止把入站 Management Authorization 转发到 CPA loopback /api-call,也禁止把任意 url/header/body 暴露成 cpa-ext 配额接口,否则会形成 SSRF 和凭证代理。
更理想的长期方案是 CPA 提供 provider-scoped quota host callback,让插件永远不读取 OAuth 原文;如果实现该宿主契约,应优先替换 host.auth.get 方案。
9. 持久化与快照
建议表:
| 表 | 用途 |
|---|---|
upstream_accounts |
内部稳定账户 |
cpa_auth_refs |
CPA AuthID/AuthIndex 外部引用 |
upstream_observations |
同步 revision 和状态变化 |
quota_snapshots |
版本化配额事实 |
scheduler_visibility |
模型/priority 可见性投影 |
upstream_pools |
池定义 |
upstream_pool_members |
成员关系 |
quota snapshot 是时间点事实,不能用新值覆盖旧审计。高频 scheduler candidate observation 可以先按账户/模型 upsert 投影并输出重要变化事件,不需要把每次 pick 都永久保存。
10. API 与 UI 输出
管理员账户列表至少返回:
- internal account ID、脱敏名称、provider;
- CPA Auth ID 的安全 preview、AuthIndex 的安全 preview;
- priority、host status、disabled/unavailable;
- scheduler visible/bindable 和 reason;
- subscription、primary/secondary/additional windows;
- quota observed/expires 时间和 quality;
- 绑定的下游 Credential 数、池和最近选择时间;
- missing/replacement 诊断。
默认不返回路径、raw metadata、token、cookie、API key 或完整 Auth ID。需要复制精确 ID 的管理员操作使用内部 upstream_account_id。
用户 API 不返回上游账户身份、配额、邮箱、订阅或 priority;最多返回“服务当前可用/暂不可用”的非敏感业务状态。
11. 故障策略
- 目录同步失败:保留 LKG Snapshot,标 stale 并告警;
- quota 刷新失败:保留最后 snapshot 和明确
stale/error,不写 0; - scheduler snapshot 不可用:已受控 strict/pool 请求 fail closed;
- strict 目标不在当前 candidates:返回 unavailable,不委托 CPA fallback;
- after-auth 发现实际 Auth 不在允许集合:interceptor 以正常 RPC success +
Terminate=true拒绝; - Auth 消失:冻结新绑定,保留历史和现有 policy 供管理员迁移;
- identity 冲突:进入人工 review,不能自动合并费用归属;
- quota 依赖失败不应让已经可靠计费的普通请求免费穿透。
12. 如何参考现有项目
12.1 CLIProxyAPI
协议权威:
sdk/pluginapi/types.go:HostAuthFileEntry、host auth callback DTO、scheduler candidate;internal/pluginhost/auth_callbacks.go:Auth 列表构造、priority 和敏感字段边界;sdk/cliproxy/auth/types.go:Auth ID/Index、状态和 quota runtime;sdk/cliproxy/auth/conductor_selection.go:priority tier 与 scheduler candidate;examples/plugin/host-callback-auth-files/:host callback 调用形状。
官方 auth-files example 同时演示了在未鉴权 resource GET 上执行 host.auth.get/save。那只是能力样例,生产安全模型绝不能复制。
12.2 cpa-usage-keeper
优先参考:
internal/cpa/dto/authfiles/:Auth 文件元数据兼容读取;internal/service/metadata_auth_files.go:身份同步;internal/quota/types.go、codex.go、normalize.go:Codex quota DTO 与规范化;internal/quota/subscription_codex.go:订阅信息;internal/quota/refresh.go、auto_refresh.go:任务、缓存和节流;web/src/components/usage/credentials/:账户、订阅和 quota 展示。
复用 DTO 语义、规范化和错误分类;不能复制其独立服务常驻 worker、持有 CPA Management Key 的拓扑,或把 provider 原始 secret 写入通用实体。
参考基线:CPA f43aad7637ad813745bf7d341acb5663617570c5,usage-keeper d62cad3f345ae574089a14a4ac75cca023c7ead6。
13. 验收标准
- CPA AuthID、AuthIndex 和内部 UpstreamAccountID 不会混用;
- OAuth 重登、文件改名、缺失和恢复不会把历史账务归到错误账户;
- 目录同步不读取或持久化 raw Auth secret;
- scheduler 只读内存 Snapshot,不做 SQL/网络;
- 较低 priority 健康账户明确显示
priority_shadowed且不能误绑定; - strict/pool 每次 after-auth 验证实际账户;
- Codex 主/次窗口、额外限制、订阅和 reset credits 保持 unknown/partial/fresh/stale 语义;
- quota 失败不会被显示为 0 或“无限”;
- quota 刷新去重、限流、超时且不形成 SSRF/凭证代理;
- 用户响应不泄露上游账户信息;
- 账户消失不级联删除 bindings、Execution、Usage 或账本;
- 重启后目录、池、配额 snapshot 和绑定关系可恢复;
- 所有 identity/reconciliation 管理动作都有审计记录。