Files
cpa-plugin/docs/modules/collection.md
T

12 KiB
Raw Blame History

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_beforerequest.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.registermanagement.handle 提供同步入口、查询 API 和内嵌 UI
scheduler scheduler.pick 供 Key→上游账户路由模块选择 auth;不是用量采集本身

ABI 和 RPC schema 必须在注册时协商。当前参考 CPA 修订的 native ABI 为 1、RPC schema 为 3;实现只能声明实际支持的能力,并为 plugin.registerplugin.reconfigure 返回相同能力形状。

3. 一次请求的完整采集过程

3.1 请求进入:建立 Request

request.intercept_before 是请求事实的起点:

  1. RequestID 创建临时请求状态;
  2. 读取 TraceIDcaller_scoperequest_path、source、source format
  3. 记录 requested/routed model、stream、generate、reasoning effort、requested service tier
  4. caller_scope 查询本地下游 credential/account
  5. 调用 Key 准入与计费余额判断;
  6. 被拒绝时返回明确的 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

必须同时使用两类来源:

  1. usage_plugin:CPA 已经标准化的用量、身份、性能和失败信息;
  2. 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

该实现是理解“为什么能计费”的参考,也同时是性能反例:

  1. 它用 response.normalize_before 取得 provider 权威 Usage
  2. 该回调没有 RequestID,于是再用 response.intercept_after / response.intercept_stream_chunk 中的 RequestID + response ID 绑定归属;
  3. Codex WebSocket 同协议透传可能没有翻译前回调,因此 v0.3.1 还会在下游 chunk 中读取 response ID/Usage
  4. 它固定声明 schema 2,生命周期 DTO 没有读取 host schema,说明实现没有做 min(host, plugin_max) 协商;
  5. 当前源码只能证明它在旧契约上实现并保留兼容,不能把作者主观动机写成事实。

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 的终态信号:

  • outcomesucceeded、failed、rejected、canceled
  • status code、error
  • started/completed time
  • RequestID、TraceID、model 和 metadata。

终态处理:

  1. 封存 Request 与所有 Execution
  2. 生成零个或多个 UsageRecord
  3. 标记 missing/partial/inconsistent 等质量;
  4. 交给持久化模块在事务中写入事实;
  5. 触发计费模块处理可结算用量;
  6. 立即释放并发槽和大对象;若结算仍为 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.gointernal/cpa/client.go
  • internal/service/sync.go
  • internal/service/tokenprocessor/
  • internal/quota/
  • internal/cpa/dto/

CPA 协议事实以当前 checkout 的 sdk/pluginapi/types.gosdk/pluginabi/types.gointernal/pluginhost/ 和官方 examples 为最高优先级。

8. 验收标准

  • 成功、失败、拒绝、取消、流式、非流式和重试都能形成正确 Request 终态;
  • 每个可计费用量能关联到下游 Key、上游账户和实际模型;
  • OpenAI/Codex cache、reasoning、Fast tier 能被表达且不会重复计数;
  • 回调乱序、重复、晚到和插件重启不会造成重复账单;
  • 身份、模型、配额和订阅可从 CPA 同步;
  • 不保存 Prompt、完整响应或秘密;
  • 采集模块只输出数据模块定义的事实,不直接生成页面聚合。