# 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 只让新记录获得精确身份,不改变历史账目。