Files
cpa-plugin/patch/README.md
T

9.1 KiB
Raw Blame History

CLIProxyAPI 宿主补丁

这里保存 billing 最初为 CLIProxyAPI v7.2.133 制作、已在 v7.2.135 重新验证的三项宿主修复。三项补丁都不改变计费规则或数据库结构,必须按下列顺序应用:

  1. cli-proxy-api-usage-context.patch
  2. cli-proxy-api-usage-identity.patch
  3. cli-proxy-api-request-lifecycle-cancel.patch

适用基线:

  • CLIProxyAPIv7.2.135
  • 提交:ee2c494788f8a089c58f7ae9aa6ebd1b422211cb
  • Native ABI1
  • 第二项补丁把 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 增加可选的 RequestIDTraceID
  • 只有协商到 schema v4 的插件收到新增字段;
  • schema v1–v3 插件保持原有载荷,能够继续加载;
  • billing v4 按 Request ID 精确关联,旧宿主和历史数据仍走保守兼容逻辑。

补丁不虚构 Execution ID。一次生命周期内可能存在多个上游尝试时,只有宿主能够提供真实的逐尝试身份;当前修复只传递已经存在且语义明确的生命周期 Request ID。

3. SSE 启动阶段取消可能缺少请求终态

billing 在请求拦截阶段创建并发占用,依赖 CPA 后续发送唯一的 request.complete 释放占用。原实现只在同步执行启动完成后,由流消费协程观察执行上下文的取消并发送终态。

如果执行上下文恰好在上游连接、鉴权选择或首段流初始化期间取消,流消费协程可能尚未建立。此时 request.complete 必须等待同步启动调用返回;若自定义执行器或异常上游没有及时响应取消,就会表现为并发槽位长期占用。内置 Codex 执行器通常会随 context 立即返回,所以这是生命周期层的防御性加固,不是内置执行器的稳定必现缺陷。

第三项补丁在请求生命周期创建时直接注册上下文取消回调:

  • 不依赖执行器或流转发协程是否已经启动;
  • 取消后立即发送 outcome=canceledstatus_code=0 的终态;
  • 正常完成时主动撤销取消回调;
  • 与原有完成路径共用 sync.Once,两条路径竞态时仍只发送一个 request.complete
  • 不增加轮询、定时器或逐流块处理。

该补丁只能处理已经到达 CPA 执行上下文的取消信号。Windows 客户端通过 WSL localhost 转发访问 WSL 内 CPA 时,客户端断开可能不会立即传递到后端连接;这种情况下 CPA 看不到取消,任何生命周期回调都无法提前触发。生产 Linux 直连和 WSL 内直连不经过该转发层。

应用

首次克隆主仓库后,先初始化锁定官方基线的 CPA submodule

git submodule update --init --recursive

然后从主仓库根目录执行:

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.135-dev
commit=ee2c4947
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.135 / ee2c4947 后,三项补丁可从干净基线按顺序重放,补丁涉及的三个包及其 race 测试、billing 全仓 race 测试和两个 Linux amd64 产物构建均通过。宿主全仓 go test ./... 有 3 个与补丁无关的 Claude 指纹测试失败;相同失败已在未应用补丁的纯上游 ee2c4947 上复现,详见 ../docs/operations.md

性能影响

三项修复都不增加队列、重试或流式分块回调。每个最终用量和请求终态仍最多调用插件一次;新增工作是创建轻量上下文包装、在 schema v4 JSON 中增加两个短字符串,以及为活跃请求注册一个随正常完成立即撤销的标准库取消回调。并发验证中没有观察到持续 CPU 或内存增长。

回滚

源码必须按应用顺序的反方向回滚:

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