跳到主要内容

模型与 Upstream 解析

总体链路

一条请求命中 route 后,当前主链路按下面顺序执行:

  1. 按路径匹配 active route
  2. 对真实模型请求从请求 JSON 中提取 model
  3. 先读取 route.upstreams[] 绑定的 upstream 集合
  4. 计算 route / consumer / api key 的 selector 交集(逻辑上是逐层收紧)
  5. 基于 upstream_models 与 label selector 过滤候选 upstream
  6. 过滤不支持当前子路径协议的候选
  7. 记录当前请求的全部 AvailableUpstreams
  8. 对候选做加权随机无放回排序,最多保留数量为 route.max_attempts

可用模型授权

当前“可用模型授权”的实际含义,是请求里的 model 如何参与运行时决策,以及它如何映射成最终发给上游的模型名。

请求模型如何参与解析

当前语义:

  • gateway 只会对真实模型请求从请求 JSON 中提取 model
  • 当请求里带了 model 时,运行时会要求它在 upstream_models.model 中命中
  • 当请求里没有 model 时,运行时会退回到“在 route 绑定 upstream 中找首批可用 upstream”

当前会提取 model 的路径白名单是:

  • POST /chat/completions
  • POST /responses
  • POST /messages
  • POST /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-completionsopen-responsesclaude-messagesbedrock-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_selectorconsumer_api_key.route_selector 必须同时匹配当前 route.labels,否则请求返回 403。

通过 route 访问控制后,运行时会取 route.upstreams[] 中显式绑定的 upstream 作为基础集合。

如果 route.upstreams[] 为空,则当前 route 直接没有任何可用 upstream。

在此基础上,运行时会继续按下面几层 selector 逐步收紧:

  • routes.upstream_selector
  • consumers.upstream_selector
  • consumer_api_keys.upstream_selector

如果模型请求携带 model,还会对 upstream_models.labels 再套用:

  • routes.model_selector
  • consumers.model_selector
  • consumer_api_keys.model_selector

完整 selector 语义、label 语法、保留前缀与内置系统 label,见 Label & Selector

按模型解析候选

如果请求里带了 model,运行时会在 upstream_models 中查:

  • upstream_models.model = requested model
  • route.upstreams[] 绑定了对应 upstream
  • upstream 与 upstream model 同时命中多层 label selector
  • upstreams.disabled_at is null
  • providers.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-completions
    • open-responses
    • claude-messages
    • bedrock-runtime
  • GET /models 允许所有已通过 route 绑定、认证与 selector 过滤的 active upstream 参与本地模型聚合,不再按协议过滤
  • 其它路径当前只允许:
    • chat-completions
    • open-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、模型映射、协议兼容、权重排序和运行时错误之后,当前请求最终实际命中的那个候选。