跳到主要内容

Upstream / Provider / Protocol

这三个概念在 V2 里容易混在一起,但当前代码里的边界其实很明确:

  • provider 是可复用的协议与默认连接模板
  • upstream 是 tenant 下实际可选的上游目标
  • protocol 不是独立资源,而是 provider.protocol 这个字段

一句话概括:

route 决定请求能不能进来,upstream_models 决定模型能落到哪些 upstream,provider 决定这些 upstream 按什么协议说话,upstream 决定这次请求具体连到哪里、带什么头、用哪个 key。

1. Provider 是什么

providers 表和 Management API 里的 Provider 资源,本质上是“模板层”。

它负责定义:

  • protocol
  • 默认 base_url
  • 默认 headers
  • 默认 compat
  • 默认 capabilities
  • 默认 check_model
  • 来源归属:source_scope / source_ref_id

当前支持的 protocol 只有三种:

  • chat-completions
  • open-responses
  • claude-messages

代码里的关键点:

  • runtime 不靠“厂商品牌”工作,而是主要靠 provider.protocol + compat
  • provider 可以是 global,也可以是 tenant 自定义
  • provider 禁用后,关联的 upstream 会一起失效,因为 runtime 查询会过滤 providers.disabled_at is null

所以 provider 更像“协议模板 + 默认连接参数”,而不是最终被请求直接命中的目标。

2. Upstream 是什么

upstreams 表和 Management API 里的 Upstream 资源,是 tenant 下真正参与选路的上游目标。

每个 upstream 必须绑定一个 provider:

upstream.provider_id -> providers.id

它负责定义:

  • 这个 tenant 有哪些实际上游目标
  • 上游属于哪个 group
  • 实际生效的 base_url
  • 实际生效的 headers
  • 实际生效的 compat
  • api_keys
  • lb_weight / priority
  • max_idle_conns_per_host
  • check_model
  • 可选的 capabilities 覆盖

当前实现里的生效规则是:

  • base_url:优先用 upstream.base_url,为空时回退到 provider.base_url
  • headers:先取 provider.headers,再用 upstream.headers 覆盖同名键
  • compat:先取 provider.compat,再用 upstream.compat 覆盖同名键
  • capabilitiesupstream.capabilities 非空时完整覆盖 provider;为空时继承 provider
  • token:不是来自 provider,也不是来自 upstream 顶层字段,而是来自当前选中的 upstreams.api_keys[]

这意味着:

  • provider 给默认值
  • upstream 决定 tenant 侧的最终连接形态

3. Protocol 是什么

当前 V2 里没有单独的 protocols 表,也没有独立的 Protocol 管理资源。

protocol 的真实位置是:

  • 管理面:Provider.protocol
  • 运行时:ResolvedUpstream.Protocol

它表达的是:

  • 这个上游要按哪种协议解析请求
  • forwarder 应该选择哪套 assembler / parser
  • gateway 是否能把当前入站请求转换成这个上游协议

入站协议怎么判定

chat 主路径的入站协议按请求子路径判断:

  • /chat/completions -> chat-completions
  • /responses -> open-responses
  • /messages -> claude-messages

出站协议怎么判定

出站协议不看 route,也不看 upstream 表上的某个字段,而是看:

selected upstream -> provider -> protocol

也就是 provider.protocol

当前协议转换的真实行为

chat 主线路径已经固定走统一 IR:

  1. 按入站协议把请求解析成内部 RequestIR
  2. 按候选 upstream 的 provider.protocol 组装出站请求
  3. 上游响应成功后,再按原始入站协议回组装响应

因此,当前 chat 主路径已经支持跨协议转发。

route.protocol_transformation_type 这个字段目前仍会被持久化和校验,但不会切换 chat 主链路的行为分支。它现在更像一个保留中的配置位,而不是实际生效的 runtime 开关。

4. 模型映射落在哪里

模型不挂在 provider 上,也不直接挂在 upstream 主表字段里。

当前 runtime 的模型映射来自 upstream_models

  • model:客户端请求使用的名字
  • upstream_model:真正发给上游的名字
  • is_alias:是否为 alias 行

所以:

  • provider 不负责“这个模型去哪”
  • upstream 也不直接负责“这个模型叫什么”
  • 真正的“模型 -> upstream -> 上游模型名”映射在 upstream_models

seed 只是为了写起来方便,允许在 upstreams[].modelsupstreams[].model_alias 里声明,再展开成 upstream_models

5. 一次请求里三者如何配合

当前主链路可以概括为:

  1. route 先匹配并完成认证
  2. runtime 取 routes.groupsconsumers.groupsconsumer_api_keys.groups 的交集
  3. 用请求里的 modelupstream_models 里查候选 upstream
  4. 把每个 upstream 的可用 api_keys 展开成独立候选;如果 upstream 没有 key,则补一个“空 token 候选”
  5. 过滤掉当前请求子路径不支持的 provider.protocol
  6. 再按 capability 过滤
  7. upstream_api_key.lb_weight 优先、否则回退到 upstream.lb_weight 做加权随机无放回排序
  8. 最多保留前 5 个候选,按顺序尝试和 fallback

这里可以看出:

  • provider 决定协议和默认值
  • upstream 决定候选目标和 tenant 侧覆盖
  • protocol 决定 forwarder 怎么说话

6. 配置时该把信息放哪

如果你的目标是:

  • “给某个厂商协议做一个可复用模板” -> 配 provider
  • “给某个 tenant 新增一个真正可打的 endpoint” -> 配 upstream
  • “控制请求用哪种协议和上游交互” -> 配 provider.protocol
  • “把客户端模型名映射到真实上游模型名” -> 配 upstream_models
  • “切换某个 tenant 当前实际使用的 header / base URL / key” -> 配 upstream

常见误区:

  • 不要把 protocol 当成 upstream 级字段理解,当前不是
  • 不要把 provider 当成真实上游实例,当前它只是模板层
  • 不要把模型映射写到 provider,当前 runtime 根本不是这么解析的

7. 相关阅读