12 KiB
CPA 数据采集模块
1. 定位
CPA 数据采集模块是 CLIProxyAPI 与 数据模块 之间的适配层。它负责从 CPA 的多个入口取得原始碎片,按请求关联、规范化并产出标准事实;它不定义价格、不扣款、不聚合图表,也不决定数据保存多久。
CPA callbacks / response hooks / Management API / provider API
↓
CPA 数据采集模块
↓
RequestRecord / ExecutionRecord / UsageRecord / IdentityRecord / QuotaSnapshot
目标不是把每个 CPA payload 原样抄进数据库,而是确保两个参考项目实际使用过的数据都存在可靠的取得路径。
2. CPA 中需要声明的插件能力
初期需要以下能力:
| CPA capability | 方法 | 用途 |
|---|---|---|
request_interceptor |
request.intercept_before、request.intercept_after |
建立请求、识别下游 Key、记录路由与最终上游格式/模型、执行准入 |
request_lifecycle_plugin |
request.complete |
接收 succeeded/failed/rejected/canceled 终态并完成或清理请求 |
response_before_translator |
对应翻译前响应方法 | 读取最接近 provider 原始语义的非流式/流式用量 |
response_interceptor |
response.intercept_after |
用 RequestID 将非流式响应关联回请求 |
response_stream_interceptor |
response.intercept_stream_chunk |
用 RequestID 关联流式响应及最终 usage chunk |
usage_plugin |
usage.handle |
获取 CPA 标准用量、上游 credential 身份、时延、TTFT、失败和响应 tier 信息 |
management_api |
management.register、management.handle |
提供同步入口、查询 API 和内嵌 UI |
scheduler |
scheduler.pick |
供 Key→上游账户路由模块选择 auth;不是用量采集本身 |
ABI 和 RPC schema 必须在注册时协商。当前参考 CPA 修订的 native ABI 为 1、RPC schema 为 3;实现只能声明实际支持的能力,并为 plugin.register 与 plugin.reconfigure 返回相同能力形状。
3. 一次请求的完整采集过程
3.1 请求进入:建立 Request
request.intercept_before 是请求事实的起点:
- 用
RequestID创建临时请求状态; - 读取
TraceID、caller_scope、request_path、source、source format; - 记录 requested/routed model、stream、generate、reasoning effort、requested service tier;
- 用
caller_scope查询本地下游 credential/account; - 调用 Key 准入与计费余额判断;
- 被拒绝时返回明确的 401/403/429 响应,同时等待终态做幂等清理。
完整 Header 和 Body 只允许在回调期间解析,默认不写入通用存储。caller_scope 是 CPA 根据认证结果 Principal 计算的不可逆稳定标识:插件自管 Key 主路径中 Principal 是稳定 credential_id,兼容 CPA 原生 Key 时才是原生 Key 对应身份。它可以作为请求期关联键,但数据库外键仍应使用本地 Credential ID。
3.2 选择上游账户
CPA 进入 scheduler.pick 时会提供:
- request-scoped headers 与 metadata,其中包含
caller_scope; - provider、model、stream;
- 当前真正可用的 auth candidates;
- 每个 candidate 的 ID、provider、priority、status 和经过安全过滤的 attributes。
路由模块可以按 caller_scope 返回指定 AuthID。选择完成后,request.intercept_after 的 metadata 会包含 selected_auth_id / selected_auth_index,采集模块据此建立 Execution 与上游身份关联。
当前 CPA 在一次 handler lifecycle 内生成一个 RequestID;认证失败重试和模型池尝试会用同一个 RequestID 多次调用 after-auth interceptor。因此每次 after-auth 都应追加带 attempt_no 的 Execution,不能用 RequestID 覆盖上一尝试。TraceID 是父级入口请求/日志关联,不代替 ExecutionID。
3.3 观察上游响应和 Token
必须同时使用两类来源:
usage_plugin:CPA 已经标准化的用量、身份、性能和失败信息;- response hooks:包含 RequestID 的上游响应,用来恢复精确请求关联和 CPA 暂未完整传出的字段。
当前 CPA 的插件 UsageRecord 没有 RequestID,并且插件适配层未完整暴露内部的 request/response service tier 双字段,因此仅靠 usage_plugin 不能构造完全可靠的逐请求账单。
初期采用 cpa-plugin-key-billing 已验证的方式:
- 在翻译前解析 provider 原始 usage;
- 支持 Codex/OpenAI Responses 的 SSE 和非流式响应;
- 用 response ID 关联翻译前无 RequestID 的观察与翻译后带 RequestID 的响应;
- 处理 Claude cache 独立计数、OpenAI cache/output 子集、Gemini reasoning 独立计数等不同语义;
- 对多 chunk 使用覆盖/合并规则,不能把累计值重复相加;
- 未知格式只记录 unclassified,不猜测可计费分项。
同时保留 CPA usage_plugin 数据,用于交叉校验、Credential 身份、TTFT、Latency、failure、executor、provider 和未来 CPA 契约补齐后的主来源切换。
每个 Execution 的多来源观察先规范化为 canonical cumulative usage vector,并保存 source rank、response ID、hash 和本地 revision。相同 vector/hash 的重复 callback 不新增 revision,多来源只择优/校验,不能相加。response hook 一旦得到新的可靠 canonical Usage,必须在返回 CPA 前同步写入 usage_observations/durable inbox;不能只放内存等待异步 request.complete。
当前 ABI 的重要限制:usage.handle 没有 RequestID/AttemptID,而带 RequestID 的 response hooks 通常只能观察最终返回响应。插件可以保存每次 after-auth 的 Execution 事实,却不能把并发环境中的无 RequestID usage 按时间或 Auth 猜配给某次失败重试。第一版只结算能够可靠关联的 Execution Usage;疑似产生消耗但无法关联的失败尝试标为 unmeasured。要完整结算所有重试成本,需要 CPA 后续在 UsageRecord 中加入 RequestID/AttemptID 或提供等价的 attempt lifecycle。
3.3.1 为什么 key-billing 使用 schema 2 和双 hook
该实现是理解“为什么能计费”的参考,也同时是性能反例:
- 它用
response.normalize_before取得 provider 权威 Usage; - 该回调没有 RequestID,于是再用
response.intercept_after/response.intercept_stream_chunk中的 RequestID + response ID 绑定归属; - Codex WebSocket 同协议透传可能没有翻译前回调,因此 v0.3.1 还会在下游 chunk 中读取 response ID/Usage;
- 它固定声明 schema 2,生命周期 DTO 没有读取 host schema,说明实现没有做
min(host, plugin_max)协商; - 当前源码只能证明它在旧契约上实现并保留兼容,不能把作者主观动机写成事实。
schema 2 使当前 CPA 为每个流式 payload chunk 重复附带完整原始/翻译后请求;双 hook 又使一个上游帧可能经过翻译前和翻译后两次同步 DLL RPC。schema 3 可以消除 stream interceptor 的逐 chunk 请求体重传,但 response-before 的请求体和 stream HistoryChunks 仍然存在。因此本项目只复用 response ID 关联与 Token 归一化算法,不直接复用“固定 schema 2 + 无条件双逐 chunk hook”的能力拓扑。
目标契约是让 CPA 在 Usage/final-frame 事件中直接提供 RequestID、Execution/AttemptID 和 ResponseID。宿主契约补齐前,任何过渡采集方案都必须通过 dev.md 13.4 的性能/正确性联合门禁。
3.4 请求终态:提交事实
request.complete 是 Request 的终态信号:
- outcome:succeeded、failed、rejected、canceled;
- status code、error;
- started/completed time;
- RequestID、TraceID、model 和 metadata。
终态处理:
- 封存 Request 与所有 Execution;
- 生成零个或多个 UsageRecord;
- 标记 missing/partial/inconsistent 等质量;
- 交给持久化模块在事务中写入事实;
- 触发计费模块处理可结算用量;
- 立即释放并发槽和大对象;若结算仍为 awaiting/partial,则保留最小持久化关联和有界 response-ID tombstone,供迟到 Usage 补记。
终态回调是异步的,不能假定与 usage/response 回调严格有序或只调用一次。所有提交都必须使用 EventID/IdempotencyKey 幂等。终态等于“请求不再执行”,不等于“所有 Usage 已经到齐”;迟到窗口和最终 unmeasured 规则以计费模块为准。
4. 非请求数据的采集
参考 cpa-usage-keeper,除逐请求回调外还需要同步:
| 数据 | CPA 来源 | 建议触发方式 |
|---|---|---|
| CPA 原生 API Key 目录 | /v0/management/api-keys |
只用于显式兼容/迁移模式;插件自管 Key 主路径读取本地目录 |
| 上游 auth files/runtime | CPA host.auth.list/get_runtime |
交给 upstream-accounts.md 在启动、配置变化和手动命令中同步 |
| Provider API Key 配置 | CPA 各 provider management endpoint | 管理员凭证页面刷新 |
| 模型目录 | /v1/models |
启动/配置变化/手动同步 |
| 上游配额与订阅 | 受限 provider adapter;必要时 host.auth.get + host.http.do |
由上游账户模块手动/有界刷新;不转发 Management Key,不接受任意 URL |
| 请求日志 | /v0/management/request-log-by-id |
管理员按需查看,不批量复制 |
原生插件没有必要复刻 keeper 的 Redis 拉取作为主入口,因为插件已经处于 CPA 进程内并能直接收到回调。Redis/HTTP usage queue 只作为未来的兼容导入或灾难恢复入口,不应与原生回调同时无条件入库造成重复。
5. 临时关联状态
内存中至少维护:
pendingRequests[RequestID];- 未绑定 RequestID 的 response observation,按 response ID 短期保存;
- 已完成 EventID 的有界去重缓存;
- 请求准入时的 account/credential/plan/cycle 快照;
- selected auth 和 execution attempt;
- 每个 attempt 的 Token observation 与质量。
所有临时项必须有 TTL 和定期清理;shutdown 必须幂等释放。不能持锁执行数据库、网络、host callback 或复杂 JSON 解析。
内存 response-ID cache 的默认 TTL 为 24 小时,但 SQLite usage_correlations 的最小 response ID→Request/Execution 映射至少保留 90 天;终态只删除大对象和释放并发。超过持久化关联期后,无 RequestID/ExecutionID 的迟到观察无法可靠归属,必须保持 unmeasured,不能按时间/Auth 猜配。
6. 失败与数据质量策略
- 没有 Token 不等于零 Token,记为
missing; - 不同来源相等则提升可信度,不一致则保留两者来源并标记
inconsistent; - 已产生 Token 的失败/取消执行仍输出 Usage,由计费模块决定收费;
- 被本地准入直接拒绝且未触达上游的请求输出 Request,不伪造 Usage;
- 晚到回调可以补充未封存数据;已记账事实不能被无版本覆盖;
- nested plugin host model callback 必须识别,避免外层请求重复采集与重复计费;
- 原始错误 Body、Prompt、Response、Authorization 和秘密凭证不得写入通用日志。
7. 与参考项目的关系
从 cpa-plugin-key-billing 直接参考:
internal/plugin/intercept.go;internal/plugin/usage_tracker.go;internal/plugin/upstream_usage.go;internal/billing/pending.go。
只参考上述文件的 Usage 语义、response ID 关联和幂等思路;schema 常量、生命周期 DTO、双逐 chunk capability 声明和流式 payload 形状必须按当前 CPA 重新设计。
从 cpa-usage-keeper 参考:
internal/cpa/endpoints.go与internal/cpa/client.go;internal/service/sync.go;internal/service/tokenprocessor/;internal/quota/;internal/cpa/dto/。
CPA 协议事实以当前 checkout 的 sdk/pluginapi/types.go、sdk/pluginabi/types.go、internal/pluginhost/ 和官方 examples 为最高优先级。
8. 验收标准
- 成功、失败、拒绝、取消、流式、非流式和重试都能形成正确 Request 终态;
- 每个可计费用量能关联到下游 Key、上游账户和实际模型;
- OpenAI/Codex cache、reasoning、Fast tier 能被表达且不会重复计数;
- 回调乱序、重复、晚到和插件重启不会造成重复账单;
- 身份、模型、配额和订阅可从 CPA 同步;
- 不保存 Prompt、完整响应或秘密;
- 采集模块只输出数据模块定义的事实,不直接生成页面聚合。