Files
cpa-plugin/docs/modules/upstream-accounts.md
T

16 KiB
Raw Blame History

上游账户与配额模块

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.IDscheduler candidate 使用该值选择账户。当前 CPA 源码说明它应跨重启稳定,但 cpa-ext 仍将它当作外部引用,不直接作为数据库主键。

2.3 CPAAuthIndex

CPA 从 Auth 文件/凭证身份派生的稳定 runtime index。host.auth.listhost.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 可提供:

  • idauth_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
  • allowedlimit_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

匹配规则:

  1. 同一 provider 下 exact CPAAuthID 命中现有 active ref
  2. 否则 exact CPAAuthIndex 命中,记录 ID 变化诊断并等待规则确认;
  3. 仅 email/label/文件名相同不得自动合并;
  4. 新引用创建新 UpstreamAccount 或进入人工关联候选;
  5. 一次扫描未出现只标 missing_candidate,经过配置的连续次数/时间后才转 missing
  6. missing 账户历史和 bindings 保留,不能级联删除;
  7. 替换 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,因此第一版固定:

  1. 所有希望被 cpa-ext 互相路由的 Codex Auth 应配置为相同 CPA priority
  2. UI 显示每个账户的 priority、scheduler_visiblebindable
  3. strict/preferred/pool 保存前执行模型维度的 bindability 检查;
  4. 较低 priority 账户返回 priority_shadowed,不允许管理员误以为 strict binding 会生效;
  5. priority 变化触发目录 revision 和绑定影响预览;
  6. 若未来 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

  1. 由已通过 CPA Management 鉴权的固定 route 触发;
  2. 读取指定 auth_index,不允许调用方传 URL 或 Header;
  3. 在 adapter 内提取最小 Token/Account 字段;
  4. 仅调用代码内 allowlist 的 HTTPS provider endpoint
  5. 通过 host.http.do 使用宿主网络策略;
  6. 立即覆盖/释放 raw JSON 和 Authorization 临时缓冲;
  7. 只把规范化 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;最多返回“服务当前可用/暂不可用”的非敏感业务状态。

接口定义见 api.md,展示结构见 ui.md

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.goHostAuthFileEntry、host auth callback DTO、scheduler candidate
  • internal/pluginhost/auth_callbacks.goAuth 列表构造、priority 和敏感字段边界;
  • sdk/cliproxy/auth/types.goAuth ID/Index、状态和 quota runtime
  • sdk/cliproxy/auth/conductor_selection.gopriority 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.gocodex.gonormalize.goCodex quota DTO 与规范化;
  • internal/quota/subscription_codex.go:订阅信息;
  • internal/quota/refresh.goauto_refresh.go:任务、缓存和节流;
  • web/src/components/usage/credentials/:账户、订阅和 quota 展示。

复用 DTO 语义、规范化和错误分类;不能复制其独立服务常驻 worker、持有 CPA Management Key 的拓扑,或把 provider 原始 secret 写入通用实体。

参考基线:CPA f43aad7637ad813745bf7d341acb5663617570c5usage-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 管理动作都有审计记录。