配置

本页说明运行中的网关如何找到自己的配置:读哪些文件、这些文件放在哪里,以及当多个层级指向同一个文件时哪一层胜出。它与添加新模型(逐字段的模型注册表参考)和路由(路由引擎拿到结果后做什么)互为概念上的配套。

网关启动时读什么

类型

存放内容

读取方

models

模型注册表:网关对外提供的每一个模型 id,以及它背后的上游路由

apps/backend/serving/servers/registry.py 中的 register_from_models_yaml

routing

部署级的路由行为:本地/远程划分、健康探测

apps/backend/routing/manager.py 中的 RoutingManager

alerts

告警规则与阈值

apps/backend/serving/observability/alert_config.py 中的 load_alert_config

环境变量

一切属于密钥或与主机相关的东西:凭据、数据库连接、功能开关

apps/backend/serving/config/settings.py 中的 Settings

只有模型注册表是承重的。没有它,网关照样启动、照样提供 /healthGET /v1/models 返回一个空列表。没有路由文件,注册表自己的每条路由权重继续生效——一份全新的 clone 拿到的其实也是这个效果,因为内置默认值 config/examples/routing.minimal.yaml 声明的是空的端点池,不会覆盖任何东西。没有告警文件,则采用内置阈值。

还有第四种类型 mcp,解析器和 distribution manifest 的 schema 都接受它,但在当前修订版本里没有任何代码读它。

配置放在哪里

本仓库里没有 config/models.yaml,也没有 config/routing.yamlconfig/ 下只有一个目录 config/examples/,里面是供复制或直接指向的参考文件:

config/examples/
├── models.openrouter.yaml     # neutral default catalog (llama-3.3-70b, ...)
├── routing.minimal.yaml       # companion routing file, names no hosts
└── distribution.example.yaml  # annotated manifest reference

真实部署的配置放在 distribution overlay 里:distributions/ 下的一个目录,装着该部署的 manifest、配置文件、Compose/env 输入和品牌信息。

distributions/<name>/
├── distribution.yaml       # the manifest: identity + where the config files are
├── config/
│   ├── models.yaml
│   └── routing.yaml
└── deploy/
    ├── backend.env         # any *.env here; Compose reads them all
    └── docker-compose.yml  # optional overlay on deploy/docker/docker-compose.yml

这样拆分,是为了让源码树和容器镜像保持中立。模型 id、上游 base URL、端口、站点标识和品牌,都属于某一个部署,而不属于项目本身;把它们放进 overlay,镜像就可以只构建一次,再把某个部署的 overlay 挂载进去(deploy/docker/docker-compose.yml 以只读方式挂载 overlay,而不是把它打包进镜像)。这也意味着,从本仓库克隆下来的代码不会带上任何人的主机地址。

网关如何找到自己的配置

优先级依次是:环境变量、manifest、内置默认值。

apps/backend/serving/config/distribution.py 中的 resolve_config_path() 对每一种配置类型独立解析,顺序如下:

  1. 显式的环境变量MODELS_CONFIG_PATHROUTING_CONFIG_PATHALERTS_CONFIG_PATH。(旧名 MODELS_CONFIGROUTING_CONFIG 仍然被接受;两者同时设置时,规范名 *_CONFIG_PATH 胜出。)ALERTS_CONFIG_PATH 只有在你真的设置了它时才算一次覆盖:它有一个非空的内置默认值,因此解析器跟踪的是你有没有提供这个值,而不是拿它跟 "" 比较。

  2. distribution manifest 的 paths:——仅当 DISTRIBUTION_CONFIG_MODE=active 时;见下文。

  3. 内置默认值,指向参考示例:config/examples/models.openrouter.yamlconfig/examples/routing.minimal.yamlalerts 的默认值是 config/alerts.yaml——本仓库有意不提供这个路径——告警文件缺失就意味着「使用内置阈值」。

有两条推论值得记牢:

  • 环境变量压过 manifest。如果一个部署既有 manifest,你又设置了 MODELS_CONFIG_PATH,那么 manifest 里的 models: 路径就只是摆设。网关会把这件事记进日志,而不是把它藏起来(explicit env override ... wins over manifest value ...)。两种机制只挑一种用。

  • 来自环境变量的路径,相对于进程的工作目录解析;manifest 里的相对路径,则相对于 manifest 文件自己所在的目录解析。

从第 1 层或第 2 层解析出来、但实际并不存在的路径,只是一条警告,而不是失败:网关记录 Models config not found: <path> (source=env),然后在没有注册任何模型的情况下继续运行。

distribution manifest

manifest 是一份带版本号的 YAML 文档,它给部署命名,并告诉网关这个部署的配置文件在哪里。config/examples/distribution.example.yaml 是带注释的参考;结构如下:

schema_version: 1

distribution:
  id: example
  display_name: Example Router
  release: "1.0.0"

site:
  public_base_url: https://your-gateway.example
  support_email: support@your-gateway.example

features:
  routers: [fixed]
  public_signup: false
  rag: false

paths:
  models: config/models.yaml      # relative to this file's directory
  routing: config/routing.yaml

deployment:
  target: local

用两个环境变量把网关指向它:

export DISTRIBUTION_CONFIG_PATH=distributions/<name>/distribution.yaml
export DISTRIBUTION_CONFIG_MODE=active

默认是 dark 模式,这是有意为之

DISTRIBUTION_CONFIG_MODE 默认为 dark。dark 模式下,manifest 会被加载并校验,解析器把它本来会改动的内容记进日志,但当前的解析结果仍然有效:

[distribution dark mode] models config stays config/examples/models.openrouter.yaml
(source=default, sha256=51fd6cde50fd); manifest would use
/srv/app/distributions/example/config/models.yaml (sha256=6117799cd0bf) — DIFFERENT

因此,只设置 DISTRIBUTION_CONFIG_PATH 改变不了任何行为;你会收到一条警告,告诉你模式取了默认值 dark。任何无法识别的模式值同样会降级为 dark 并给出警告,所以拼错只可能压掉一次计划中的启用,绝不会误触发一次启用。启用 manifest 的做法是:先跑 dark,读那些对比行,确认之后再设成 active

失败行为按模式区分,这是刻意的。dark 模式下,加载不了的 manifest 只被记录并跳过。active 模式下,路径正是从 manifest 来的,所以加载不了的 manifest——overlay 挂载丢了、YAML 写错了——会拒绝启动,而不是悄悄用另一份注册表对外服务。

站点标识,以及 manifest 里不能放什么

site:features: 会由 GET /site-config 以公开子集的形式对外提供;distribution.display_namesite: 则通过 apps/backend/serving/config/site_identity.py 中的 get_site_identity(),供给后端渲染的内容(事务性邮件、署名头)。两者都以 DISTRIBUTION_CONFIG_MODE=active 为前提;在其他任何模式下,/site-config 返回一份中立文档,站点标识回退到 SITE_NAME / SITE_PUBLIC_BASE_URL / SITE_DOCS_URL / SITE_SUPPORT_EMAIL,或者回退到中立的默认值。

manifest 里绝不能放密钥。凭据留在环境变量里,而且 schema_version: 1 有意不把环境变量插值进 manifest 的取值。

示例 distribution

distributions/example/ 是一个完整、可运行的 overlay,作为教学用的样例留在仓库里:一份 manifest、一份只含单个模型的注册表(指向随仓库自带、无需凭据的假 provider)、Compose overlay,以及冒烟脚本。可以配合快速开始走一遍:

make up DISTRIBUTION=example
make smoke DISTRIBUTION=example

它带着一个标记文件 EXAMPLE_OVERLAY,只要这个文件在,它就不会被自动发现选中。Makefile 挑选 distribution 的规则是这样的:

  • overlay 从 distributions/*/deploy/*.env 发现,并排除任何含有 EXAMPLE_OVERLAY 的目录。

  • 恰好只有一个候选:自动选中它,make 会打印它正在按哪一个站点标识编译。

  • 有多个候选:make 失败,并要求你指定其中一个。

  • 一个都没有(刚克隆本仓库时就是这个状态):不选任何 overlay,整套服务保持中立。

DISTRIBUTION=<name> 按名字选择——示例也在其中,而它只可能被显式选中。DISTRIBUTION=none 表示不选。

全新克隆、完全没有配置时会怎样

在一个干净的检出里启动网关,没有 MODELS_CONFIG_PATH、没有 manifest、也没有 overlay,那么内置默认值生效:网关提供 config/examples/models.openrouter.yaml。这个文件注册了三个模型——两个直接走 OpenRouter 的路由,一个把本地 OpenAI 兼容服务器排在前面、以 OpenRouter 作为自动回退——而它们需要的都是同一把凭据。

export OPENROUTER_API_KEY=sk-or-...
uv run uvicorn serving.servers.app:app --port 8080

没有这个变量,文件里的每个模型都会被跳过——它那条路由唯一的 API key 展开后是空的——网关会在开始对外服务之前就把这件事说出来:

No models are available: every model in config/examples/models.openrouter.yaml was
skipped because its credential is unset. Set OPENROUTER_API_KEY and restart.
/v1/models will stay empty until then, and requests will report the model as not found.

这是让网关跑上真实流量的最快路径。等你想清楚要提供哪些模型,再用自己的注册表替换这个文件。

模型注册表

添加新模型 是逐字段的参考。有三件事属于本页,因为它们是配置加载的性质,而不是某一个字段的性质:

路由的 kind: 决定用哪个 adapter;模型的 provider: 是它的默认值route: 下的每一项都写明一个 kind:,而 registry._make_adapter 正是按 kind 分派的。路由省略 kind: 时,取模型顶层的 provider:;模型完全没有 route: 时,会用顶层的 provider:base_urlapi_key 合成出一条路由。provider 同时也是写入 api_logs.provider、并在指标里展示的标签,所以一条路由可以用 provider: 单独覆盖它。

adapter 的 kind 只在一个地方定义_make_adapterapps/backend/serving/servers/registry.py)里的分派逻辑就是唯一的事实来源。在当前修订版本里它接受:

类型

Adapter

openai_compat, staging, vllm, sglang, ollama, chutes, featherless, cliproxy, deepseek, kimi, minimax

OpenAICompatAdapter

kimi_coding, zai

CodingIdentityAdapter

openrouter, openrouter[<slug>]

OpenRouterAdapter

claude

ClaudeAdapter

gemini

GeminiAdapter

anthropic

AnthropicAdapter

本地推理服务器没有专用 adapter:vllmsglangollama 都是 OpenAI 兼容的 kind,区别只在 provider 标签和用量处理上。其他任何值都会在启动时抛出 ValueError: Unknown adapter kind: <kind>。如果不想信这张表,而想从代码里重新推出这个列表:

sed -n '/def _make_adapter/,/Unknown adapter kind/p' apps/backend/serving/servers/registry.py

grepif kind 会漏掉其中大部分:OpenAI-compat 那一支是一个 if kind in (,后面十一个名字各占一行,所以它们一个都不会出现在输出里。

同一个模块里紧挨着的 RESERVED_PROVIDER_LABELS 集合是另一份不同的、更长的列表——那是路由不能拿去当自定义 provider: 的标签——其中还包含 openairouter 这类并不是 adapter kind 的名字。

注册表里的环境变量插值只支持整值替换。在 models.yaml 里,只有当整个字符串恰好是 ${VAR} 时才会展开。没有 ${VAR:-default},也没有嵌在字符串中间的替换:

base_url: ${LOCAL_BASE_URL}                  # expanded
base_url: ${LOCAL_BASE_URL:-http://x/v1}     # NOT expanded — treated as a var
                                             #   named "LOCAL_BASE_URL:-http://x/v1",
                                             #   resolves empty, model is skipped
base_url: http://${LOCAL_HOST}/v1            # NOT expanded — the literal string,
                                             #   including "${LOCAL_HOST}", is used

路由文件用的是另一个更强的展开器(见下文)。不要把一个文件里的习惯带到另一个文件上。

路由文件

routing.yaml 是可选的。它配置部署级的权重策略和健康探测循环。它的 schema 是 apps/backend/routing/config.py 中的 RoutingConfig;下面是每个键及其真实默认值:

默认值

含义

default_router

fixed

部署级的权重策略。只有 fixed 有实际作用:取其他任何值时,RoutingManager.apply() 都会直接返回,不动权重。

timeout

2

一次健康探测等待连接的秒数。

health_check

0

两次健康探测之间的秒数;0 表示关闭探测。

local_deployment

[]

被当作本地的端点。

remote_deployment

[]

被当作远程的端点。

logging

{}

自由格式的映射。

每个 deployment 条目都必须同时有 endpoint:(必须以 http://https:// 开头)和一个非空的 models: 列表;models: 为空的条目会让整个文件校验失败,网关会记录 RoutingManager failed to initialize,并继续使用注册表自己的权重。而 endpoint: 展开为空的条目只会被单独丢弃,并给出一条警告点名那些被孤立的模型,所以一个没设置的变量不会让其余端点全部作废。

default_router: fixed
timeout: 2
health_check: 30

local_deployment:
  - endpoint: ${LOCAL_BASE_URL:-http://localhost:8000}
    models: [<model-id>]

remote_deployment:
  - endpoint: https://api.your-provider.example/v1
    models: [<model-id>]

它是这样生效的:RoutingManager 把每个模型已注册的 adapter 分成本地和远程两组——一个 adapter 归入某一组的条件是,它的 base_url 与该条目的 endpoint: 完全一致,并且该模型出现在这个条目的 models: 里。然后它在两组之间分配流量(FixedRatioStrategy,默认 50/50,除非已废弃的 routing_parameter.local_fraction 块另有设置),在每组内部平均分摊权重,最后重新归一化,使一个模型各条路由的权重之和为 1.0。当某个模型的一组为空时,全部权重归另一组。在这里什么都匹配不上的模型,保留它 route: 列表里的权重。

健康探测只覆盖 local_deployment 的端点——远程 provider 不提供网关那条 /health 路径,会因此被判为不健康。每次探测都是对端点 origin 根路径加 /health 发一个 GET。探测失败的端点会被排除在分组之外,它的权重转给仍然存活的路由;一旦某次探测成功,它会自动回归。

与模型注册表不同,这个文件的展开器支持 ${VAR}${VAR:-default},以及嵌在更长字符串里的变量,且在任意嵌套深度上都有效。

routing_strategy:routing_parameter:default_router: 和每模型 router_params: 的已废弃写法。它们仍然能加载,并会记录一条废弃警告。每模型的 router 选择(模型注册表里的 router: / router_params:)会覆盖 default_router,其说明见路由

环境变量

Settingsapps/backend/serving/config/settings.py)会读取工作目录下的 .env 文件和进程环境变量,大小写不敏感;进程环境变量胜出。仓库根目录的 .env.example 是带注释的清单——把它复制成 .env 再改。密钥只属于这里:不放在模型注册表里(那里用 ${VAR} 引用),也不放在 manifest 里。

用你自己的配置运行

uv run uvicorn serving.servers.app:app --port 8080

然后检查实际加载了什么:

curl -s localhost:8080/health           # includes routes_configured
curl -s localhost:8080/v1/models        # generated from the registered adapters

启动日志是解析结果的权威。Registered N routes from <path> 点明最终胜出的是哪个文件;[distribution dark mode] ... 那几行显示 manifest 本来会改动什么;Skipping model '<id>' ... after env expansion 逐个点名凭据未设置的模型。

警告

GET /routing 不需要任何凭据,就会为每一条已发布的路由返回上游的 base_url、它的 provider 标签和它的权重——也就是你完整的上游拓扑,包括 base URL 里写着的主机和端口。它的别名 GET /admin/routing 在当前修订版本里同样没有挂 admin 依赖,这与 /admin/* 下的其余接口不同,而且它还会额外报出 /routing 隐藏掉的路由。任何能从公网访问到的网关,都要在反向代理上把这两个路径挡掉,并把它们的输出当作敏感信息对待。