611 lines
34 KiB
Markdown
611 lines
34 KiB
Markdown
# 插件运行时与 CPA 集成开发文档
|
||
|
||
## 1. 定位
|
||
|
||
本模块是 `cpa-ext` 与 CLIProxyAPI(CPA)之间的工程适配层。它负责:
|
||
|
||
- native dynamic library ABI;
|
||
- JSON RPC envelope 和方法分发;
|
||
- capability 注册与 schema 协商;
|
||
- CPA callback 到 Core/采集模块的转换;
|
||
- 配置注册、热重配、运行时切换和 shutdown;
|
||
- Management API 与静态资源接入;
|
||
- 构建、动态库检查、安装和宿主集成测试。
|
||
|
||
它必须保持很薄。任何计价公式、余额规则、SQL、聚合算法或路由策略都不应写进 C ABI 入口或 RPC dispatcher。
|
||
|
||
生产安全前提见 [security.md](security.md),安装/升级/恢复见 [operations.md](operations.md),所有 ABI、故障、性能和 soak 证据见 [test-plan.md](test-plan.md)。
|
||
|
||
## 2. 当前兼容基线
|
||
|
||
设计时核对的源码版本:
|
||
|
||
| 项目 | revision |
|
||
| --- | --- |
|
||
| `CLIProxyAPI` | `f43aad7637ad813745bf7d341acb5663617570c5` |
|
||
| `cpa-plugin-key-billing` | `25b534ae386f830f537cca9215cff5586e630b3a` |
|
||
| `cpa-usage-keeper` | `d62cad3f345ae574089a14a4ac75cca023c7ead6` |
|
||
|
||
该 CPA revision 的契约:
|
||
|
||
- minimum host baseline:CLIProxyAPI `v7.2.130`(上述 exact tag/revision);
|
||
- Native ABI:`1`;
|
||
- RPC schema:最高 `3`;
|
||
- 宿主实际调用的 RPC lifecycle:`plugin.register`、`plugin.reconfigure`;`plugin.shutdown` 目前只存在方法常量,没有宿主调用点;
|
||
- CPA module 声明 Go `1.26.0`,当前 cpa-ext module 声明 Go `1.24`;构建矩阵必须固定实际 Go/toolchain 版本并纳入双 runtime soak,不能只比较 `go.mod` 最低版本;
|
||
- stream schema 3 的 payload chunk 不再重复携带 request body,必须在 header-init 或请求 hook 缓存关联所需的最小信息。
|
||
|
||
源码 checkout 才是最终规范。每次升级 CPA 都必须重新核对:
|
||
|
||
- `CLIProxyAPI/sdk/pluginabi/types.go`;
|
||
- `CLIProxyAPI/sdk/pluginapi/types.go`;
|
||
- `CLIProxyAPI/internal/pluginhost/rpc_schema.go`;
|
||
- `CLIProxyAPI/internal/pluginhost/rpc_client.go`;
|
||
- 相同 capability 的官方 example 和测试。
|
||
|
||
ABI 版本与 RPC schema 版本相互独立,不能因为两者当前分别是 1 和 3 就混成一个“插件版本”。
|
||
|
||
## 3. 目标 capability
|
||
|
||
完整产品需要以下 capability,但开发时只声明已经实现并测试的方法:
|
||
|
||
| Capability | RPC 方法 | 适配到内部能力 |
|
||
| --- | --- | --- |
|
||
| `frontend_auth_provider` | `frontend_auth.identifier`、`frontend_auth.authenticate` | 插件自管下游 Key 认证 |
|
||
| `frontend_auth_provider_exclusive` | 注册字段,无独立方法 | 生产模式阻止其他认证 provider 绕过 Core |
|
||
| `request_interceptor` | `request.intercept_before`、`request.intercept_after` | 准入、Request 建立、Execution attempt |
|
||
| `request_lifecycle_plugin` | `request.complete` | 成功/失败/拒绝/取消终态与清理 |
|
||
| `response_before_translator` | `response.normalize_before` | 读取接近 provider 原始语义的 Usage |
|
||
| `response_interceptor` | `response.intercept_after` | 非流式响应与 RequestID 关联 |
|
||
| `response_stream_interceptor` | `response.intercept_stream_chunk` | 流式 response ID/Usage 与 RequestID 关联 |
|
||
| `usage_plugin` | `usage.handle` | CPA 标准 Usage、身份、TTFT、Latency、失败交叉校验 |
|
||
| `scheduler` | `scheduler.pick` | Key/账户绑定到候选 AuthID |
|
||
| `management_api` | `management.register`、`management.handle` | 管理 JSON API、静态 shell、用户只读资源 |
|
||
|
||
不需要声明:model registrar/provider、auth provider、executor、translator、model router 等。`cpa-ext` 初期不替代 CPA 的 Codex 执行器或协议翻译器。
|
||
|
||
### 3.1 分阶段声明
|
||
|
||
建议 capability 开通顺序:
|
||
|
||
1. 当前骨架:`usage_plugin`;
|
||
2. 采集闭环:request interceptor + lifecycle + response hooks;
|
||
3. 自管 Key:frontend auth,并在生产测试完成后打开 exclusive;
|
||
4. 账户绑定:scheduler;
|
||
5. 管理与 UI:management API。
|
||
|
||
未实现的方法不能返回空成功来假装支持。注册字段为 true 后,该 capability 隐含的所有方法都必须有结构化响应和测试。
|
||
|
||
## 4. Native ABI 入口
|
||
|
||
Go c-shared 入口放在 `cmd/cpa-ext/main.go`,使用 `//go:build cshared`。普通测试使用 `main_stub.go` 的 `//go:build !cshared`,确保 `go test ./...` 不要求 CGO。
|
||
|
||
`cliproxy_plugin_init` 必须严格按当前 CPA C 声明填充:
|
||
|
||
```text
|
||
abi_version
|
||
call
|
||
free_buffer
|
||
shutdown (optional but cpa-ext 必须实现)
|
||
```
|
||
|
||
硬规则:
|
||
|
||
- `plugin == nil` 时返回非零;
|
||
- 每次 call 开始先把 response ptr/len 初始化为零;
|
||
- 请求字节若要离开当前函数必须先复制到 Go-owned memory;
|
||
- 返回值使用 C allocator 分配,且只由插件自己的 `free_buffer` 释放;
|
||
- host callback 返回的 buffer 必须由 host 的 free function 释放;
|
||
- 不向 C 返回 Go heap pointer;
|
||
- 不在 ABI 层使用 `os.Exit`、`log.Fatal` 或故意 panic;
|
||
- recovered panic 转换成脱敏 error envelope;宿主自身也会 fuse panic 插件,但插件仍应保护 Core 边界。
|
||
|
||
入口只做:byte copy、envelope marshalling、App 指针读取、C buffer ownership。业务逻辑进入普通 Go dispatcher/Core。
|
||
|
||
## 5. RPC envelope 与本地 DTO
|
||
|
||
所有 RPC 使用:
|
||
|
||
```json
|
||
{
|
||
"ok": true,
|
||
"result": {}
|
||
}
|
||
```
|
||
|
||
或:
|
||
|
||
```json
|
||
{
|
||
"ok": false,
|
||
"error": {
|
||
"code": "stable_code",
|
||
"message": "safe message",
|
||
"http_status": 400,
|
||
"retryable": false
|
||
}
|
||
}
|
||
```
|
||
|
||
未知方法返回 `unknown_method`,不能 panic 或返回裸字符串。
|
||
|
||
支持多个 CPA release 时可以维护插件自己的 wire DTO,避免编译期强耦合宿主 module;但每个字段名、JSON tag、零值和新增字段都必须逐项对照当前 `pluginapi/types.go` 和 `rpc_schema.go`。CPA capability payload 多数嵌入无 JSON tag 的 exported struct,wire 字段通常是 PascalCase;生命周期与 registration wrapper 使用明确 snake_case,不能统一猜测命名规则。
|
||
|
||
## 6. 注册与热重配
|
||
|
||
### 6.1 `plugin.register`
|
||
|
||
宿主传入:
|
||
|
||
- `config_yaml`;
|
||
- host 支持的 `schema_version`。
|
||
|
||
插件执行:
|
||
|
||
1. 解析 lifecycle request;
|
||
2. 协商 `min(host_schema, plugin_max_schema)`;
|
||
3. 严格解析和校验配置;
|
||
4. 初始化或取得 Core Runtime;
|
||
5. 返回 metadata、协商 schema 和已实现 capability;
|
||
6. metadata 的 Name、Version、Author、GitHubRepository 必须非空且稳定。
|
||
|
||
### 6.2 `plugin.reconfigure`
|
||
|
||
`plugin.reconfigure` 返回的 metadata/capability shape 必须与 register 一致。配置更新流程:
|
||
|
||
```text
|
||
parse → validate → build immutable candidate → open/check dependencies
|
||
→ atomic swap Runtime pointer → drain old Runtime
|
||
```
|
||
|
||
首次 `plugin.register` 配置无效时返回错误,不进入 active capabilities。**已经注册后的 `plugin.reconfigure` 不能直接返回错误**:当前 CPA 会因此把该插件从本轮 active records 移除、清除 exclusive provider,可能恢复原生认证。安全做法是保留最后一个有效 Runtime,记录 `reconfigure_rejected` 诊断,并仍以 success envelope 返回上一次有效 metadata/schema/capability shape。只有先修改 CPA 使 reconfigure 失败保留旧 active record 后,插件才可以对无效热配置返回 error。
|
||
|
||
配置分两类:
|
||
|
||
- 热配置:UI 选项、限流阈值、未绑定策略、统计刷新参数;
|
||
- 冷配置:database path、Key HMAC secret 来源、结算币种、插件 identity。
|
||
|
||
冷配置变化默认以上述 LKG 方式拒绝热切换并在诊断/API 提示重启;除非实现了完整的双 Runtime 打开、迁移和原子切换。当前 CPA 替换二进制时会把旧 native 实例放入 retired 集合直到宿主整体 shutdown,不能假定热替换时旧实例已排空。
|
||
|
||
### 6.3 建议配置
|
||
|
||
```yaml
|
||
plugins:
|
||
enabled: true
|
||
dir: plugins
|
||
configs:
|
||
cpa-ext:
|
||
enabled: true
|
||
priority: 100
|
||
codex_only: true
|
||
data_dir: data/cpa-ext
|
||
database_file: cpa-ext.db
|
||
settlement_currency: USD
|
||
frontend_auth_exclusive: true
|
||
unbound_key_policy: deny
|
||
unpriced_usage_policy: deny_new_requests
|
||
```
|
||
|
||
实例 Key 校验 secret 使用环境变量、secret file 或操作系统 secret store,不直接写进该 YAML,也不放进管理 metadata。
|
||
|
||
### 6.4 Schema 协商也是性能契约
|
||
|
||
生命周期 DTO 必须同时读取 `config_yaml` 和宿主传入的 `schema_version`,注册结果使用:
|
||
|
||
```text
|
||
negotiated_schema = min(host_schema, plugin_max_schema)
|
||
```
|
||
|
||
不能把 schema 固定成编译期常量后忽略宿主版本。当前 `cpa-plugin-key-billing v0.3.1` 正是一个反例:
|
||
|
||
- `internal/plugin/types.go` 固定 `SchemaVersion = 2`;
|
||
- 它自己的 `LifecycleRequest` 只定义 `config_yaml`,没有读取 host schema;
|
||
- `registration()` 每次都返回固定 schema 2;
|
||
- README 同时需要兼容最低 CPA `7.2.103`,而其计费闭环使用 schema 2 已具备的 request lifecycle/terminate 能力。
|
||
|
||
因此可以确认“固定 2 是旧契约/兼容实现且尚未适配新性能契约”;无法仅凭源码断言作者的主观动机。当前 CPA `v7.2.130` 的 schema 3 专门解决一项流式放大:payload chunk 不再重复携带 `OriginalRequest` 和 `RequestBody`,只在 `ChunkIndex=-1` 的 header-init 发送一次。由于 Go JSON 会把 `[]byte` 编成 base64,schema 2 在长 Prompt 下会把复制量和编码量放大到“请求体大小 × chunk 数”。
|
||
|
||
本项目规则:
|
||
|
||
- cpa-ext 的生产流式计费最低要求 RPC schema 3;宿主低于 3 时拒绝启用流式计费,不静默回退到高开销 schema 2;
|
||
- register 与 reconfigure 都返回实际协商版本,并用真机测试确认宿主记录的 schema;
|
||
- schema 3 只解决 `response.intercept_stream_chunk` 的重复请求体,**不解决全部流式开销**;
|
||
- 当前 `response.normalize_before` 在每个上游流式帧仍携带 `OriginalRequest`、`TranslatedRequest` 和 `Body`;stream interceptor 还会携带最多 64 个、合计 1 MiB 的 `HistoryChunks`;
|
||
- 插件本地 DTO 即使忽略这些字段,宿主侧 JSON/base64 编码、内存复制和 C ABI 调用已经发生,不能把“没有读取字段”当成零成本。
|
||
|
||
所以 schema 3 是必须的立即缓解,不是完整解决。完整解决需要缩小宿主 callback DTO 或消除逐 chunk callback,见 8.4 和 13.4。
|
||
|
||
## 7. Runtime 与模块装配
|
||
|
||
只保留一个有明确所有权的全局 App 入口:
|
||
|
||
```text
|
||
atomic App/Runtime pointer
|
||
├─ Core facade
|
||
├─ CPA adapters
|
||
├─ repositories
|
||
├─ callback-driven maintenance
|
||
├─ optional sidecar client
|
||
└─ immutable config/catalog snapshots
|
||
```
|
||
|
||
建议目录:
|
||
|
||
```text
|
||
cmd/cpa-ext/
|
||
main.go # cshared ABI
|
||
main_stub.go
|
||
internal/plugin/
|
||
dispatcher.go
|
||
registration.go
|
||
envelope.go
|
||
wire_*.go
|
||
internal/cpaadapter/
|
||
frontend_auth.go
|
||
interceptor.go
|
||
lifecycle.go
|
||
responses.go
|
||
usage.go
|
||
scheduler.go
|
||
management.go
|
||
internal/core/
|
||
internal/domain/
|
||
internal/repository/
|
||
internal/statistics/
|
||
web/ # React/Vite source
|
||
internal/webui/ # go:embed built assets
|
||
```
|
||
|
||
`internal/plugin` 只理解 RPC,`internal/cpaadapter` 只把 CPA DTO 转成内部 command/observation,`internal/core` 不 import `C` 或 CPA wire DTO。
|
||
|
||
### 7.1 P0:双 Go runtime 可行性门禁
|
||
|
||
`cpa-plugin-key-billing/internal/billing/store.go` 记录过实际事故:Go c-shared 插件在 Go 宿主进程中运行自己的 timer/GC/preemption 后,整个 CPA 因 `fatal error: bad flushGen` 崩溃,因此该插件刻意不保留 goroutine。当前 CPA 在 config disable 或二进制热替换时也不会立刻 shutdown 旧 native 实例,旧 goroutine 可能长期存活。
|
||
|
||
所以在实现业务前必须完成 runtime spike/soak,不能先把“动态库内 SQLite + 多个长期 worker”当成已成立:
|
||
|
||
1. 最小 c-shared 插件零长期 goroutine,验证 ABI、并发 callback、GC 压力和反复 register/reconfigure;
|
||
2. 加入选定 SQLite driver 的同步短事务,确认 driver/`database/sql` 是否暗启长期 goroutine/timer;
|
||
3. 分别测试 callback-driven 维护、一个受控 worker、完整 worker 集合;
|
||
4. 覆盖 config disable、无效 reconfigure、binary replacement、plugin fuse、CPA shutdown;
|
||
5. 在目标 Windows DLL 与 Linux/WSL `.so` 上执行并发压力和至少 24 小时 soak,任何宿主 crash、旧实例活动或不可解释 goroutine 增长都判失败。
|
||
|
||
门禁通过前的 MVP 采用 callback/management-request 驱动:事实同步短事务落库,聚合小批追赶、清理和 checkpoint 只在安全宿主调用中有预算地执行;备份和长任务交给外部 sidecar/运维命令。门禁失败时长期聚合、归档、备份、provider refresh 必须永久移到 sidecar,动态库只保留认证/准入/采集/结算的薄同步路径。
|
||
|
||
## 8. CPA 生命周期映射
|
||
|
||
### 8.1 Frontend auth
|
||
|
||
`frontend_auth.authenticate` 收到 method/path/headers/query/body。适配器应:
|
||
|
||
- 在读取/验证 Key 前应用 [access-routing.md](access-routing.md) 的 method + path allowlist,未知入口默认 `Authenticated=false`;
|
||
- 只读取允许的认证 Header;
|
||
- 限制 body/headers 的处理量;
|
||
- 不记录完整请求或 Key;
|
||
- 调用 Core credential authentication;
|
||
- 返回稳定 Credential ID 作为 Principal;
|
||
- 失败返回 `Authenticated=false`,不泄露“Key 是否存在”等枚举信息。
|
||
|
||
生产打开 exclusive 时,当前 CPA 会在多个 exclusive provider 中选择最高 priority,priority 相同按 plugin ID 决定。必须在真实宿主中确认 `cpa-ext` 是唯一生效的认证路径。
|
||
|
||
当前 CPA frontend-auth contract 不能携带插件自定义认证错误。`internal/pluginhost/adapters_auth.go` 会将插件 RPC 错误或 `Authenticated=false` 都转换为 access manager 的 `NotHandled`;如果所有生效 provider 都是 `NotHandled`,manager 返回宿主 `401 no_credentials`。由此得到三条实现约束:
|
||
|
||
- 第一版不要在 API 文档中承诺 `invalid_credential`、disabled/expired 的独立错误码;
|
||
- credential lookup 数据库故障仍然 fail closed,但外部也会看到 `401 no_credentials`,内部必须保留可告警的诊断分类;
|
||
- 如需向客户端准确返回认证服务 `503`,必须升级 CPA frontend-auth wire response/adapter,并更新兼容性矩阵。
|
||
|
||
Core readiness(数据库、HMAC keyring、账本、当前价格政策)不健康时,frontend auth 也返回 `Authenticated=false`,作为 interceptor 之前的第二道 fail-closed;外部仍只看到 401,真实原因进入诊断。
|
||
|
||
当前宿主在调用 frontend auth 前会 `ReadAll` 完整请求 Body 并将 headers/query/body 复制进 RPC,即使插件只检查 Header 也无法在内存分配前拒绝;Management handler 同样先被宿主完整读取。反向代理/CPA HTTP 层必须配置全局请求体上限,frontend-auth 对 Codex 大 Prompt 还要做容量基准。长期应推动 CPA 提供 header-only auth DTO 和宿主级 `MaxBytesReader`;插件自己的长度校验只减少后续解析,不能宣称保护了宿主内存。
|
||
|
||
更关键的是:exclusive 只在插件 active 时存在。当前 CPA 没有 `required plugin` 或“零认证 provider 默认拒绝”的配置;插件缺失/加载失败/被 fuse 时会清除 exclusive,恢复其他 provider,甚至在零 provider 时走 legacy 放行。生产部署必须同时满足:
|
||
|
||
1. CPA 配置一个不分发给用户的 256-bit 以上随机 native sentinel key,确保插件缺失时不是零 provider;
|
||
2. `cpa-ext` 注册 management readiness 路径,报告数据库、HMAC keyring、价格、exclusive/capability 自检结果;
|
||
3. 外部启动器/反向代理在 readiness 成功前不开放用户端口,并在插件丢失/fuse 后撤流量;
|
||
4. 最终推动 CPA 增加 required-plugin + authentication-default-deny;在此之前发布说明必须标注这是部署安全前提,而非动态库自身能力。
|
||
|
||
sentinel 只由部署运维保管,不能交给用户、写进前端或作为日常调用 Key;否则它本身就是计费旁路。
|
||
|
||
### 8.2 Request interceptors
|
||
|
||
- before-upstream-auth(CPA 方法名 before-auth):同步耐久化 Request pending、金额准入、占用并发;
|
||
- after-auth:每次上游尝试记录 selected auth/model/format;
|
||
- metadata 视为只读 JSON-like snapshot;
|
||
- interceptor response 不能任意给后续阶段增加 metadata;
|
||
- nested `plugin_host_model_callback` 必须识别,避免重复计费。
|
||
|
||
当前 CPA 对 interceptor RPC error 或 host-boundary panic 的处理是记录后继续请求。所有预期拒绝以及 DB、账本、价格故障必须返回正常 RPC success envelope,结果为 `Terminate=true`、合法状态码和脱敏错误体;不得 `return error` 表示拒绝。dispatcher 在方法边界 recover,并把 interceptor panic 转为上述 `503` termination。若 panic 已越过插件边界并导致宿主 fuse,当前请求无法由纯插件保证 fail closed,因此必须依赖上一节的部署门禁。
|
||
|
||
### 8.3 Scheduler
|
||
|
||
`scheduler.pick` 只能从宿主提供的 candidate 列表选择 AuthID,或明确 delegate 内置 scheduler。候选已经经过 provider/model/disabled/cooldown/tried 等过滤,插件不能选列表之外的 Auth。
|
||
|
||
宿主把 `Handled=false`、空响应、unknown AuthID 和 scheduler host-boundary panic 当作 unhandled,然后回退内置选择。strict 绑定的拒绝必须由插件内部 recover 后返回 scheduler RPC error;after-auth interceptor 在真正执行前还要复核 selected AuthID。不应承诺 scheduler 自定义 HTTP error body:当前契约通常只能向调用方形成 generic 5xx。
|
||
|
||
当前宿主只会让最高优先级的有效 scheduler 策略实际主导选择,因此所有 Key binding、strict/preferred/pool 和 fallback 规则应在 `cpa-ext` 一个 scheduler 内组合。CPA `home.enabled` 会跳过 plugin scheduler,并使 plugin Management/resource routes 返回 404;cpa-ext 第一版整体不支持 Home,生产启动检查必须拒绝该模式。
|
||
|
||
### 8.4 Response 与 Usage
|
||
|
||
- 翻译前 response 用来解析 provider 权威 Usage;
|
||
- 非流式/流式 response interceptor 用 RequestID 绑定 response ID;
|
||
- schema 3 stream payload chunk 不含重复 request body,关联数据在 header-init/请求阶段缓存;
|
||
- `usage.handle` 当前没有 RequestID,不能单独作为逐请求扣费提交点;
|
||
- 多来源观察保留 provenance 和 quality,不一致时不静默覆盖;
|
||
- callback 可能并发、重复、乱序或迟到。
|
||
|
||
为什么参考插件会同时使用两个同步 response hook:
|
||
|
||
- 翻译前 hook 能看到 provider 权威 Usage,但当前没有 RequestID;
|
||
- 翻译后的 response/stream hook 有 RequestID,可以用 response ID 完成归属;
|
||
- Codex WebSocket 同协议透传时可能不经过翻译前 hook,`cpa-plugin-key-billing v0.3.1` 因而又从下游 chunk 读取 response ID/Usage。
|
||
|
||
这个关联思路可以借鉴,但 capability 组合不能原样复制。当前两个 hook 都在下游交付关键路径同步执行:每个 frame/chunk 要经过宿主结构复制、JSON/base64、C ABI、插件 JSON 解码和返回 envelope。快速判断“这个事件没 Usage”只能节省插件内部解析,无法消除前面的传输成本。
|
||
|
||
采集方案按以下优先级选择:
|
||
|
||
1. 最优:扩展 CPA `UsageRecord`,直接提供 RequestID、Execution/AttemptID、ResponseID、Usage revision/source;插件不注册逐 chunk hook;
|
||
2. 次优:新增只读 usage observer/final-frame callback,只发送 response ID、RequestID 和 Usage,不携带 Prompt、HistoryChunks 或可修改响应的能力;
|
||
3. 过渡:必须使用双 hook 时协商 schema 3,在 header-init 缓存最小关联,并推动 response-before 增加同类“payload frame 省略 request bodies”契约以及 stream capability 的 `needs_history=false`;
|
||
4. 禁止:schema 2 + 双 hook 作为生产默认,或通过时间/AuthID 猜配 Usage 来换取速度。
|
||
|
||
在宿主契约尚未补齐时,是否保留 stream hook 由 13.4 的性能/正确性联合门禁决定,不能只因功能测试能扣到钱就发布。
|
||
|
||
每个 Execution 维护 canonical cumulative usage vector、source rank、response ID 和 hash。只有事务内 CAS 确认 vector 改变时才增加本地 `usage_revision`;相同 callback 重放不产生新 revision,多来源不能相加。response hook 取得新的可靠 canonical Usage 后,必须在返回 CPA 前同步写 SQLite observation/inbox;至少最终 Usage 不能只进入内存 channel。
|
||
|
||
若上游已经产生 Usage 后 SQLite 持久化失败,纯插件无法撤销该上游成本。adapter 记录高严重度诊断、把 Core readiness 置为 unhealthy,并让后续 frontend auth/interceptor 拒绝新请求;不得返回一个会被宿主忽略的 response-hook RPC error并假装已经可靠落库。该故障是管理员对账中的 `persistence_gap`,恢复后需要显式核对,不能猜费。
|
||
|
||
### 8.5 Request completion
|
||
|
||
当前 CPA 定义:succeeded、failed、rejected、canceled。completion 包含 RequestID、TraceID、模型、时间、状态和 metadata,但不包含 Token。
|
||
|
||
宿主以异步、单次且无 durable retry 的方式通知 lifecycle plugin,业务不能依赖调用方 context 仍然存活。适配器应快速完成持久化命令或投递到**已经持久化的 inbox**;不能只放进可能丢失的内存 channel。completion 只负责终态、释放并发和触发结算,不是 Request/Usage 的唯一落库点。启动恢复必须扫描 pending、已保存 Usage 但未结算的 Execution 和未发布 projection/outbox。
|
||
|
||
取消处理按 `billing.md`/`core.md`:释放并发与完成金额核算分离;有 Usage 结算,无 Usage 进入 awaiting,迟到 Usage 追加补记。
|
||
|
||
## 9. Management API 与用户页面限制
|
||
|
||
CPA 当前提供两类 plugin route:
|
||
|
||
### 9.1 Management routes
|
||
|
||
- 挂载于 `/v0/management/...`;
|
||
- 由 CPA Management Key 认证;
|
||
- 支持声明的精确 HTTP method/path;
|
||
- 用于管理员 CRUD、账本、价格、路由、统计和诊断;
|
||
- handler 必须在 DTO 层拒绝过大 body、错误 content type,并限制分页和响应大小;但宿主已先完整读取 body,真正的内存上限必须设置在反向代理/CPA HTTP 层。
|
||
|
||
### 9.2 Resource routes
|
||
|
||
- 挂载于 `/v0/resource/plugins/<pluginID>/...`;
|
||
- 当前只允许 browser GET resource;
|
||
- 不经过 CPA Management Key;
|
||
- 只能声明精确路径,不支持 `:param`、`*`、`..`;
|
||
- 适合静态 HTML/JS/CSS shell,但天然不是安全的管理员 API。
|
||
|
||
因此:
|
||
|
||
- 静态 shell 可以公开;
|
||
- 管理员数据只从 Management routes 取得;
|
||
- 不能把 CPA Management Key 写入 HTML、JS、URL、localStorage 或插件 session;
|
||
- 用户自助页不能直接调用管理员接口。
|
||
|
||
MVP 用户只读数据可以注册少量精确 resource GET JSON 路径,并由插件 handler 自行校验 `Authorization: Bearer <downstream-key>`,只返回该 Credential 的金额投影。Key 只保存在页面内存,不放 URL、cookie 日志或 localStorage,刷新后重新输入。若需要 HttpOnly session、POST 操作、稳定登录和完整 CSRF 模型,应增加经过明确设计的外部 sidecar/public API,或推动 CPA 增加 authenticated user plugin routes;不能假装当前 Management capability 已经提供这类接口。
|
||
|
||
Home 模式下 Management 与 resource route 都不可用;不是只有 scheduler 失效。第一版 readiness 遇到 `home.enabled` 必须失败。
|
||
|
||
### 9.3 Adapter 数据最小化与脱敏
|
||
|
||
CPA wire payload 的秘密面比业务契约大。每个 adapter 在进入 Core 前执行 allowlist/drop:
|
||
|
||
| 入口 | 可能含秘密 | adapter 规则 |
|
||
| --- | --- | --- |
|
||
| frontend auth | Authorization、query、完整 Prompt Body | 只提取允许 Header 和 method/path;验证后丢弃原值,禁止日志/持久化 |
|
||
| scheduler | `Options.Headers` 当前未保证已脱敏 | 路由只读取 host metadata 中 caller_scope;headers 默认全部丢弃 |
|
||
| Management | CPA Management Key、Cookie、完整 body | 进入 Core 前删除 Authorization/Cookie;body 严格 DTO 解码、字段/大小校验 |
|
||
| compatibility usage | `UsageRecord.APIKey` 可能是原始 CPA Key | 在 adapter 内立即映射/HMAC,之后只传 credential/scope;原值不得落库 |
|
||
| failure/response hooks | 错误 body、request/response 正文 | 只提取标准错误类别、response ID、Usage;原始正文默认丢弃 |
|
||
|
||
日志 API 默认只接收 stable IDs、preview、长度和分类,不接受通用 wire DTO。任何调试开关也不得输出 Authorization、Management Key、OAuth/provider token、Prompt 或完整响应。
|
||
|
||
## 10. 后台任务
|
||
|
||
只有通过 7.1 的目标平台 soak gate 后,插件才允许启用有界后台 worker;否则以下任务由 callback-driven runner 或 sidecar 承担:
|
||
|
||
- statistics aggregation;
|
||
- awaiting usage reconciliation;
|
||
- archive/cleanup;
|
||
- backup;
|
||
- catalog/quota refresh。
|
||
|
||
规则:
|
||
|
||
- 每个 worker 接收 Runtime context;
|
||
- queue 有界且可观测,关键事实先落 SQLite;
|
||
- 不持锁执行 host callback、网络或慢 SQL;
|
||
- 单个可选 worker 失败不杀死 CPA;
|
||
- migration/账本等核心失败进入 fail-closed health;
|
||
- reconfigure 不得重复启动同一 worker;
|
||
- shutdown cancel 后有界等待,超时记录错误并继续释放资源。
|
||
|
||
CPA config disable/热替换不会立即调用旧 native shutdown,因此“worker 属于 Runtime”仍不足以保证停止;启用 worker 的版本还必须有 host generation/lease,使失去 active 身份的旧 Runtime 在下一次 lease 检查时自行停机。纯插件拿不到可靠 active lease 时,不得启用长期 worker。
|
||
|
||
## 11. Shutdown
|
||
|
||
当前 CPA revision 只会通过 native function table 的 `shutdown` 真正通知关闭;虽然 ABI 常量中定义了 `plugin.shutdown`,宿主没有调用点,不能依赖它。native shutdown 必须进入一个幂等 `sync.Once`/状态机:
|
||
|
||
1. 拒绝新管理写入与新准入;
|
||
2. cancel worker context;
|
||
3. 有界等待 worker;
|
||
4. 提交已在事务中的必要账本写入;
|
||
5. checkpoint/关闭 SQLite reader 和 writer;
|
||
6. 清除 host callback context 引用;
|
||
7. 重复 shutdown 返回成功。
|
||
|
||
不能无限等待外部网络、不能调用 `os.Exit`、不能在动态库卸载后留下仍访问插件状态的 goroutine。
|
||
|
||
不能依赖 shutdown 处理 config disable 或 binary hot replacement:当前 CPA 可能只摘除 capability/retire 旧库,直至整个 plugin host 关闭才调用旧实例 shutdown。生产更新动态库采用排空并重启 CPA,不支持带长期资源的原地热替换。
|
||
|
||
## 12. 构建、发现与安装
|
||
|
||
Go c-shared 需要目标平台 C toolchain。仅设置 `GOOS/GOARCH` 通常不足以交叉编译 CGO,应在目标系统或可靠对应 toolchain 中分别构建。
|
||
|
||
Linux/WSL:
|
||
|
||
```bash
|
||
gofmt -w cmd internal
|
||
go test ./...
|
||
go test -race ./...
|
||
CGO_ENABLED=1 go build -tags cshared -buildmode=c-shared \
|
||
-o bin/cpa-ext.so ./cmd/cpa-ext
|
||
file bin/cpa-ext.so
|
||
nm -D bin/cpa-ext.so | grep cliproxy_plugin_init
|
||
```
|
||
|
||
WSL 产出的 `.so` 只能给 Linux/WSL CPA 使用,不能放进 Windows CPA。
|
||
|
||
Windows:
|
||
|
||
```powershell
|
||
$env:CGO_ENABLED='1'
|
||
go build -tags cshared -buildmode=c-shared -o bin/cpa-ext.dll ./cmd/cpa-ext
|
||
# 使用 Visual Studio dumpbin /exports 或 llvm-nm 验证 cliproxy_plugin_init
|
||
```
|
||
|
||
CPA discovery 支持:
|
||
|
||
```text
|
||
plugins/<goos>/<goarch>/cpa-ext.so
|
||
plugins/cpa-ext.so
|
||
plugins/cpa-ext-v<version>.so
|
||
```
|
||
|
||
Windows 使用 `.dll`,macOS 使用 `.dylib`。插件 ID 必须与配置 key、文件名 ID 和 management/resource path 一致。版本化文件名不带前导 `v` 的版本字段部分,例如 `cpa-ext-v0.1.0.so`。
|
||
|
||
发布按 OS/architecture 分包,包含 checksum、插件版本、目标 CPA revision/minimum version、ABI/RPC schema、固定构建工具链和 migration 说明;不包含数据库、secret、auth 文件或生成的 `.h`(除非确有消费者)。版本化文件名、Metadata.Version 和 release metadata 必须一致。第一版升级要求排空并重启 CPA,不宣传 live binary hot reload。
|
||
|
||
## 13. 测试顺序
|
||
|
||
### 13.1 普通 Go 测试
|
||
|
||
- RPC envelope、未知方法、畸形 JSON;
|
||
- register/reconfigure 一致性;已注册后的无效 reconfigure 返回 LKG registration、保留旧 Runtime并记录诊断;
|
||
- Core 领域/用例;
|
||
- SQLite transaction、migration、幂等、恢复;
|
||
- 并发认证、准入、终态、迟到 Usage;
|
||
- 各 capability dispatcher;
|
||
- shutdown/reconfigure race。
|
||
- adapter redaction:Authorization/Management Key/Prompt/UsageRecord.APIKey 不进入 Core、日志或数据库。
|
||
|
||
### 13.2 动态库测试
|
||
|
||
- 实际 c-shared build;
|
||
- `cliproxy_plugin_init` export;
|
||
- 文件名/目录 discovery;
|
||
- ABI 不匹配拒绝;
|
||
- response buffer 能被正确释放;
|
||
- shutdown 可重复。
|
||
- 零常驻 goroutine基线、SQLite driver goroutine/timer 清单和双 runtime 压力/soak。
|
||
|
||
### 13.3 CPA 真机端到端
|
||
|
||
按 capability 逐项验证:
|
||
|
||
1. 宿主日志显示 plugin registered、schema 和 capability 正确;
|
||
2. reconfigure 保留数据库且不重复 worker;
|
||
3. 自管 Key 成功,未知/禁用 Key 失败,原生 Key 无法绕过 exclusive;
|
||
4. 非流式与流式 Codex 请求;
|
||
5. strict/preferred/pool Auth 路由;
|
||
6. 成功、上游失败、本地拒绝、客户端取消;
|
||
7. 取消前有 Usage 扣费、无 Usage 标 unmeasured、迟到 Usage 补记;
|
||
8. 重试和重复 callback 不重复扣;
|
||
9. 额度耗尽返回 429;
|
||
10. 管理路由受 Management Key 保护,resource 不泄密;
|
||
11. 重启后余额、账本、pending recovery 和统计 checkpoint 一致。
|
||
12. 写入畸形热配置后 exclusive/LKG 仍 active;插件缺失、fuse 和 Home 模式被 gateway/readiness 阻断;
|
||
13. 枚举全部 authenticated endpoint,allowlist 外入口拒绝;
|
||
14. DB error、interceptor/scheduler panic/invalid response 故障注入不形成可控边界内的免费请求或串号路由;
|
||
15. 大 Prompt/Management body 的代理上限和 frontend-auth 复制成本达到容量目标;
|
||
16. `A & B <x>` Management JSON 往返和 CSV 公式注入防护正确。
|
||
|
||
### 13.4 流式性能回归门禁
|
||
|
||
基准必须在**同一个 CPA、同一个 OAuth 账户、同一模型、同一 effective service tier、同一 transport**下做 A/B,避免把账号、Fast 或网络差异误判成插件开销。固定四组:
|
||
|
||
| 组别 | 目的 |
|
||
| --- | --- |
|
||
| 不加载任何目标插件 | CPA/OAuth 基线 |
|
||
| schema 3 空 hook 插件 | 测量宿主 callback/ABI 固有成本 |
|
||
| 仅采集与关联 | 测量 response parsing、锁和持久化成本 |
|
||
| 完整 cpa-ext | 最终性能与计费正确性 |
|
||
|
||
覆盖:
|
||
|
||
- SSE 与 Codex WebSocket;
|
||
- 1 KiB、128 KiB、1 MiB Prompt;
|
||
- 短输出以及 32、256、1024 chunks;
|
||
- 并发 1、8、32;
|
||
- standard 与 priority/Fast;
|
||
- 成功、上游失败和客户端取消。
|
||
|
||
采集指标:TTFT P50/P95、总时长、tokens/s 或 chunks/s、chunk 间隔、CPU、alloc/GC、插件 RPC 次数/总字节/最大 payload、锁等待,以及最终 canonical Usage/金额。
|
||
|
||
结构性断言:
|
||
|
||
- schema 3 payload chunk 的 `OriginalRequest`/`RequestBody` 为空,完整请求只允许出现在 header-init;
|
||
- 若仍启用 response-before,必须单独统计它重复携带请求体的字节数,不能混入“schema 3 已优化”的结论;
|
||
- `HistoryChunks` 未被业务使用时不得长期作为生产 payload;当前宿主无法关闭时,必须以基准证明成本可接受或先修改宿主;
|
||
- 单个普通 chunk 的目标大小为 `O(chunk)`,不得随 Prompt 或累计历史线性增长;
|
||
- 优化前后响应字节、Usage、金额和取消结算结果一致。
|
||
|
||
默认发布阈值:常规负载吞吐下降不超过 5%,压力负载不超过 10%,P95 TTFT 增量不超过 `max(20ms, 5%)`。任一结构性断言失败,或正确性依赖猜配,均直接判定不通过,百分比阈值不能豁免。
|
||
|
||
## 14. 源码导航
|
||
|
||
CPA 权威源码:
|
||
|
||
| 问题 | 路径 |
|
||
| --- | --- |
|
||
| ABI/schema/method 常量 | `CLIProxyAPI/sdk/pluginabi/types.go` |
|
||
| capability DTO | `CLIProxyAPI/sdk/pluginapi/types.go` |
|
||
| registration wire schema | `CLIProxyAPI/internal/pluginhost/rpc_schema.go` |
|
||
| schema negotiation/RPC adapter | `internal/pluginhost/rpc_client.go` |
|
||
| 加载、配置和 lifecycle | `internal/pluginhost/host.go`、`config.go` |
|
||
| 动态库命名/发现 | `internal/pluginhost/platform.go` |
|
||
| C loading/ownership | `internal/pluginhost/loader_windows.go`、`loader_unix.go` |
|
||
| frontend auth/exclusive | `internal/pluginhost/adapters_auth.go` |
|
||
| scheduler | `internal/pluginhost/scheduler.go` |
|
||
| intercept/lifecycle adapters | `internal/pluginhost/adapters_interceptors.go` |
|
||
| usage/response adapters | `internal/pluginhost/adapters_usage_translation.go` |
|
||
| management/resource routes | `internal/pluginhost/management.go` |
|
||
| Principal → caller_scope | `CLIProxyAPI/sdk/api/handlers/handlers.go`、`sdk/cliproxy/session/identity.go` |
|
||
|
||
优先官方 examples:
|
||
|
||
- `examples/plugin/frontend-auth-exclusive/`;
|
||
- `examples/plugin/request-lifecycle/`;
|
||
- `examples/plugin/scheduler/`;
|
||
- `examples/plugin/usage/`;
|
||
- `examples/plugin/management-api/`;
|
||
- `examples/plugin/codex-service-tier/`。
|
||
|
||
生产结构参考 `cpa-plugin-key-billing/cmd/cpa-key-billing/main.go` 和 `internal/plugin/`,但协议冲突时永远以目标 CPA 源码和同 revision 官方测试为准。
|
||
|
||
## 15. 验收标准
|
||
|
||
- C ABI 入口保持薄且内存所有权正确;
|
||
- register/reconfigure 协商不高于宿主 schema,并返回相同 capability shape;
|
||
- 只声明已实现能力,所有隐含方法均有测试;
|
||
- Core、领域和 Repository 可以在无 CGO 情况下测试;
|
||
- callback 并发、重复、乱序、迟到不会造成状态泄漏或重复账单;
|
||
- production exclusive auth 已通过真实 CPA 验证,不存在认证旁路;
|
||
- 全部 CPA 已认证 endpoint 已枚举,MVP allowlist 外入口用用户 Key 一律失败;
|
||
- 插件缺失、加载失败、host fuse 时 sentinel + readiness gate 能阻止用户流量;
|
||
- interceptor/scheduler 的 error、invalid response 和 panic 已验证不会在插件可控边界内放行;
|
||
- 部署检查拒绝 `home.enabled`,因为 scheduler 与全部插件 UI/API 均不可用;
|
||
- Management API、resource 和用户只读路径权限分离;
|
||
- build 产物导出正确 init symbol,并能被目标 CPA revision 加载;
|
||
- shutdown/reconfigure 不泄漏 worker、数据库连接或 host callback context;
|
||
- Windows/Linux 目标工具链下双 Go runtime/SQLite spike 与 24h soak 通过,或所有长期任务已移入 sidecar;
|
||
- 发布说明准确报告 CPA revision、ABI、schema、capability、构建命令、产物和未执行检查。
|