Expose bounded usage readers through Admin, Store, Runtime, and SDK. Preserve snapshot consistency, coverage metadata, cancellation, and parent-call cleanup. Co-authored-by: Zyy <zyycn@users.noreply.github.com> Co-authored-by: Codex <noreply@openai.com>
Gateway Plugin SDK
用于编写 Codex Proxy 插件的 Rust 合同与可选会话辅助。插件只依赖本包,不依赖宿主 Core、Admin、Store 或 Host。
SDK 包版本为 0.1.0,独立于网关版本,处于实验阶段。能力是否可用取决于宿主支持、清单声明与实际注册
清单、进程协议和各项能力分别版本化,当前版本与声明规则统一见插件清单
开发顺序:编写清单 → 实现能力处理器 → 用 CLI
打包 → 安装、配置与启用。完整可运行示例位于独立仓库
codex-proxy-plugins 的 examples/workbench
使用 AI 协作开发时,可调用仓库技能 $cpr-plugin-dev,按任务定位合同、示例和验证入口
模块与依赖
| 入口 | 职责 |
|---|---|
| 根门面 | 清单、能力声明、调用上下文、消息与帧、公开错误 |
call::frontend_authentication |
数据面认证信封、认证器标识与外部 principal 结果 |
call::model |
宿主模型调用的 canonical 事实、原生 wire 与有界事件编解码 |
call::middleware |
HTTP、WebSocket 消息、公开服务、模型请求、attempt 与上游出站准备的洋葱调用、单次 next 与惰性正文合同 |
call::services |
公开服务的类型化操作、输入输出及错误合同,usage 提供用量查询、维护与计价 |
call::upstream_adapter |
内置 OpenAI / xAI 的受管上游适配器注册、执行、出站与续接合同 |
call::policy |
模型路由、账号调度与重试决策合同 |
call::catalog |
固定 Provider 的直接别名与适配模型注册合同,见模型目录 |
call::data |
最小账号事实与已有额度观测的只读合同 |
call::observation |
请求完成、用量、费用与实际上游 WebSocket 响应事件的统一观察合同 |
call::host |
账号、私有状态、受管 HTTP、模型、亲和、日志及调用内流的宿主回调合同 |
call::management |
管理 API、页面与资源声明、公开回调,以及 CLI 命令与待保存账号 |
client |
通过 io feature 开启的 PluginBuilder、会话、中间件与异步帧收发 |
call/ 按业务能力组织数据,message.rs 定义消息与帧。已有请求链的请求/响应改写、协议转换和思考参数映射使用
call::middleware;需要指定业务上游并解析结果时使用 call::upstream_adapter,作为洋葱链终端复用宿主账号与结算。
下游 WebSocket 改写使用 websocket 中间件;上游发送前的请求准备使用 outbound 中间件,响应观察接口保留只读合同
插件使用的导入路径例如:
use gateway_plugin_sdk::{Frame, Message, PluginFault};
use gateway_plugin_sdk::call::{host::ModelEventBatch, model::ExecutionEvent};
// 需要 Cargo 依赖声明启用 features = ["io"]。
use gateway_plugin_sdk::client::{read_frame, write_frame};
默认 feature 不引入异步运行时。io 除帧编解码外,还提供 PluginBuilder、
PluginSession::accept/run 和 PluginHandler。组合插件从作者清单建立类型化方法,SDK 自动生成
plugin.register;作者不需要复制贡献项或手写方法名、阶段和敏感载荷编解码。
会话管理握手、有界并发、父调用关联、回调、流信用、取消、排空与关闭,容量与计时见并发与期限
HostClient::scan_usage_records 与 query_usage_statistics 提供用量明细扫描和完整分组统计,按批读取与关闭见用量查询
ResponseStream::channel 提供有界流生产入口,ResponseStream::pull 按消费进度驱动生产者;PluginCall 的宿主客户端只代表当前父调用,
不能把一个调用的资源误用到另一个调用。插件程序自行选择运行时和启动入口,不会启动网关。
SessionConfig.maximum_stream_chunk_bytes 默认 1 MiB,只约束单个业务流分块;
流式响应还需满足宿主实际授予的信用窗口。普通调用与回调的正文不受此预算限制。
普通业务处理器仍接收完整正文,内存占用随正文增长;HTTP 与下游 WebSocket 中间件使用惰性正文句柄,透传不复制正文
通信边界
启用 io 时优先用 PluginBuilder 组合业务处理器,再交给
PluginSession::run。仅实现中间件时也可直接使用
MiddlewarePlugin
sequenceDiagram
participant H as 宿主 Runtime
participant P as 插件会话
H->>P: Hello
P-->>H: Ready(相同 incarnation、所需预算声明)
H->>P: 注册能力,核对清单
P-->>H: 注册结果
H->>P: Call(ID、上下文、载荷)
opt 父处理等待宿主回调
P->>H: 回调(关联父调用 ID)
P->>H: Suspend(父处理 Pending)
H-->>P: 回调结果
P->>H: Resume
H-->>P: Resumed(取得执行名额)
P->>H: CallbackConsumed
end
P-->>H: 结果或错误;流以 End 结束
并发与期限
每个宿主插件会话默认允许 64 个执行中的 RPC 调用、64 个初次排队调用和 256 个总在途上下文。 初次排队和恢复排队各限 30 秒;恢复等待占总在途名额,不占初次排队名额
SDK 只在父处理 future 返回 Pending 后报告挂起,Runtime 随即归还执行名额。
收到唤醒后,SDK 请求恢复,取得授权才继续驱动父任务;并行回调只完成一个也遵守这一边界。
等待普通回调及其恢复排队继续计入处理预算;只有父任务挂起且驻留回调全部为 next 时暂停预算。
外部取消、请求期限和排队上限独立生效,调度不保证插件自行创建的线程、任务或锁能够推进
插件侧 SessionConfig.maximum_calls 默认 256,限制包含下游等待和响应流在内的在途调用。
SDK 和宿主的 maximum_callbacks 默认 320、最大 512,统一限制全部宿主回调,包含已返回但尚未消费的结果。
取消或丢弃回调 future 会释放该回调;宿主不保证已发出的业务操作可以撤销,不自动重试或补偿。
已返回响应头的流继续占用总在途预算,不占首结果执行名额,由正文或连接的 owner 管理真实寿命
实际容量由宿主预算与 SDK 会话配置共同限制,较小的插件侧上限会提前拒绝新调用。
达到容量或等待上限时,中间件返回过载错误;HTTP 响应为 503(middleware_capacity_unavailable),
不跳过插件或重放请求。调整 SDK 会话配置需重新构建插件包,容量应结合插件耗时、正文和响应流的资源占用评估
声明调用预算
插件在 PluginSession::accept 后、run 前通过 set_call_budgets 声明各阶段所需的毫秒预算,可读取
session.handshake().configuration,由自身配置决定模型、批量任务等场景的时长
use gateway_plugin_sdk::{CallBudgets, Stage};
use gateway_plugin_sdk::client::{PluginHandler, PluginSession, SessionConfig, SessionError};
# async fn configure<R, W, H>(reader: R, writer: W, handler: H) -> Result<(), SessionError>
# where R: tokio::io::AsyncRead + Unpin + Send + 'static,
# W: tokio::io::AsyncWrite + Unpin + Send + 'static, H: PluginHandler {
let mut session = PluginSession::accept(reader, writer, SessionConfig::default()).await?;
let maintenance_ms = session.handshake().configuration["maintenanceBudgetMs"]
.as_u64()
.unwrap_or(600_000);
session.set_call_budgets(CallBudgets::from([
(Stage::Maintenance, maintenance_ms),
(Stage::CommandLine, 1_800_000),
]))?;
session.run(handler).await?;
# Ok(())
# }
未声明的阶段使用协议默认值,默认表由 SDK 集中维护,宿主与插件使用同一份合同。 默认值不限制插件显式声明的最大时长
| 阶段 | 默认预算 |
|---|---|
registration |
5 秒 |
configuration、authentication |
30 秒 |
routing、scheduling、retry、observation |
2 秒 |
maintenance、management、public_management、command_line |
120 秒 |
http、service、websocket、request、attempt、outbound、upstream |
120 秒 |
Ready.call_budgets 携带显式声明,双方按相同规则解析并冻结该会话预算。预算必须为正的有限毫秒值,
不接受零值、数值溢出和平台无法表示的期限。修改声明需要创建新会话,不支持调用中无限续期
每次 CallContext.timeout_ms 是该调用的初始处理预算,外部剩余期限较短时可以小于会话预算。
call.host.remaining_budget() 读取共享期限中的剩余值,正确反映 next 暂停;资源流 owner 接管寿命后返回 None。
普通模型和网络回调消耗父调用剩余预算,不会重置预算。请求总期限、网络操作期限、停用和取消仍独立生效,
声明长预算不保证模型必然完成,也不提供跨宿主重启恢复;检查点和业务幂等由插件维护
宿主关闭使用独立宽限并取消未完成调用,不等待插件处理预算耗尽。 注册与配置迁移位于实例准备链路,其实际耗时仍会影响宿主启动和实例就绪
帧与会话
CallContext.timeout_ms 限定初始处理期限。宿主对由流或连接 owner 管理的调用设置 resource_stream:
首个流式结果成功返回后,正文、会话及关联回调由资源取消与终态回收,不再沿用初始期限。
普通调用继续使用原期限;资源模式仍保留并发容量、Credit、父调用关联及取消约束
协议 v7 的帧头由大端 u32 元数据长度和大端 u64 载荷长度组成,随后依次传输 JSON 元数据和二进制载荷。
元数据保留 64 KiB 上限;正文总量不由插件传输层额外限制。传输内部以 256 KiB 为单次 I/O 块,
不逐块发起 RPC,也不要求插件配置与宿主同步。读取按进度扩展缓冲区;截断正文必须报错,不能作为完整消息交付
JSON 元数据只承载消息类别、调用关联及分段长度。载荷依次包含入口快照 JSON、业务 JSON 和原始二进制正文:
request_context_bytes指定CallContext.request的快照长度;无快照时省略data_bytes指定Hello.configuration、Call.params、Callback.params或Result.result的 JSON 长度;这些消息必须携带该段,其他消息省略- 两个 JSON 段之后的剩余字节是
Frame.payload,其内容不经过 JSON 编码
入口快照的 headers[].value 使用标准带填充 base64 字符串,保留重复头与任意字节;Rust API 仍为 Vec<u8>。这个表示法仅用于 RequestContext,不改变中间件业务数据的头值合同
SDK 帧读写器自动拆装这些字段,不拼接复制二进制正文。Message 与 Handshake 的 serde 表示只包含控制字段;完整消息应通过 read_frame / write_frame 读写
帧写入队列前用 EncodedFrame::new 完成编码和校验,写泵通过 EncodedFrame::write 复用编码结果,
避免重复序列化或让本地输入错误关闭共享传输。编码帧不保留业务 JSON 与入口快照的原始副本,
EncodedFrame::message 仅供关联消息类别和 ID。只需校验时可使用 validate_frame。
协议校验不会自动重试业务调用;部分读写中断后不能把当前位置当作新帧边界。
底层帧读写方法不是取消安全的;自行编排会话时,不能在 select! 的其他分支完成后
重建半途中的帧读写。PluginSession 会在处理调用完成通知时保留同一个读取 future,直到完整帧到达或会话关闭
RPC v7 的调度控制消息没有载荷:Suspend { id, epoch } 归还执行名额,Resume { id, epoch }
请求同一轮恢复,宿主 Resumed { id, epoch } 授权后才能继续首结果前的处理。epoch 从 1 递增,
每次挂起与恢复严格配对。插件发出的控制消息和业务帧使用同一有界 FIFO,保证回调登记先于挂起
CallbackConsumed { id, parent_id } 表示回调结果已消费或 future 已丢弃,宿主据此释放驻留额度并取消尚未完成的回调。
结果接收本身不恢复父任务、不释放驻留额度;父调用终结统一回收剩余回调。
取消、恢复授权和结果可能交错,终结后的消息不得重新打开调用
宿主只接受 RPC v7,插件须校验 Hello 的协议版本并遵守上述挂起、恢复和回调回收合同。
一次 Call 的结果、错误和流帧按 ID 关联;实例、代次、阶段、账号或资源 ID 用于关联调用和管理资源生命周期
本地验证
以下命令面向宿主仓库中的 SDK 维护。独立插件的构建、安装与能力验收见开发技能及所用示例
生成服务合同
设置与账号自动重置服务的 SDK 类型和操作由宿主领域类型及操作声明生成,SDK 构建不依赖宿主源码。 修改宿主合同后,在仓库根目录更新生成文件;Admin 合同测试会检查生成结果是否同步:
CPR_UPDATE_SERVICE_CONTRACT=1 RUST_MIN_STACK=16777216 cargo +1.99.1 test --manifest-path backend/Cargo.toml -p gateway-admin --test main sdk_settings_contract_matches_host_declarations --locked
CPR_UPDATE_SERVICE_CONTRACT=1 RUST_MIN_STACK=16777216 cargo +1.99.1 test --manifest-path backend/Cargo.toml -p gateway-admin --test main sdk_accounts_contract_matches_host_declarations --locked
cargo +1.99.1 fmt --all --manifest-path backend/Cargo.toml
用量服务合同由 Admin 与 SDK 分别持有,sdk_wire_types_match_host_contracts 检查数值、明细、聚合、查询和计价类型一致性;修改合同需同步两侧类型与 使用说明
验证命令
RUST_MIN_STACK=16777216 cargo +1.99.1 test --manifest-path backend/Cargo.toml -p gateway-plugin-sdk --no-default-features --locked
RUST_MIN_STACK=16777216 cargo +1.99.1 test --manifest-path backend/Cargo.toml -p gateway-plugin-sdk --all-features --locked
RUST_MIN_STACK=16777216 RUSTDOCFLAGS='-D warnings' cargo +1.99.1 doc --manifest-path backend/Cargo.toml -p gateway-plugin-sdk --all-features --no-deps --locked
源码中的类型和字段是当前合同;宿主接入、目录边界、内置 Provider 与 Runtime 集成验证见 系统架构。 单元测试与文档构建不代替目标平台上的安装、配置及实际能力验证