190 lines
12 KiB
Markdown
190 lines
12 KiB
Markdown
# 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、完整响应或秘密;
|
||
- 采集模块只输出数据模块定义的事实,不直接生成页面聚合。
|