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-completionsopen-responsesclaude-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_keyslb_weight/prioritymax_idle_conns_per_hostcheck_model- 可选的
capabilities覆盖
当前实现里的生效规则是:
base_url:优先用upstream.base_url,为空时回退到provider.base_urlheaders:先取provider.headers,再用upstream.headers覆盖同名键compat:先取provider.compat,再用upstream.compat覆盖同名键capabilities:upstream.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:
- 按入站协议把请求解析成内部
RequestIR - 按候选 upstream 的
provider.protocol组装出站请求 - 上游响应成功后,再按原始入站协议回组装响应
因此,当前 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[].models 和 upstreams[].model_alias 里声明,再展开成 upstream_models。
5. 一次请求里三者如何配合
当前主链路可以概括为:
- route 先匹配并完成认证
- runtime 取
routes.groups、consumers.groups、consumer_api_keys.groups的交集 - 用请求里的
model去upstream_models里查候选 upstream - 把每个 upstream 的可用
api_keys展开成独立候选;如果 upstream 没有 key,则补一个“空 token 候选” - 过滤掉当前请求子路径不支持的
provider.protocol - 再按 capability 过滤
- 按
upstream_api_key.lb_weight优先、否则回退到upstream.lb_weight做加权随机无放回排序 - 最多保留前 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 根本不是这么解析的