Files
cpa-plugin/patch/README.md
T

149 lines
9.1 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.
# CLIProxyAPI 宿主补丁
这里保存 billing 最初为 CLIProxyAPI `v7.2.133` 制作、已在 `v7.2.139` 重新验证的三项宿主修复。三项补丁都不改变计费规则或数据库结构,必须按下列顺序应用:
1. [`cli-proxy-api-usage-context.patch`](cli-proxy-api-usage-context.patch)
2. [`cli-proxy-api-usage-identity.patch`](cli-proxy-api-usage-identity.patch)
3. [`cli-proxy-api-request-lifecycle-cancel.patch`](cli-proxy-api-request-lifecycle-cancel.patch)
适用基线:
- CLIProxyAPI`v7.2.139`
- 提交:`0a14eb70ce19fac1d114bcdb4a476d61adc819e2`
- Native ABI`1`
- 第二项补丁把 RPC schema 从 `3` 提升到 `4`
升级 CLIProxyAPI 后不要直接假设补丁仍然需要或仍能应用。应先在新版本复测,再根据新版本的宿主契约重新核对。
## 1. 已取消上下文导致动态插件漏记
使用 `/v1/responses` 且请求体为 `"stream": false` 时,上游请求能够成功,CLIProxyAPI 内置用量统计也会增加,但动态用量插件可能完全收不到 `usage.handle`
- billing 没有新增用量事实;
- 用户余额没有扣减;
- 内置统计和动态插件统计不一致。
CLIProxyAPI 会异步分发最终用量记录,但分发任务沿用了原始 HTTP 请求上下文。非流式响应很快结束后,该上下文会被取消;动态插件加载器在调用 C ABI 前检查 `ctx.Done()`,于是跳过插件调用。内置插件不经过动态 C ABI 检查,所以仍能收到记录。
第一项补丁在动态用量适配器入口使用 `context.WithoutCancel`
- 保留上下文值;
- 不再继承已经结束的 HTTP 请求的取消信号和截止时间;
- 每个最终用量仍只调用一次插件,不增加逐块回调;
- `nil` 上下文退回 `context.Background()`
## 2. Usage 缺少请求身份导致并发拆行
CLIProxyAPI 的请求生命周期会生成唯一 Request ID,并通过 `request.complete` 发送给插件,但 schema v3 的 `usage.handle` 没有携带该身份。billing 只能按用户、模型和时间窗口关联两类事实。
串行请求通常可以唯一匹配;同用户、同模型高并发时会出现多个等价候选。billing 为避免错配,保守地保留一条 request-only 和一条 usage-only,因此请求明细数量可能翻倍,但底层用量和扣费并未重复。
第二项补丁引入 RPC schema v4
- 请求生命周期创建后,把 Request ID 和父 Trace ID 写入实际执行上下文;
- `usage.handle` 增加可选的 `RequestID``TraceID`
- 只有协商到 schema v4 的插件收到新增字段;
- schema v1–v3 插件保持原有载荷,能够继续加载;
- billing v4 按 Request ID 精确关联,旧宿主和历史数据仍走保守兼容逻辑。
补丁不虚构 Execution ID。一次生命周期内可能存在多个上游尝试时,只有宿主能够提供真实的逐尝试身份;当前修复只传递已经存在且语义明确的生命周期 Request ID。
## 3. SSE 启动阶段取消可能缺少请求终态
billing 在请求拦截阶段创建并发占用,依赖 CPA 后续发送唯一的 `request.complete` 释放占用。原实现只在同步执行启动完成后,由流消费协程观察执行上下文的取消并发送终态。
如果执行上下文恰好在上游连接、鉴权选择或首段流初始化期间取消,流消费协程可能尚未建立。此时 `request.complete` 必须等待同步启动调用返回;若自定义执行器或异常上游没有及时响应取消,就会表现为并发槽位长期占用。内置 Codex 执行器通常会随 context 立即返回,所以这是生命周期层的防御性加固,不是内置执行器的稳定必现缺陷。
第三项补丁在请求生命周期创建时直接注册上下文取消回调:
- 不依赖执行器或流转发协程是否已经启动;
- 取消后立即发送 `outcome=canceled``status_code=0` 的终态;
- 正常完成时主动撤销取消回调;
- 与原有完成路径共用 `sync.Once`,两条路径竞态时仍只发送一个 `request.complete`
- 不增加轮询、定时器或逐流块处理。
该补丁只能处理已经到达 CPA 执行上下文的取消信号。Windows 客户端通过 WSL localhost 转发访问 WSL 内 CPA 时,客户端断开可能不会立即传递到后端连接;这种情况下 CPA 看不到取消,任何生命周期回调都无法提前触发。生产 Linux 直连和 WSL 内直连不经过该转发层。
## 应用
首次克隆主仓库后,先初始化锁定官方基线的 CPA submodule
```bash
git submodule update --init --recursive
```
然后从主仓库根目录执行:
```bash
git -C .externals/CLIProxyAPI apply --check ../../patch/cli-proxy-api-usage-context.patch
git -C .externals/CLIProxyAPI apply ../../patch/cli-proxy-api-usage-context.patch
git -C .externals/CLIProxyAPI apply --check ../../patch/cli-proxy-api-usage-identity.patch
git -C .externals/CLIProxyAPI apply ../../patch/cli-proxy-api-usage-identity.patch
git -C .externals/CLIProxyAPI apply --check ../../patch/cli-proxy-api-request-lifecycle-cancel.patch
git -C .externals/CLIProxyAPI apply ../../patch/cli-proxy-api-request-lifecycle-cancel.patch
cd .externals/CLIProxyAPI
gofmt -w internal/pluginhost sdk/api/handlers sdk/cliproxy/usage sdk/pluginabi sdk/pluginapi
go test -race ./internal/pluginhost ./sdk/api/handlers ./sdk/cliproxy/usage -count=1
version=v7.2.139-dev
commit=0a14eb70
build_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
CGO_ENABLED=1 go build -buildvcs=false \
-ldflags="-s -w -X main.Version=$version -X main.Commit=$commit -X main.BuildDate=$build_date" \
-o test-output ./cmd/server
help_output="$(./test-output -h 2>&1)"
first_line="${help_output%%$'\n'*}"
test "$first_line" = "CLIProxyAPI Version: $version, Commit: $commit, BuiltAt: $build_date"
rm test-output
```
应用补丁后 submodule 显示工作区修改是预期状态;主仓库只提交 submodule 的官方基线指针和 `patch/`,不提交官方仓库中的派生提交。
billing 必须使用 RPC schema v4 重新构建;旧 billing 动态库会向下协商到旧 schema,无法获得 Usage 请求身份。
## 验证标准
运行时至少交叉核对:
1. 分别发送一次非流式和一次 SSE 请求;
2. 两次请求均只产生一条明细和一次扣费;
3. 并发发送相同用户、相同模型的混合请求;
4. N 个 HTTP 200 最终对应 N 条明细、N 个唯一 Request ID、N 条底层 Usage 和 N 次扣费;
5. CLIProxyAPI 内置队列、billing 和独立 keeper 的增量一致;
6. 等待异步任务稳定后没有迟到重复;
7. 请求正常结束后并发占用回到零;
8. 在上游启动阶段和已开始传输后分别强制中断 SSE,两次都只产生一个 canceled 终态且并发占用回到零;
9. 日志中没有插件错误或 panic。
## 已验证结果
三个补丁与 billing schema v4 使用真实 DeepSeek 上游完成了本地验证:
- `cpads` 有 Key 成功、无 Key 返回 401
- 非流式和 SSE 各一次:2 条明细、2 次扣费,等待 4 秒无重复;
- 12 个混合并发请求:12 次 HTTP 200、12 条合并明细、12 个唯一 Request ID、12 次扣费;
- 流式与非流式各 6 条,终态、状态码、Token 和 Trace ID 完整;
- keeper 同步增加 12 条,等待 5 秒没有迟到重复;
- WSL 内直连分别在 50ms(首字节前)和 1s(流传输中)强制断开:两次均只有一条 canceled 明细,并发占用立即回到零;
- 仅前两项补丁与三项补丁在内置 Codex 执行器上的直连取消 A/B 都能正常终止;第三项补丁额外覆盖执行器启动阻塞且未响应 context 的单元场景;
- 请求正常结束后 `active_requests=0`,空闲 5 秒没有持续 CPU 占用;
- billing 全部测试、billing race 测试及 CLIProxyAPI 相关包 race 测试通过;
- schema v3 对照插件仍能正常注册,新增字段不会发送给旧 schema。
升级到 CLIProxyAPI `v7.2.139` / `0a14eb70` 后,三项补丁可从干净基线按顺序重放,补丁涉及的三个包及其 race 测试通过。`usage-identity` 已适配上游插件 executor 的独立 `execCtx`,保留嵌套执行防重复统计逻辑。宿主全仓测试中的 3 个 Claude 指纹失败和 2 个 `reviewedInPlaceByteWrites` 清单过期失败均已在未应用补丁的纯上游 `0a14eb70` 上原样复现,属于上游例外,详见 [`../docs/operations.md`](../docs/operations.md)。
## 性能影响
三项修复都不增加队列、重试或流式分块回调。每个最终用量和请求终态仍最多调用插件一次;新增工作是创建轻量上下文包装、在 schema v4 JSON 中增加两个短字符串,以及为活跃请求注册一个随正常完成立即撤销的标准库取消回调。并发验证中没有观察到持续 CPU 或内存增长。
## 回滚
源码必须按应用顺序的反方向回滚:
```bash
git apply -R /path/to/billing/patch/cli-proxy-api-request-lifecycle-cancel.patch
git apply -R /path/to/billing/patch/cli-proxy-api-usage-identity.patch
git apply -R /path/to/billing/patch/cli-proxy-api-usage-context.patch
```
部署回滚应恢复升级前保存的 CLIProxyAPI 和 billing 动态库后重启宿主。数据库不需要回滚;schema v4 只让新记录获得精确身份,不改变历史账目。