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

190 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 的终态信号:
- 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](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、完整响应或秘密;
- 采集模块只输出数据模块定义的事实,不直接生成页面聚合。