# CPA 数据采集模块 ## 1. 定位 CPA 数据采集模块是 CLIProxyAPI 与 [数据模块](data.md) 之间的适配层。它负责从 CPA 的多个入口取得原始碎片,按请求关联、规范化并产出标准事实;它不定义价格、不扣款、不聚合图表,也不决定数据保存多久。 ```text 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` 是请求事实的起点: 1. 用 `RequestID` 创建临时请求状态; 2. 读取 `TraceID`、`caller_scope`、`request_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](dev.md) 13.4 的性能/正确性联合门禁。 ### 3.4 请求终态:提交事实 `request.complete` 是 Request 的终态信号: - outcome:succeeded、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](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、完整响应或秘密; - 采集模块只输出数据模块定义的事实,不直接生成页面聚合。