跳到主要内容

模型与 Upstream 解析

总体链路

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

  1. 按路径匹配 active route
  2. 从请求 JSON 中提取 model
  3. 计算 route / consumer / api key 的分组交集
  4. 基于 upstream_models 和分组约束解析候选 upstream
  5. 过滤不支持当前子路径协议的候选
  6. 记录当前请求的全部 AvailableUpstreams
  7. 对候选做加权随机无放回排序,最多保留 5 个
  8. 普通请求按候选顺序转发;GET /v1/models 则基于 AvailableUpstreams 本地构建 models 响应

1. 可用模型授权

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

1.1 请求模型如何参与解析

当前语义:

  • gateway 会从请求 JSON 中提取 model
  • 当请求里带了 model 时,运行时会要求它在 upstream_models.model 中命中
  • 当请求里没有 model 时,运行时会退回到“按分组找首批可用 upstream”

因此,这一层回答的是:

当前这个请求里的模型名,是否能解析出一组明确的 upstream 候选?

1.2 模型名映射

upstream_models 当前同时承担两件事:

  • model:客户端请求时使用的模型名
  • upstream_model:真正发给上游的模型名

如果命中的是 alias 行,当前实现会把请求里的模型名改写成对应的 upstream_model

当前改写路径有两条:

  • chat 路径会把 RequestIR.Model.UpstreamModel 设为候选的 ResolvedModel
  • raw JSON 路径会在出站前把顶层 model 字段重写成 ResolvedModel

因此,当前已经不是“只用模型名选 upstream”,而是已经把模型映射接进了实际出站请求。

1.3 /v1/models 的当前行为

GET /v1/models 当前不是代理上游结果,而是由 gateway 本地构建。

构建方式:

  • 先拿到这次请求解析出的 AvailableUpstreams
  • 提取去重后的 upstream IDs
  • 查询这些 upstream 对应的 upstream_models.model
  • 去重、按字母序排序
  • 返回 OpenAI 兼容的 models list

因此,/v1/models 返回的是:

当前这次请求在当前分组和 upstream 可用性约束下,可见的客户端模型名集合。

它返回的是 upstream_models.model,不是 upstream_models.upstream_model

1.4 日志里的模型字段

当前日志会区分两层模型名:

  • requested_model:客户端原始请求中的模型名
  • upstream_requests[].request.model:真正发给上游的模型名

因此,当 alias 被改写时,日志里可以同时看到:

  • 用户请求的是哪个别名
  • 实际打到上游的是哪个真实模型名

2. 可用 Upstream 选择

当前“可用 upstream”判断的目标,是为本次请求选出一组真实可用的候选,并从中决定最终尝试顺序。

2.1 分组约束

候选 upstream 解析前,运行时会先取下面几组约束的交集:

  • routes.groups
  • consumers.groups
  • consumer_api_keys.groups

然后用交集结果去匹配 upstreams.group

当前语义:

  • 任意一侧空数组都表示“不限制”
  • 如果最终完全没有显式约束,运行时默认只看 default
  • 如果交集为空,直接视为没有可用 upstream

2.2 按模型解析候选

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

  • upstream_models.model = requested model
  • upstreams.group 命中允许组
  • upstreams.disabled_at is null
  • providers.disabled_at is null

同时会把 upstream 的 API key 展开成独立候选,并过滤掉:

  • 没有 key 的无效项
  • 已禁用 key
  • 已过期 key

如果某个 upstream 没有配置任何 API key,当前实现会给它补一个“空 token 候选”,仍允许参与选择。

当前这里没有“模型未命中时退回任意 upstream”的额外回退。

2.3 没有请求模型时的选择

如果请求里没有 model,运行时不会查 upstream_models,而是直接在允许组里查首批 active upstream。

这条路径主要服务于:

  • 不带模型的请求
  • 需要先拿到 upstream 再由上游决定细节的请求

2.4 协议过滤

运行时会根据请求子路径过滤候选协议:

  • /chat/completions/responses/messages 允许:
    • chat-completions
    • open-responses
    • claude-messages
  • 其它路径当前只允许:
    • chat-completions
    • open-responses

因此,数据库里“按模型查得到”不等于“当前请求路径下真的可转发”。

2.5 候选记录、排序与 fallback

过滤后的候选会先完整保存到 state.AvailableUpstreams,供:

  • /v1/models 本地构建使用
  • 运行时上下文和日志使用

然后运行时会:

  • upstream_api_key.lb_weight 优先,回退到 upstream.lb_weight
  • 做加权随机无放回排序
  • 最多保留前 5 个候选进入真正的转发计划

真正发请求时,gateway 会按排序后的候选顺序逐个尝试:

  • raw HTTP 路径上,首个 2xx 响应成功即停止
  • chat 路径上,transport error、非 2xx、assemble/parse error 都会触发 fallback
  • 流式 chat 一旦已经向客户端成功写出首个 event,就锁定当前候选,不再切换

所以“最终可用 upstream”指的是:

在分组、模型映射、协议兼容、权重排序和运行时错误之后,当前请求最终实际命中的那个候选。