模型与 Upstream 解析
总体链路
一条请求命中 route 后,当前主链路按下面顺序执行:
- 按路径匹配 active route
- 对真实模型请求从请求 JSON 中提取
model - 先读取
route.upstreams[]绑定的 upstream 集合 - 计算 route / consumer / api key 的 selector 交集(逻辑上是逐层收紧)
- 基于
upstream_models与 label selector 过滤候选 upstream - 过滤不支持当前子路径协议的候选
- 记录当前请求的全部
AvailableUpstreams - 对候选做加权随机无放回排序,最多保留数量为
route.max_attempts个
可用模型授权
当前“可用模型授权”的实际含义,是请求里的 model 如何参与运行时决策,以及它如何映射成最终发给上游的模型名。
请求模型如何参与解析
当前语义:
- gateway 只会对真实模型请求从请求 JSON 中提取
model - 当请求里带了
model时,运行时会要求它在upstream_models.model中命中 - 当请求里没有
model时,运行时会退回到“在 route 绑定 upstream 中找首批可用 upstream”
当前会提取 model 的路径白名单是:
POST /chat/completionsPOST /responsesPOST /messagesPOST /embeddings
GET /v1/models 不会提取请求体中的 model。即使客户端错误地给 /v1/models 带了 JSON body,gateway 也会忽略其中的 model,不会用它收窄 upstream 候选。
因此,这一层回答的是:
当前这个请求里的模型名,是否能解析出一组明确的 upstream 候选?
模型名映射
upstream_models 当前同时承担两件事:
model:客户端请求时使用的模型名upstream_model:真正发给上游的模型名
如果命中的是 alias 行,当前实现会把请求里的模型名改写成对应的 upstream_model。
当前改写路径有两条:
- chat 路径会把
RequestIR.Model.UpstreamModel设为候选的ResolvedModel - raw JSON 路径会在出站前把顶层
model字段重写成ResolvedModel
因此,当前已经不是“只用模型名选 upstream”,而是已经把模型映射接进了实际出站请求。
/v1/models 的当前行为
GET /v1/models 当前不是代理上游结果,而是由 gateway 本地构建。
构建方式:
- 先拿到这次请求解析出的
AvailableUpstreams - 提取去重后的 upstream IDs
- 查询这些 upstream 对应的
upstream_models.model - 去重、按字母序排序
- 根据请求头返回 OpenAI 或 Claude 兼容的 models list
因此,/v1/models 返回的是:
当前这次请求在当前 route 绑定、selector 与 upstream 可用性约束下,可见的客户端模型名集合。
它返回的是 upstream_models.model,不是 upstream_models.upstream_model。
/v1/models 会聚合所有可见 active upstream 的模型,不按 upstream 协议收窄。因此同一个 route 下的 chat-completions、open-responses、claude-messages、bedrock-runtime 等 upstream 都可以贡献模型。
响应格式按原始认证头推断:
Authorization: Bearer ...返回 OpenAI/models格式x-api-key: ...返回 Claude/models格式- 两者同时存在时,
Authorization优先 - 都无法判断时,默认返回 OpenAI 格式
日志里的模型字段
当前日志会区分两层模型名:
requested_model:客户端原始请求中的模型名upstream_requests[].request.model:真正发给上游的模型名
因此,当 alias 被改写时,日志里可以同时看到:
- 用户请求的是哪个别名
- 实际打到上游的是哪个真实模型名
可用 Upstream 选择
当前“可用 upstream”判断的目标,是为本次请求选出一组真实可用的候选,并从中决定最终尝试顺序。
Route 绑定 + Selector 约束
候选 upstream 解析前,运行时会先完成 route 访问控制:consumer.route_selector 与 consumer_api_key.route_selector 必须同时匹配当前 route.labels,否则请求返回 403。
通过 route 访问控制后,运行时会取 route.upstreams[] 中显式绑定的 upstream 作为基础集合。
如果 route.upstreams[] 为空,则当前 route 直接没有任何可用 upstream。
在此基础上,运行时会继续按下面几层 selector 逐步收紧:
routes.upstream_selectorconsumers.upstream_selectorconsumer_api_keys.upstream_selector
如果模型请求携带 model,还会对 upstream_models.labels 再套用:
routes.model_selectorconsumers.model_selectorconsumer_api_keys.model_selector
完整 selector 语义、label 语法、保留前缀与内置系统 label,见 Label & Selector。
按模型解析候选
如果请求里带了 model,运行时会在 upstream_models 中查:
upstream_models.model = requested modelroute.upstreams[]绑定了对应 upstream- upstream 与 upstream model 同时命中多层 label selector
upstreams.disabled_at is nullproviders.disabled_at is null- 已认证 consumer 的
disabled_at is null
同时会把 upstream 的 API key 展开成独立候选,并过滤掉:
- 没有
key的无效项 - 已禁用 key
- 已过期 key
如果某个 upstream 没有配置任何 API key,当前实现会给它补一个“空 token 候选”,仍允许参与选择。
当前这里没有“模型未命中时退回任意 upstream”的额外回退。
没有请求模型时的选择
如果请求里没有 model,运行时不会查 upstream_models,而是直接在当前 route 绑定的 upstream 集合里查首批 active upstream,并继续应用 upstream selector。
这条路径主要服务于:
- 不带模型的请求
- 需要先拿到 upstream 再由上游决定细节的请求
协议过滤
运行时会根据请求子路径过滤候选协议:
/chat/completions、/responses、/messages允许:chat-completionsopen-responsesclaude-messagesbedrock-runtime
GET /models允许所有已通过 route 绑定、认证与 selector 过滤的 active upstream 参与本地模型聚合,不再按协议过滤- 其它路径当前只允许:
chat-completionsopen-responses
因此,数据库里“按模型查得到”不等于“当前请求路径下真的可转发”。
候选记录、排序与 fallback
过滤后的候选会先完整保存到 state.AvailableUpstreams,供:
/v1/models本地构建使用- 运行时上下文和日志使用
然后运行时会:
- 先按
route.upstreams[].priority从小到大分层(数值越小越优先) - 多 API key 当前只是把一个 upstream 展开成多个
id/key凭证候选 - 仅在同 route-upstream 优先级内,按
route.upstreams[].lb_weight做加权随机无放回排序 - 最多保留前
route.max_attempts个候选进入真正的转发计划,默认2 - 最终命中的候选会把自己的 API key 作为本次上游请求使用的出网凭证;如果命中的是空 token 候选,则不会额外附带上游认证 key
真正发请求时,gateway 会按排序后的候选顺序逐个尝试:
- raw HTTP 路径上,首个
2xx响应成功即停止 - chat 路径上,transport error、非
2xx、assemble/parse error 都会触发 fallback - 流式 chat 一旦已经向客户端成功写出首个 event,就锁定当前候选,不再切换
所以“最终可用 upstream”指的是:
在 route 绑定、label selector、模型映射、协议兼容、权重排序和运行时错误之后,当前请求最终实际命中的那个候选。