智能路由技术参考
智能路由也称模型路由。本页供需要直接配置 Gate、调用管理接口或核对路由细节的管理员与开发者查阅。概念与常用场景请先阅读智能路由指南。
插件配置
在 Gate 的 config.yaml 中启用智能路由插件:
plugins:
- name: routing
enabled: true
pre_order: 20
post_order: 15
error_policy: fail_open
config:
definition_file: "routing.yaml" # 规则定义文件,相对 config.yaml 所在目录
allow_request_bypass: false # 是否允许调用方用 hcd-og-route: off 跳过路由
complexity:
classifier: llm # heuristic | heuristic_first | llm | embedding;省略表示不做复杂度分级
llm:
url: "http://<平台分类模型>/v1/chat/completions"
model: "qwen3-14b"
api_key: "${ROUTING_CLASSIFIER_KEY}"
extra_body: {chat_template_kwargs: {enable_thinking: false}} # Qwen3 关闭思考| 字段 | 默认值 | 说明 |
|---|---|---|
definition_file | routing.yaml | 规则定义文件路径。文件修改后自动热更新 |
allow_request_bypass | false | 为 true 时,带 hcd-og-route: off(不区分大小写)的请求跳过路由 |
complexity | 不分级 | 复杂度分级配置,见复杂度分级 |
error_policy | fail_open | 插件自身出错时原样转发请求,响应头 og-route: ERROR |
config 块内的未知字段会让加载失败,拼错的字段不会被静默忽略。
与其他插件的顺序约束
网关在加载配置时强制检查以下顺序,违反时直接报错:
| 约束 | 原因 |
|---|---|
安全护栏的 pre_order 小于智能路由的 pre_order | 被拦截的请求不产生复杂度判断的开销;判断看到的是已脱敏的文本 |
智能路由的 pre_order 小于语义缓存、Token 压缩的 pre_order | 缓存身份与压缩预算按改写后的模型计算 |
日志插件的 pre_order、post_order 都小于智能路由对应的值 | 日志包在外层,才能记录路由决策和每次尝试 |
规则定义文件
schema_version: 1
max_chain_depth: 5
allowed_models: [qwen-plus, deepseek-v3, claude-sonnet-4-5, gpt-4o]
fallback:
max_attempts: 3
rules:
- id: auto-simple
name: og-auto 简单请求
priority: 10
match: {models: [og-auto], complexity_tiers: [SIMPLE]}
targets: [{model: qwen-plus}]
fallbacks: [gpt-4o]顶层字段
| 字段 | 默认值 | 说明 |
|---|---|---|
schema_version | 1 | 格式版本 |
max_chain_depth | 5 | 链式规则最多连续触发的层数,取值 1–20 |
allowed_models | 不限制 | 目标模型与候选模型的白名单,不在列表内的模型在加载时报错。只是静态白名单,不核验供应商侧的定价、授权与可用渠道 |
fallback | 见切换策略 | 失败切换的判定策略 |
rules | [] | 规则列表 |
规则字段
| 字段 | 必填 | 说明 |
|---|---|---|
id | 是 | 唯一标识,最多 128 个字符;出现在响应头 og-route-rule 与日志中,改 id 会让历史统计断开 |
name / description | 否 | 展示用 |
enabled | 否 | 省略为启用 |
priority | 是 | 越小越先匹配;启用的规则之间不能重复 |
match | 否 | 声明式匹配条件,见匹配条件 |
when | 否 | CEL 表达式,与 match 同时满足才命中,见CEL 条件 |
targets | 否 | 目标模型列表 [{model, weight}]。多个目标时按权重随机选择(权重不需要加起来等于 100,权重为 0 的目标不参与选择);只有一个目标时忽略权重,即使写 0 也会选中它,要停用规则请用 enabled: false。为空表示不改模型,只附加候选模型 |
fallbacks | 否 | 候选模型,按顺序尝试 |
fallback_policy | 否 | 只对本规则候选生效的切换策略,字段与顶层 fallback 相同;写了的字段替换顶层值,省略的沿用顶层,见切换策略。必须同时有 fallbacks |
chain | 否 | 为 true 时,用选出的模型从第一条规则重新匹配,用于"虚拟模型先归一、再按条件分发"。每条规则最多触发一次,不会出现死循环 |
builtin | 否 | 标记为内置规则,管理界面上只能启停 |
complexity_profile | 否 | 选用的命名分类器:本规则的 match.complexity_tiers 与 CEL 里的 complexity_tier 由它判断。省略时用默认分类器;只能写在引用档位的规则上 |
scenario | 否 | 场景标记 {id, type},管理界面据此把多条规则归为一个场景,不影响路由结果。id 最多 128 个字符,同一场景的规则共用;type 为 1–64 个小写字母、数字、_、-,以字母开头。同一个 id 下 type 不一致时加载失败 |
一条规则至少要有 targets 或 fallbacks 之一;链式规则必须有 targets。
求值规则
- 启用的规则按
priority升序依次匹配,第一条命中的规则生效。 - 链式规则命中后,用新模型从头再匹配,直到命中非链式规则、没有规则命中,或达到
max_chain_depth。 - 链上最后命中的规则决定候选模型及其切换策略(
fallback_policy)。 - 某条规则求值出错(例如 CEL 读取了不存在的请求头)时只跳过这条规则,请求继续往下匹配,不会被拦截。
加载期校验
以下问题都会让加载失败,加载失败时保留上一份可用的规则:
- 未知字段(如把
fallbacks写成fallbaks); - 规则 id 为空、重复或超过 128 个字符,启用规则之间
priority重复; - CEL 语法错误、结果不是布尔值、请求头键名使用大写;
- 目标或候选模型不在
allowed_models内,候选模型重复; scenario缺少id、id首尾有空白、type格式不对,或同一个id对应不同的type;fallback_policy没有对应的fallbacks、取值越界、合并后没有任何可切换的状态码或错误码,或max_attempts为 1(等于不切换);- 在不引用档位的规则上写了
complexity_profile;启用的规则用complexity_profile引用了插件配置中未定义的命名分类器(停用的规则不检查。:validate只校验规则文件本身,这一项在warnings里提示,真正写入或热更新时才拒绝加载); - 非法的档位、协议或模型通配符,权重为负数;
- 定义文件超过 4 MB。
YAML 格式错误(含未知字段)在解析阶段就会报错并直接返回;解析成功后,语义问题会一次全部列出。
不要用截断写的方式直接改文件
cat > routing.yaml 这类"先清空再写入"的方式,中间会出现一个 0 字节文件。空文件是合法定义(没有规则),网关会在这一瞬间按"无规则"处理请求。请使用管理界面、规则管理接口,或"写临时文件再改名"的方式更新规则。
匹配条件
match 中不同字段之间是"且",同一字段的多个值之间是"或";全部省略表示匹配所有请求。
| 字段 | 说明 |
|---|---|
models | 模型通配符,整串匹配、不区分大小写。*、? 可匹配任意字符(包括 /),[45]、[!x] 为字符类。gpt-4o 不会匹配 gpt-4o-mini |
tenants | 精确匹配 hcd-og-tenant-id 请求头;配置后,没有带租户头的请求不会命中 |
headers | 请求头精确匹配,键名不区分大小写,值区分大小写 |
protocols | openai、claude、gemini;OpenAI Responses 接口属于 openai |
stream | true 或 false;省略表示不限制 |
complexity_tiers | SIMPLE、MEDIUM、COMPLEX;用到时才会做复杂度判断 |
租户与请求头条件不是安全边界
hcd-og-tenant-id 和其他请求头都可以由调用方随意设置。按租户或请求头分流只能作为业务分流手段,不能用来做权限或计费隔离。
请求中的模型
- 模型取自请求体的
model;Gemini 原生接口取自 URL 路径,请求体里的model不参与路由,也不会被改写。 - 请求体顶层有重复或大小写不同的
model键时(如{"Model": "a", "model": "b"}),网关按"不区分大小写、重复时取最后一个"的规则取值,并合并成一个model后再转发,与供应商侧的解析结果一致。 - Gemini 路径中的冒号写成
%3A等编码形式也会被识别。
CEL 条件
when 使用 CEL 表达式,结果必须是布尔值。
| 变量 | 类型 | 说明 |
|---|---|---|
model | string | 当前模型(链式匹配中每一步都会更新),保留调用方原样大小写 |
protocol | string | openai / claude / gemini |
endpoint | string | 请求路径(含查询串) |
stream | bool | 是否流式 |
tenant_id | string | hcd-og-tenant-id 请求头 |
headers | map | 请求头,键名小写,多值头取第一个值 |
params | map | 请求体顶层的标量字段(如 temperature、max_tokens) |
input_tokens | int | 整个请求体的估算 token 数 |
message_count | int | 消息条数 |
has_tools / has_image | bool | 是否带工具定义、是否含图片 |
complexity_tier | string | 复杂度档位,由本规则 complexity_profile 指定的分类器判断(省略时为默认分类器) |
常用写法:
"x-tier" in headers && headers["x-tier"] == "premium"
model.startsWith("gpt-") && input_tokens > 120000
has_tools && message_count > 10
has(params.temperature) && params.temperature < 0.3
complexity_tier == "COMPLEX" || ("x-vip" in headers && headers["x-vip"] == "1")注意:
- 读取不存在的请求头会让表达式出错,这条规则不命中(
==和!=都不命中)。先用"x" in headers判断是否存在。 - CEL 的
model.matches("gpt-4")是正则部分匹配,会命中openai/gpt-4o;需要整串匹配时用match.models或==。 - 拿不到复杂度档位时,
complexity_tier的值是"未知":complexity_tier != "SIMPLE"不命中,而complexity_tier == "COMPLEX" || 其他条件在其他条件为真时仍然命中。 - 表达式有求值开销上限,超出时按出错处理、跳过该规则。
- 没有额度、预算或限流相关的变量:这些数据在供应商侧,网关拿不到。
复杂度分级
只有匹配到引用了 complexity_tiers 或 complexity_tier 的规则时才会判断,每个分类器每个请求最多判断一次。complexity 下是默认分类器,没有写 complexity_profile 的规则共用;不同虚拟模型要用不同分类器时见命名分类器。
分类器
classifier | 原理 | 依赖 | 额外耗时 |
|---|---|---|---|
heuristic | 本地打分(移植自 LiteLLM complexity_router 的默认分类器):按长度、代码词、推理词、技术词、简单问句、多步骤、多问号 7 个维度加权打分,按分界线定档 | 无 | 不到 1 毫秒,不调用任何模型 |
heuristic_first | 先本地打分;有触发信号、且档位不高于 heuristic_first_max_tier 时直接用,否则调用 llm 分类器 | 同 llm | 本地定档时不到 1 毫秒;升级时同 llm |
llm | 调用平台提供的 OpenAI 兼容小模型,要求返回 {"tier": "..."};使用平台自己的模型与凭据,不使用调用方的 API Key 和模型 | 一个 Chat Completions 端点 | 一次模型调用,默认超时 1.5s |
embedding | 把请求与三档预置样例句比相似度,取最相近的若干条按相似度加权投票 | 一个 Embedding 端点(OpenAI 兼容或 Ollama) | 一次向量化调用;内网部署 Embedding 模型的测试中约几十到一百毫秒,实际取决于部署位置与网络 |
不配置 classifier 时不做复杂度分级:引用档位的规则一律不命中,日志中 complexity_mechanism 记为 skipped;加载时如果有规则引用档位,网关日志会出现一条告警,列出这些规则;通过管理接口读取、保存或校验路由规则时,响应里的 warnings 也会给出同样的提示。
主分类器超时、失败、返回无法解析的档位、并发已满或弃权时,可以改用 fallback 指定的另一种分类器(例如 classifier: embedding 搭配 fallback: llm;fallback: heuristic 用本地打分兜底,不花钱、一定有结果),或使用 fallback: llm_backup 调用第二个独立 LLM。主模型成功时绝不调用备用模型;每次新的分级都先尝试主模型。两个分类器都失败时拿不到档位。备用档位只用于当前请求,不写入会话,调整备用模型不重置主模型的会话表。
classifier: embedding 可搭配 fallback: llm 或 fallback: llm_backup:前者读取 llm 配置,后者读取独立的 llm_backup 配置,后者不使用 llm 块。
分类在发往上游之前执行,耗时会直接增加首字节等待时间。当前每次新的分级都先尝试主分类器,没有熔断;主模型持续超时时,每个请求都会等满主超时。按主服务实测高分位延迟设置较短的主超时,备用独立设置超时;凭据探测、主分类和备用分类的最坏等待应一起计入请求延迟预算。
两个 LLM 配置示例(routing 插件的 config.complexity):
classifier: llm
fallback: llm_backup
llm:
url: "http://<primary-classifier>/v1/chat/completions"
model: "qwen3-14b"
api_key: "${ROUTING_CLASSIFIER_KEY}"
timeout: "1500ms"
extra_body: {chat_template_kwargs: {enable_thinking: false}}
llm_backup:
url: "https://api.siliconflow.cn/v1/chat/completions"
model: "Qwen/Qwen2.5-7B-Instruct"
api_key: "${ROUTING_BACKUP_CLASSIFIER_KEY}"
timeout: "5s"
max_concurrent: 4备用 API Key 应独立注入网关进程或容器环境。免费模型仍需要账号凭据,限额与价格以服务商当前政策为准。主备分别使用自己的超时,失败时等待可能累加。旧版网关不认识 llm_backup,需先升级程序再启用配置。
输入范围:只取最近 message_window(默认 1)条用户本人写的消息。系统提示词、助手回复、工具返回结果都不参与;<system-reminder>、<environment_context> 等 Agent 工具自动附加的标签会被去掉。最后一条用户消息之后还有助手回复或工具结果(Agent 的工具调用循环)时,视为续写而不是新问题。
通用参数
| 字段 | 默认值 | 说明 |
|---|---|---|
classifier | 不配置 | heuristic / heuristic_first / llm / embedding;不配置表示不做复杂度分级 |
fallback | none | 主分类器没有结果时改用的分类器:none、heuristic、另一种分类器或 llm_backup;备用配置必须完整。classifier: heuristic 时只能是 none;classifier: heuristic_first 时不能是 llm |
heuristic_first_max_tier | SIMPLE | 只用于 heuristic_first:本地打分最多自己定到的档位,SIMPLE 或 MEDIUM |
llm_backup | 未配置 | fallback: llm_backup 时必填;字段与 llm 相同,端点、模型、密钥、超时、并发上限、提示词、附加字段与凭据校验独立配置,不继承主模型的设置 |
message_window | 1 | 参与判断的最近用户消息条数 |
本地打分(heuristic)
参数位于 complexity.heuristic,全部可选,键名与 LiteLLM 一致:
| 字段 | 默认值 | 说明 |
|---|---|---|
tier_boundaries.simple_medium / .medium_complex | 0.15 / 0.35 | 加权分数低于前者为简单,低于后者为中等,其余为复杂 |
token_thresholds.simple / .complex | 15 / 400 | 估算 token 数(中文每字 1 个)低于前者减分、高于后者加分 |
dimension_weights | 见下 | 各维度权重(0–1);写了的覆盖默认值,没写的沿用默认值 |
code_keywords / reasoning_keywords / technical_keywords / simple_keywords | 内置列表 | 写了就整体替换内置列表;内置列表是 LiteLLM 的英文列表加中文对应词 |
| 维度 | 默认权重 | 打分 |
|---|---|---|
tokenCount | 0.10 | 短于阈值 -1,长于阈值 +1 |
codePresence | 0.30 | 代码词命中 1 个 0.5,2 个以上 1 |
reasoningMarkers | 0.25 | 推理词命中 1 个 0.7,2 个以上 1;命中 2 个以上且分数不低于 simple_medium 时直接定为复杂 |
technicalTerms | 0.25 | 技术词命中 2 个 0.5,4 个以上 1 |
simpleIndicators | 0.05 | 简单问句词命中即 -1 |
multiStepPatterns | 0.03 | first…then、step 1、1. 、首先…然后、第一步 等,0.5 |
questionComplexity | 0.02 | 问号超过 3 个,0.5 |
- 只看用户本人写的文本,不看系统提示词。英文关键词要求两端是词边界(
api不命中capital),中文关键词按子串匹配。 - LiteLLM 有第四档 REASONING,这里并入复杂档,所以没有
complex_reasoning分界线。 - 本地打分不花钱,不做调用方凭据校验,有人写文本时一定给出档位。它是路由提示,不是安全边界。
- 关键词只扫描最近 16 KB 文本,长度仍按全文估算;16 KB 和 64 KB 的输入都约 0.6 毫秒。
- 准确率:在 36 条中英文标注句上,单用本地打分命中 50%(简单档全对,但较多中等、复杂请求被判成简单),平台
qwen3-14b命中 97%,heuristic_first命中 89% 且省掉 39% 的 LLM 调用。建议用heuristic_first,不要单用heuristic。 heuristic_first下,没有任何维度触发时(分数 0)不算证据,一律交给 LLM;本地档位和 LLM 档位都会写入会话档位;fallback: heuristic直接复用已经打好的分数。
complexity:
classifier: heuristic_first
fallback: heuristic
heuristic_first_max_tier: SIMPLE
llm:
url: "http://<platform-llm>/v1/chat/completions"
model: "qwen3-14b"
api_key: "${ROUTING_CLASSIFIER_KEY}"
heuristic:
technical_keywords: [kubernetes, 分布式, 订单中心]LLM 分类器(llm)
| 字段 | 默认值 | 说明 |
|---|---|---|
url | 必填 | OpenAI 兼容的 chat/completions 完整地址 |
model | 必填 | 分类模型 |
api_key | 空 | 平台自己的凭据,建议用 ${ENV_NAME} 引用环境变量 |
prompt | 内置指引 | 覆盖默认的分级指引;防注入段落固定附加在后面 |
timeout | 1500ms | 单次分类超时 |
max_concurrent | 16 | 同时进行的分类调用上限;满了立即放弃(有兜底分类器时改用它),不排队 |
extra_body | 空 | 合并进分类请求体的额外字段,用于端点特有的开关。例如 Qwen3 系列默认先输出思考过程,单次分类要几秒,设 {chat_template_kwargs: {enable_thinking: false}} 后约 0.2 秒。model、messages、max_tokens、max_completion_tokens、stream、temperature、n、tools、tool_choice 由网关控制,不能设置(键名不区分大小写) |
网关只接受合法 JSON 里的 tier 字段,或整条回复恰好是一个档位词;推理模型输出的 <think>…</think> 段会先去掉。用户文本里伪造的数据边界会被转义,不能借此篡改分类结果。
Embedding 分类器(embedding)
默认不开启,需要显式设置 classifier: embedding。
| 字段 | 默认值 | 说明 |
|---|---|---|
url | 必填 | 完整地址:OpenAI 兼容为 …/v1/embeddings,Ollama 为 …/api/embed |
model | 必填 | Embedding 模型,如 bge-m3 |
api_key | 空 | 平台自己的凭据 |
format | openai | openai 或 ollama |
timeout | 1s | 单次向量化超时 |
max_concurrent | 16 | 同时进行的调用上限 |
top_k | 5 | 参与投票的最相近样例数;平票时取更高的档位 |
min_similarity | 0(不设下限) | 最相近样例的相似度低于它时放弃判断(有兜底分类器时改用它) |
exemplars.simple / .medium / .complex | 内置样例(每档 36 条,中英各半) | 覆盖某一档的样例;省略的档位沿用内置样例。每档最多 500 条,每条最多 512 字符 |
complexity:
classifier: embedding
fallback: none
embedding:
url: "http://<ollama-host>:11434/api/embed"
model: "bge-m3:latest"
format: ollama
top_k: 5- 网关加载时在后台向量化样例并保存在内存里,不阻塞加载和请求;未就绪期间没有结果(有兜底分类器时改用它),失败时自动重试。状态见指标
og_routing_embedding_ready{state}(warming/ready/failed)。 - 端点、模型、格式与样例都没变时,热更新不会重新向量化。
- 内置样例只是通用起点。在一组 18 句的中英文标注样本上测试(
bge-m3),简单与复杂两档基本准确,中等档误判较多,整体约三分之二;样本量小,只能作为参考。生产使用前建议用业务语料替换样例:按"要做的事"而不是话题分档,同一话题在三档都放一些;中英文、问句与祈使句、长短在三档保持均衡。
调用方凭据校验
LLM 和 Embedding 分类器使用的是平台自己的凭据,而路由发生在供应商侧验证调用方身份之前。为了不让无凭据或伪造凭据的请求先消耗一次付费分类调用,默认会先用调用方的凭据向供应商侧的 /v1/models 探测一次,通过才调用分类器。主分类器和兜底分类器各按自己的 auth_validation 决定是否校验,例如主分类器是本地免费模型时可以关掉校验,付费的兜底分类器仍然受保护;一次请求最多探测一次,两个分类器共用结果。需要校验的分类器在校验不通过时不会被调用,两个都调不了时这次请求拿不到档位。
字段(llm.auth_validation / embedding.auth_validation) | 默认值 | 说明 |
|---|---|---|
enabled | true | 只有入口已经做过可信认证时才关闭 |
endpoint | /v1/models | 探测路径 |
ttl / negative_ttl | 5m / 30s | 有效 / 无效凭据的缓存时长 |
timeout | 3s | 探测超时 |
配置了分类器且开启校验时,Gate 必须配置 proxy.upstream,否则加载失败。Gemini 原生客户端使用 x-goog-api-key 传凭据时不支持校验,这类请求拿不到档位。
命名分类器(complexity.profiles)
不同虚拟模型可以用不同的分类器,例如闲聊入口用本地打分、编码入口用 LLM 加编码专用的分级指引。参照的是 LiteLLM 每个复杂度路由各带一套分类器配置的做法。
complexity: # 默认分类器
classifier: heuristic
profiles:
coding: # 规则里写 complexity_profile: coding 选用
classifier: heuristic_first
fallback: heuristic
llm: {url: "http://<platform-llm>/v1/chat/completions", model: "qwen3-14b", api_key: "${ROUTING_CLASSIFIER_KEY}"}
session: {enabled: true}rules:
- {id: code-complex, priority: 10, match: {models: [og-coding], complexity_tiers: [COMPLEX]}, targets: [{model: strong}], complexity_profile: coding}- 每个命名分类器是一份完整、独立的配置,字段与
complexity相同,不继承默认分类器,必须写classifier,不能嵌套,最多 16 个;名字为小写字母开头的小写字母、数字、_或-。 - 规则的
complexity_profile决定这条规则的档位条件和 CEL 里的complexity_tier由哪个分类器判断。只能写在引用档位的规则上;启用的规则引用了未定义的命名分类器时,网关拒绝加载(保存时保留旧规则)。 - 同一请求里,每个分类器最多判断一次;链式规则用到不同分类器时各判各的,不会借用彼此的档位。
- 日志、响应头
og-route-tier记录的是最后命中规则所用分类器的档位:链上后面没有命中的规则即使读了另一个分类器,也不会覆盖它;最后命中的规则不看档位时,才记录最后一次被规则读到的档位。试运行结果里的complexity_profile说明档位来自哪个命名分类器。 - 每个命名分类器有自己的分类调用并发上限、凭据校验和会话档位表;同一会话在不同虚拟模型上的档位互不影响,只修改某个命名分类器时只重置它自己的会话档位。
会话档位(session)
| 字段 | 默认值 | 说明 |
|---|---|---|
enabled | false | 同一会话内已记录的档位只升不降 |
ttl | 24h | 会话空闲多久后过期 |
max_entries | 100000 | 最多保存的会话数 |
- 会话 ID 取自
hcd-og-session-id或x-claude-code-session-id,与调用方凭据一起计算,不同凭据之间互不影响;没有凭据头的请求不启用会话档位。 - 只有请求成功返回后才写入会话档位;只有主分类器给出的档位才写入,兜底分类器的结果只用于本次请求。
- 已经升到复杂的会话不再调用分类器;只有工具结果、没有新问题的续写请求直接沿用会话档位。
- 开启会话档位必须配置
classifier。 - 修改分级口径(主分类器、
message_window,以及主分类器的端点、模型、提示词、extra_body、样例,或本地打分参数与heuristic_first_max_tier等)会换一张新的会话表;只改超时、并发、会话的ttl/max_entries,或增删、调整兜底分类器不会(兜底分类器的档位本来就不写进会话)。 - 会话档位保存在单个网关进程内,多实例之间不共享。
切换策略
fallback: # 以下为默认值
max_attempts: 3 # 含第一次尝试,取值 1–10;1 表示不切换
total_timeout: 180s # 超过后不再开始新的尝试(不中断进行中的尝试)
on_status: [429, 500, 502, 503, 529]
on_codes: [model_not_found, upstream_unreachable, read_response_failed, do_request_failed]
exclude_codes: [model_price_error, insufficient_user_quota, sensitive_words_detected, new_api_error]判定顺序:先看 exclude_codes,命中即不切换;再看 on_codes,命中即切换;最后看 on_status。错误码从供应商侧错误体中提取(OpenAI 格式取 error.code,Claude 格式取 error.type)。
按规则覆盖:规则的 fallback_policy 字段与上面相同,只对这条规则的候选生效。写了的字段整体替换顶层值,省略(或写 0)的沿用顶层,显式写空列表表示清空。因为是整体替换,要在默认错误码之外追加时需把默认值一并写上:
- id: long-context
priority: 5
when: 'input_tokens >= 120000'
targets: [{model: gemini-2.5-pro}]
fallbacks: [claude-sonnet-4-5]
fallback_policy:
on_status: [429, 500, 502, 503, 504, 529] # 长任务接受叠加耗时,504 也切换
on_codes: [model_not_found, upstream_unreachable, read_response_failed, do_request_failed, context_length_exceeded]
max_attempts: 2错误码按入口协议取值:context_length_exceeded 是 OpenAI 格式的错误码,Claude 格式下上下文超长的 error.type 是 invalid_request_error(也涵盖其他请求错误)。覆盖 exclude_codes 时建议保留 new_api_error。试运行结果的 fallback_policy 字段给出合并后实际生效的策略。
| 供应商侧返回 | 默认 | 原因 |
|---|---|---|
503 model_not_found(无可用渠道) | 切换 | 该模型已没有可用供应商 |
| 429、500、502、503、529、连接失败 | 切换 | 供应商侧的同模型重试已用尽 |
| 504 / 524 | 不切换 | 供应商侧本身不重试超时,再换模型会让耗时叠加;需要时加入 on_status |
400 model_price_error、403、额度不足 | 不切换 | 换模型解决不了,还可能借切换绕过授权 |
| 敏感词拒绝 | 不切换 | 同样的内容在其他模型上也会被拒绝 |
Claude 格式 500 + error.type: new_api_error | 不切换 | 无法区分具体原因;文案为 upstream error: do request failed 时按连接失败处理并切换 |
- 流式请求只在首字节之前切换;已经开始输出后上游中断,不再切换。
- 候选全部失败时,返回最后一次尝试的状态码与错误体;遇到不可切换的错误立即停止并原样返回,后面的候选不再尝试。
- 客户端断开后不再开始新的尝试,也不计为候选模型失败。
- 失败尝试的预扣费由供应商侧退回,只有成功的那次计费。
响应头
非流式响应在返回时附带;流式响应在第一段内容之前写出。
| 响应头 | 说明 |
|---|---|
og-route | PASS / ROUTED / FALLBACK / BYPASS / ERROR |
og-route-rule | 最后命中的规则 id |
og-route-model | 路由后发给供应商的对外模型 ID,也是计费所用的模型;发生切换时是最后一次尝试的模型。与响应体里供应商回报的模型名可能不同 |
og-route-requested-model | 调用方原本请求的模型,只在最终模型与原模型不同时出现 |
og-route-attempts | 上游尝试次数(含第一次) |
og-route-tier | 复杂度档位,只在做过判断时出现 |
FALLBACK 只表示发生了切换,不代表候选一定成功:流式响应头写出时还不知道候选能否完整结束,最终结果以状态码与日志为准。不切换的错误(如 403)显示为 ROUTED。
日志与指标
请求日志(标准输出与日志存储的 logs 表)新增以下字段:
| 字段 | 说明 |
|---|---|
requested_model | 调用方原本请求的模型 |
routing_rule_id | 命中的规则 id |
routing_outcome | 同 og-route |
routing_attempts | 尝试次数 |
complexity_tier | 复杂度档位 |
complexity_mechanism | 档位来源:heuristic / llm / llm_backup / embedding / session / skipped;heuristic_first 按实际定档的一方记为 heuristic 或 llm |
日志中 model 字段的口径:先记为路由后发给供应商的模型(发生切换时为最后一次尝试的模型);非流式请求如果响应体里带有 model,会被供应商回报的模型名覆盖,流式请求不会。因此同一个路由结果,非流式与流式日志中的 model 可能不同;核对路由选择时以 requested_model、routing_rule_id 与响应头 og-route-model 为准,核对费用以供应商侧的计费明细为准。
日志表结构在网关启动时自动迁移,旧数据保留。每次切换另有一条 routing fallback 日志,带 request_id、来源与目标模型、状态码与错误码。
进程内指标(GET /admin/v1/plugins/routing/metrics,Prometheus 文本格式,网关重启后清零):
| 指标 | 标签 | 说明 |
|---|---|---|
og_routing_decisions_total | rule、outcome | 路由决策次数 |
og_routing_fallback_total | from、to、result | 切换次数与候选结果(success / failure) |
og_routing_complexity_total | tier、mechanism,命名分类器另有 profile | 复杂度判断次数 |
og_routing_evaluation_seconds | — | 规则求值耗时(_sum、_count),含复杂度分级 |
og_routing_classification_seconds | le,命名分类器另有 profile | 复杂度分级耗时直方图(含凭据探测与兜底分类器),只计真实请求。规则匹配本身的耗时 = 求值耗时 − 分级耗时 |
og_routing_embedding_ready | state,命名分类器另有 profile | Embedding 样例预热状态(用到 Embedding 分类器时出现,作主分类器或兜底均可) |
og_routing_embedding_exemplars | kind,命名分类器另有 profile | 已向量化与配置的样例数(同上) |
默认分类器的指标不带 profile 标签,与引入命名分类器之前的序列一致。每个指标最多保留 1000 种标签组合,超出后计入 _other。
规则管理接口
需要 Gate 的管理凭据(Authorization: Bearer <ADMIN_TOKEN>)。
| 方法与路径 | 说明 |
|---|---|
GET /admin/v1/plugins/routing/definitions | 返回 {rules, etag, format, content},同时带 ETag 响应头 |
PUT /admin/v1/plugins/routing/definitions | 用请求体整体替换定义文件,支持 If-Match;先校验、再原子写入并立即生效,失败时回滚 |
POST /admin/v1/plugins/routing/definitions:validate | 只校验,不写入。合法时返回 200 {"valid": true, "rules": n},有需要提醒的问题时另带 warnings(如引用档位的规则没有可用的分类器、规则引用了未定义的命名分类器);不合法时返回 422 {"valid": false, "error": "…"}(注意与 PUT 的错误结构不同) |
POST /admin/v1/plugins/routing/definitions:dry-run | 用样例请求试运行,不改写、不转发;可附带一份未保存的候选定义 |
GET /admin/v1/plugins/routing/metrics | 进程内指标,见日志与指标 |
GET /admin/v1/plugins/routing/metrics/summary | 按"规则 × 结果""请求模型 → 实际模型""档位 × 来源"聚合日志存储中的数据 |
GET /admin/v1/plugins/routing/logs | 带路由信息的请求明细,新记录在前 |
GET /admin/v1/plugins/routing/config | 插件条目的 {enabled, config, etag, version, config_hash, applied_at},带 ETag 响应头;config 中的密钥脱敏为 {"configured": true, "redacted": true} |
PUT /admin/v1/plugins/routing/config | 请求体 {enabled, config},替换插件开关与配置(分类器、命名分类器、allow_request_bypass),支持 If-Match;先用候选配置初始化插件,成功后写回主配置文件并立即生效,失败时文件不变。响应同 GET,另带 sessions_reset |
POST /admin/v1/plugins/routing/config:validate | 只校验,不写入、不连接分类器;返回 {valid, enabled, normalized_config, config_hash, sessions_reset},不合法时返回 422 |
GET /admin/v1/config/status | 主配置文件的 config_hash(sha256)与最近一次热更新是否失败 |
插件配置接口
- 保留密钥:某个密钥字段原样提交
GET返回的脱敏标记,表示保留当前值(${ENV}引用保持原样,不会把明文写进文件);要更换密钥时提交新值。当前没有可保留的值时返回 400。 definition_file不能修改:省略时沿用当前值,改成其他值返回 400。sessions_reset:为true表示分级口径变了,活跃会话的档位从头计算。- 删除仍被启用规则引用的命名分类器会被拒绝,返回 422,
validate会提前报出。 - 省略
If-Match时,以读取当前配置时的版本为准,期间配置被改过返回 412。
并发控制与生效确认
GET 返回的 ETag 是定义文件内容的 sha256。PUT 时带上 If-Match,别人已经改过时返回 412,避免互相覆盖。省略 If-Match 也允许写入,因此自动化脚本应始终带上。内容与当前版本完全相同时,PUT 返回 200 且 ETag 不变。
确认规则已生效:GET 返回的 etag 应等于磁盘上定义文件的 sha256。通过 PUT 写入时网关会立即重建,不依赖文件监听。
试运行
按当前生效的规则试运行:
{
"endpoint": "/v1/chat/completions",
"headers": {"x-tier": "premium", "hcd-og-tenant-id": "acme"},
"body": {"model": "og-auto", "messages": [{"role": "user", "content": "设计一个多地域容灾方案"}]}
}按一份尚未保存的候选定义试运行时,在 definition 中放完整的规则文件内容(YAML 或 JSON 字符串):
{
"body": {"model": "og-auto", "messages": [{"role": "user", "content": "设计一个多地域容灾方案"}]},
"definition": "rules:\n - {id: auto-complex, priority: 10, match: {models: [og-auto], complexity_tiers: [COMPLEX]}, targets: [{model: claude-sonnet-4-5}], fallbacks: [gpt-4o]}\n"
}返回示例:
{
"requested_model": "og-auto",
"routed_model": "claude-sonnet-4-5",
"matched": true,
"rule_id": "auto-complex",
"steps": ["auto-complex"],
"fallbacks": ["gpt-4o"],
"complexity_tier": "COMPLEX",
"complexity_mechanism": "embedding",
"complexity_detail": {"similarity": 0.593, "nearest_exemplar": "…", "nearest_tier": "COMPLEX", "votes": {"COMPLEX": 2.41, "MEDIUM": 0.57}}
}definition可选:提供时只按这份定义求值,不会与当前生效的规则合并,生效规则也不变;内容无法解析时返回 422dry_run_invalid。endpoint为 Gemini 原生路径时,与真实请求一样从 URL 取模型。- 试运行不会把请求转发给目标模型,但会真实执行复杂度判断:配置为 LLM 或 Embedding(或本地优先需要升级)时会发出一次分类调用,可能产生平台侧费用,样例文本也会发送给分类服务。试运行不写入会话档位,也不做调用方凭据校验。
skipped_rules列出求值出错被跳过的规则及原因;链式规则达到深度上限、候选定义引用了当前插件配置中未定义的命名分类器时,也会在这里提示。complexity_profile只在档位来自命名分类器时出现,说明是哪一个;用默认分类器时省略。complexity_detail与complexity_tier来自同一个分类器,只在 Embedding 分类器或本地打分参与时出现:Embedding 给出最近样例、相似度与投票,用于校准min_similarity与样例;本地打分给出加权分数score、触发的信号signals与本地档位heuristic_tier,用于调整分界线、权重与关键词。
查询接口参数
metrics/summary 与 logs 支持相同的过滤参数:
| 参数 | 说明 |
|---|---|
from / to | RFC3339 时间,默认最近 24 小时;to 不含边界 |
tenant_id | 租户 |
rule_id | 规则 id |
outcome | 路由结果 |
model | 匹配实际模型或请求模型 |
mechanism | 档位来源(llm、heuristic、session 等);none 表示没有档位 |
result | 按最终状态码分类:success;error(全部失败,最终状态码 ≥ 400);caller_error(4xx,429 除外,如令牌无效、额度不足、未定价);model_error(429 与 5xx) |
request_id | 按请求 ID 查找 |
limit | 仅 logs,默认 50,最大 200 |
metrics/summary 的规则行与模型流向行在 errors(最终状态码 ≥ 400)之外,另给 caller_errors 与 model_errors,口径同 result。档位聚合同时计入需要档位却拿不到的请求,档位为空、来源为 skipped。
两个接口都需要启用日志存储,未启用时返回 503 routing_metrics_unavailable。
错误码
| 状态码 | 说明 |
|---|---|
400 bad_request | 定义请求体(PUT、:validate)为空或超过 4 MB;试运行请求体不是合法 JSON 或超过 8 MB |
409 definition_drift | 磁盘上的定义文件已被外部修改,尚未成为当前生效版本 |
412 definition_precondition_failed | If-Match 与当前版本不一致 |
422 definition_invalid | 定义未通过校验 |
422 dry_run_invalid | 试运行的样例请求或候选定义无效(如样例缺少模型) |
503 plugin_dependency_unavailable | 定义本身合法,但其他插件初始化失败(如日志插件连不上数据库),插件链无法重建;文件已回滚、旧规则继续生效,带 Retry-After,依赖恢复后重试 |
503 routing_unavailable | 智能路由插件未加载 |
按现象排查(管理侧)
| 现象 | 优先检查 |
|---|---|
| 改了规则文件但没生效 | 网关日志是否有 hot-reload failed;GET 返回的 etag 是否等于文件 sha256 |
PUT 返回 409 | 有人直接改过磁盘文件但网关未加载成功;先修复或还原文件 |
PUT 返回 503 plugin_dependency_unavailable | 查看消息里提到的插件及其依赖(多为日志存储的数据库) |
档位总是 skipped、没有 og-route-tier | 是否配置了 classifier;分类器是否可达;Embedding 是否已 ready;调用方凭据是否通过校验 |
og-route: ERROR | 网关日志中路由插件的错误;请求已按原模型转发 |
| 切换没有发生 | 命中的规则是否有 fallbacks;错误码是否在 exclude_codes 中;流式请求是否已开始输出 |