运行时初始化、Gateway 调用、协议桥接、预算跟踪、回放脱敏与运维模式。
使用指南
这份指南说明如何把 OmniLLM 用作:
- 生成请求的运行时网关
- 支持协议之间的转码层
- 面向向量嵌入、图像、音频与重排序 API 的类型化多端点转换层
- 面向测试夹具的回放脱敏辅助工具
- 自带官方 OmniLLM Skill 的 Rust 项目
如果你想看 Skill 的安装方式,请阅读 skill.md。如果你想继续了解系统设计与源码细节,请阅读 architecture.md 与 implementation.md。
这个库提供什么
OmniLLM 目前有三条主要使用面:
-
Gateway当你需要在运行时发送生成请求,并同时获得以下能力时使用它:- provider 无关的请求/响应类型
- 多 Key 负载均衡
- 每个 Key 的 RPM 与 TPM 控制
- 熔断保护
- 预算跟踪
- 规范化流式事件
-
Provider primitive runtime API 当你需要直接发送 provider-native 请求、并且不希望经过
LlmRequest、LlmResponse、ApiRequest或ApiResponse转换时使用它。Primitive call、stream 和 realtime 入口复用同一个GatewayKey 池、RPM 防护、timeout 和BudgetTracker。 -
API 与协议转换辅助工具 当你需要以下能力时使用这一组工具:
- 把上游原始 payload 解析成规范化类型
- 把规范化类型重新输出成 provider 的传输格式
- 在支持的协议之间做转码
- 检查桥接过程和字段损耗元数据
- 为测试用例清洗请求/响应夹具
注意:Gateway::call 和 Gateway::stream 仍然是以 OpenAI Responses 为中心的 canonical generation path。Provider primitive API 是显式启用的增量入口,用于发送 provider-native payload,包括 primitive support registry 已启用的非生成 API。
安装
把这个库加到你的 Cargo.toml 中:
选择一种 TLS 后端:
- 默认:
rustls - 可选:
native-tls
示例:
OmniLLM Skill 集成
OmniLLM 在仓库的
skill/ 目录
中附带了一份官方 agent skill。如果你想通过 Vercel Labs skills 流程把它安装到 Claude Code、Codex 或 OpenCode 中,请阅读 技能指南。
核心概念
这个库围绕 LlmRequest 和 LlmResponse 来统一生成请求模型。
LlmRequest是规范化的生成请求。LlmResponse是规范化的生成响应。LlmStreamEvent是规范化的流式事件模型。CapabilitySet用来承载跨 provider 的工具、结构化输出、推理、内置工具等能力。EndpointProtocol表示运行时端点配置,包括*_compat模式。ProviderProtocol表示底层生成协议,供编解码与转码层使用。ProviderEndpoint用于标识 canonical 请求要发送到哪里,以及如何发送。PrimitiveProviderEndpoint与PrimitiveRequest用于标识 provider-native primitive 请求要发送到哪里,以及如何发送。
对于多端点场景:
ApiRequest和ApiResponse是跨端点家族的规范化类型封装。WireFormat表示某一种具体的上游传输格式。ConversionReport<T>会告诉你转换是否发生了桥接,以及是否有字段损耗。
快速开始
下面是一个最小但可用的运行时示例:
Provider Primitive Runtime
当你需要 provider-native payload 且不希望经过 canonical 转换时,使用 primitive runtime API。 原始 body 是 source of truth;OmniLLM 只追加认证、默认 header、query、timeout、base URL/path 推导等 transport metadata。
当 provider 返回 OpenAI usage、Anthropic usage 或 Gemini usageMetadata 等已知字段时,primitive usage telemetry 会保存在 PrimitiveResponse::usage。Budget settlement 会优先使用这些 telemetry;没有 token usage 时则回退到预留估算。
Primitive provider support 的边界是模型工作负载网关能力,不追求完整 provider SDK parity。P1 HTTP gaps 包括 OpenAI Files/Uploads/Models/Audio Translations/Image edits/variations、Anthropic Models/Files hardening,以及 Gemini Models/Operations/Files/Caches hardening。P2 覆盖 batch-style API 的显式 async job lifecycle。P3 覆盖 OpenAI Audio Speech binary chunk streaming,以及 OpenAI Realtime 和 Gemini Live 的 WebSocket realtime session;WebRTC 仍是 planned,不声明为已实现。Metadata 和 read-only operations 默认 zero-cost;uploads 归为 upload/storage;media calls 和 realtime sessions 使用 provider telemetry 或预留估算回退。
Deferred surfaces 包括 admin、billing、webhooks、fine-tuning、evals、tunings、managed-agent platforms、hosted RAG/vector-store administration 和 SDK helper layers。
构建 Gateway
GatewayBuilder 用来配置运行时客户端:
Builder 选项
-
add_key/add_keys为同一个上游端点注册一个或多个 API Key。 -
budget_limit_usd设置进程内预算上限。请求会在派发前预留估算成本,并在完成后按实际成本结算。 -
pool_config配置获取重试策略与熔断阈值。 -
request_timeout设置Dispatcher使用的 HTTP 客户端超时时间。
Key 配置
每个 KeyConfig 都包含:
- 原始 key 字符串
- 人类可读的 label
tpm_limitrpm_limit
建议用标签字段做可观测性标识。gateway.pool_status() 会返回这些标签。
选择 Provider Endpoint
内置的生成端点包括:
ProviderEndpoint::openai_responses()ProviderEndpoint::openai_chat_completions()ProviderEndpoint::claude_messages()ProviderEndpoint::gemini_generate_content()
你也可以自己构造一个自定义端点:
当 base_url 只是 host 或前缀时,使用非 compat 协议,让 OmniLLM 自动补齐标准路径。
当 base_url 已经是某个包装层或兼容网关暴露出来的完整请求 URL 时,使用 *_compat 协议。
这也包括那些要求 OpenAI Chat Completions payload 使用严格
messages[].content[] 数组形态的包装层。
EndpointProtocol 是运行时配置层;ClaudeMessages、GeminiGenerateContent 这类名字保留在 ProviderProtocol 上,因为它们对应的是上游 wire API 形态,供 parse_*、emit_*、transcode_* 使用。
鉴权方式
AuthScheme 支持:
BearerHeader { name }Query { name }
如果你没有显式设置鉴权方式,ProviderEndpoint 会使用该协议对应的默认值。
构建请求
input 与 messages
LlmRequest 同时支持:
input:规范化执行输入messages:兼容式聊天风格视图
如果 input 非空,它会被视为真实输入来源;如果 input 为空,则回退使用 messages。
新代码建议优先使用 input。
Message.parts 是兼容式聊天视图背后的内容模型。当 OmniLLM 输出
OpenAI Chat Completions 请求时,纯文本聊天消息会保持为数组形态:
MessagePart::Text { text: "hi?".into() } 会变成
content: [{ "type": "text", "text": "hi?" }]。这对拒绝裸字符串
content 的 compat 包装层尤其重要。
Provider 特有的顶层字段
对于 OmniLLM 没有规范化建模的请求字段,请放到
LlmRequest.vendor_extensions 中。
对于 OpenAI responses 和 chat_completions,OmniLLM 会在解析、输出和
transport emission 过程中保留这些顶层 vendor extensions。像
enable_thinking 这种包装层特有开关,就应该放在这里。
规范化控制项仍然应该放在 generation、capabilities 和 metadata
里。只有当包装层额外要求 OmniLLM 尚未直接建模的字段时,才使用
vendor_extensions。
instructions 字段
instructions 是规范化模型里放置 system/developer 指令的顶层字段。
如果没有显式提供 instructions,这个库可以从聊天风格视图中的 system/developer 消息中推导出规范化指令。
生成控制
GenerationConfig 包含:
max_output_tokenstemperaturetop_ptop_kstop_sequencespresence_penaltyfrequency_penaltyseed
这些都是规范化控制项。转码到能力更窄的协议时,某些字段可能会被丢弃,并通过 ConversionReport.loss_reasons 体现出来。
能力集
CapabilitySet 是跨 provider 的能力层。
自定义工具
结构化输出
推理与内置工具
CapabilitySet 还包括:
reasoningbuiltin_toolsmodalitiessafetycache
这些都是规范化抽象。具体是否受支持取决于目标协议;如果目标协议无法表达其中一部分能力,转换报告会把它标记为 bridged,并在必要时标记为 lossy。
Prompt Cache
Prompt cache 位于 LlmRequest.capabilities.prompt_cache,旧的 CapabilitySet.cache 仍作为兼容提示迁移到新策略。缓存只是优化时使用 PromptCachePolicy::BestEffort;如果缓存语义丢失就不应发起请求,使用 PromptCachePolicy::Required。
Provider 映射规则:
- OpenAI Responses 与 Chat Completions 输出
prompt_cache_key和prompt_cache_retention,其中短期保留为in_memory,长期保留为24h。OpenAI 不表达显式 breakpoint;BestEffort 在 typed bridge 中记录 loss,Required 会在 transport 前报错。 - Claude Messages 输出 provider 原生
cache_control,type为ephemeral;Short映射为ttl: 5m,Long映射为ttl: 1h,可支持 tools、system instructions、message、content block 等 breakpoint。 - Gemini GenerateContent 没有 typed prompt cache 映射;BestEffort 会被丢弃并写入
ConversionReport.loss_reasons,Required 会在 transport 前失败。
Provider usage 会保存在 TokenUsage.prompt_cache:
- OpenAI 的缓存前缀命中进入
cached_input_tokens。 - Claude 的缓存命中与写入进入
cache_read_input_tokens、cache_creation_input_tokens、cache_creation_short_input_tokens、cache_creation_long_input_tokens。
PromptLayoutBuilder 可以把稳定 prefix 放在动态用户/RAG suffix 前面,并生成不包含动态内容的、租户隔离的稳定 prefix key:
非流式调用
单次生成调用可以使用 Gateway::call:
Gateway 会按下面的顺序执行:
- 估算 token 与成本
- 获取一个健康且拥有足够 TPM 容量的 key
- 检查本地预算
- 检查本地 RPM 窗口
- 派发上游 HTTP 请求
- 按实际 usage 结算成本
- 根据成功或失败结果更新 key 健康状态
流式调用
如果你希望收到规范化流事件,请使用 Gateway::stream:
流语义
- 流会产出
Result<LlmStreamEvent, GatewayError>。 - 有些上游会发送一个终态
Completed事件;也有些上游会以[DONE]或协议特定的停止标记结束。 - 某些 OpenAI Chat Completions compat 包装层会把
delta.role = "assistant"和首段delta.content合并进同一个 SSE frame。gateway 会保留这段首个文本增量,而不是只把它当成ResponseStarted。 - 当上游没有提供终态完成事件时,gateway 会按需合成一个
Completed,让调用方仍然能拿到最终规范化响应。 - 如果流在 usage 元数据出现之前就结束或失败,gateway 会回退到内部 usage 估算来结算预算,而不是把整笔预留全部退回。
取消
可以使用 CancellationToken 来停止一个进行中的请求:
取消会表现为 GatewayError::Cancelled。
预算跟踪
预算跟踪是进程内的,并且不依赖锁。
关键点:
- 请求会在派发前预留估算成本
- 最终成本会按实际 usage 结算
- 成功请求可能向上或向下修正结算
- 失败或被截断的流不会自动全额退款;只要可能,gateway 会依据已知或估算出的部分 usage 来结算
可观测性相关方法:
gateway.budget_used_usd()gateway.budget_remaining_usd()
如果你在 Gateway 之外也需要这个底层原语,可以直接使用 BudgetTracker。
Key 池、限流与熔断
每个 key 都会被独立跟踪。
Key 池负责约束什么
- 使用原子 in-flight 计数器进行 TPM 预留
- 通过滑动窗口做 RPM 准入
- 用随机化选择降低竞争
- 在 provider 返回限流响应时进入冷却
- 在未授权响应后永久标记为不可用
- 在连续 provider 故障后触发熔断
可观测性
KeyStatus 包含:
labelavailabletpm_inflighttpm_limitcool_down_untilfailure_cool_down_untilconsecutive_failures
冷却字段使用 Unix epoch 毫秒时间戳。
错误处理
对外暴露的运行时错误会统一为 GatewayError:
NoAvailableKeyBudgetExceededRateLimitedUnauthorizedCancelledProvider(ProviderError)Protocol(String)Http(reqwest::Error)
一般可以这样理解这些错误:
-
NoAvailableKey当前没有任何健康 key 拥有足够的本地容量。 -
BudgetExceeded你的预算上限在请求派发前就拒绝了这次调用。 -
RateLimited要么是本地 RPM 窗口拒绝了请求,要么是上游 429 被统一归一化成了这个错误。 -
Unauthorized上游返回了 401/403,对应 key 会被标记为永久不可用。 -
Provider请求传输完成了,但 provider 执行失败;或者网络异常被归一化成了 provider 侧故障。 -
Protocolcrate 无法按预期解析或输出目标协议 payload。
协议解析与输出
当你想直接处理受支持的生成协议时,可以使用这些辅助函数:
parse_requestemit_requestparse_responseemit_responseparse_stream_eventemit_stream_eventtranscode_requesttranscode_responsetranscode_stream_eventtranscode_error
这些 helper 是面向底层单 frame 的协议工具。对于生产环境里的运行时流处理,优先使用 Gateway::stream;尤其当上游 compat 包装层可能把多个流语义塞进同一个 provider frame 时更是如此。
示例:
多端点 API 层
多端点 API 层是类型化且规范化的。当你想为非生成类端点家族构建转换器或请求发射器时,它会很有用。
支持的规范化端点家族
- 生成:
ApiRequest::Responses - 向量嵌入:
ApiRequest::Embeddings - 图像生成:
ApiRequest::ImageGenerations - 音频转写:
ApiRequest::AudioTranscriptions - 语音合成:
ApiRequest::AudioSpeech - 重排序:
ApiRequest::Rerank
输出传输请求
桥接与损耗报告
ConversionReport<T> 会告诉你:
-
bridged表示目标 wire format 与规范化端点模型并不原生一致,需要经过桥接。 -
lossy表示有一个或多个字段无法被表达出来。 -
loss_reasons具体说明哪些内容被丢弃了,或发生了怎样的降级。
示例:
内置 Provider 注册表
你可以使用内置注册表查看当前已经建模的 provider 和端点家族:
这个注册表是元数据,不是运行时 dispatcher。它更适合用在能力发现、配置 UI 和校验逻辑中。
回放脱敏
如果你在做 record/replay 风格的测试,可以使用:
ReplayFixturesanitize_transport_requestsanitize_transport_responsesanitize_json_value
这些辅助函数会脱敏常见敏感信息,例如:
- 鉴权头
- query token 参数
- JSON 中看起来像 key 的敏感字段
- 体积较大的二进制或 base64 blob
示例:
仓库中附带的示例
在仓库根目录运行:
它们各自展示的内容如下:
-
basic_usage带预算跟踪和 Key 池状态打印的并发运行时生成调用。 -
multi_endpoint_demo不发起网络请求的前提下,展示类型化请求输出、协议转码、provider 注册表查询以及回放脱敏。 -
responses_live_demo一个完全通过环境变量配置、支持图像输入的实时运行时示例请求。
实时示例与实时测试
仓库中附带了 .env.example,用于实时运行时示例以及被忽略的实时测试。
典型流程:
可选的 ignored 测试:
实践模式
1. OpenAI 兼容运行时 Gateway
运行时配置请使用 ProviderEndpoint::new(...) 配合 EndpointProtocol。
当需要走标准上游路径时使用官方变体;当需要命中包装层自定义完整 URL 时使用 *_compat 变体。
2. 纯转换服务
如果你在写代理、SDK 适配层或测试工具,可能根本不需要 Gateway。这种情况下可以直接使用 emit_*、parse_* 和 transcode_*。
3. 安全夹具采集
如果你会把请求/响应夹具落到仓库里,记得在写盘前先做脱敏。
故障排查
我遇到了 NoAvailableKey
可能原因:
- 所有 key 都在冷却中
- 所有 key 都已经失效
- 所有 key 在本地 TPM 上都已经饱和
- 本地熔断器已经对所有 key 打开
请先检查 gateway.pool_status()。
我比预期更早遇到了 BudgetExceeded
要记住 Gateway 会在派发前预留估算成本,之后才会进行结算。在流量尖峰期间,当前使用量在结算完成前可能会暂时偏高。
我遇到了 Protocol(...)
这通常意味着以下几种情况之一:
- 上游请求体结构发生了变化
- 你选择的协议与实际上游不匹配
- 你请求了目标协议无法编码的能力
如果你在做转码,请检查 loss_reasons。
流在没有 provider usage 元数据的情况下结束了
这在某些上游流式协议形态中是正常现象。Gateway 会回退到部分 usage 估算来进行预算结算,并在需要时合成终态 Completed 响应。
API 表面参考
最常用的项目包括:
-
运行时生成:
Gateway,GatewayBuilder,KeyConfig,PoolConfig,ProviderEndpoint,EndpointProtocol -
规范化生成类型:
LlmRequest,LlmResponse,LlmStreamEvent,Message,RequestItem,CapabilitySet -
转换辅助函数:
parse_request,emit_request,parse_response,emit_response,transcode_request,transcode_response -
多端点 API:
ApiRequest,ApiResponse,WireFormat,ConversionReport,emit_transport_request,parse_transport_response -
回放脱敏:
ReplayFixture,sanitize_transport_request,sanitize_transport_response,sanitize_json_value
推荐阅读顺序
如果你第一次接触这个 crate:
- 先读主仓库里的
README.md - 运行
cargo run --example basic_usage - 阅读这份使用指南
- 如果需要设计背景,再读 architecture.md
- 如果需要内部实现,再读 implementation.md