通过 OpenRouter 路由

OpenRouter 是 HybridInference 可以路由到的 provider 之一,也是新克隆的仓库默认使用的 provider。本页专门讲 OpenRouter adapter。路由的整体工作方式见架构;模型注册表的语法见添加新模型。

默认的模型目录

如果没有设置任何环境变量,也没有部署 overlay,配置解析最终会落到 config/examples/models.openrouter.yaml。这个文件在 OpenRouter 上注册了三个模型:两个直连,另一个演示本地优先的混合路由,以 OpenRouter 作为回退分支(leg)。只要提供一个 API key,就能跑起一个可用的网关:

source .venv/bin/activate            # the env `make setup-dev` creates
export OPENROUTER_API_KEY=sk-or-...
uvicorn serving.servers.app:app --no-proxy-headers --host 127.0.0.1 --port 8080

请在仓库根目录下运行,因为默认路径都是相对路径。启动日志里会有类似 Registered 3 routes from config/examples/models.openrouter.yaml 的一行,说明选中的是哪份注册表。

curl -s --noproxy '*' http://127.0.0.1:8080/routing

这份目录是起点,不是一成不变的样板:OpenRouter 的模型 slug 会变动,而那个文件里的价格数字只是近似值,仅用于网关自己的用量核算。复制一份随意改。

路由语法

一条 OpenRouter 路由就是某个模型 route: 列表里的一项。kind: 有两种写法。

kind: openrouter

由 OpenRouter 自己按它的默认策略挑选上游 provider。

- kind: openrouter
  weight: 1.0
  base_url: https://openrouter.ai/api/v1
  api_keys:
    - ${OPENROUTER_API_KEY}
  provider_model_id: meta-llama/llama-3.3-70b-instruct

provider_model_id 是 OpenRouter 给这个模型起的 slug。客户端请求的 id: 则是网关自己的名字;两者有意保持独立。

kind: openrouter[<slug>]

把这条分支上的每个请求都钉在同一个 OpenRouter 上游,做法是发送 provider: {order: [<slug>], allow_fallbacks: false}。

- kind: openrouter[deepinfra]
  weight: 1.0
  base_url: https://openrouter.ai/api/v1
  api_keys:
    - ${OPENROUTER_API_KEY}
  provider_model_id: meta-llama/llama-3.3-70b-instruct

slug 必须匹配 [A-Za-z0-9_.-]+,也可以带以 / 分隔的多段(deepinfra、deepinfra/turbo)。写坏的方括号形式——空的 pin、空白字符、嵌套或不配对的方括号——会在注册时报错,而不是被悄悄忽略。OpenRouter 的 provider slug 列表见 https://openrouter.ai/docs/features/provider-routing。

两条分支即使共用一个 base_url,只要 pin 住的上游不同,endpoint_id 也会不同,因为带方括号的 kind 会原样进入标识符。所以它们的熔断器和可用性状态互相隔离:一个上游不稳定,不会连累另一个。不过在 api_logs 里,两种写法记录的都是 provider = "openrouter",所以统计时它们都归在同一个 OpenRouter 分组里。

排序策略

通过管理端 provider-routes API 创建的 OpenRouter 路由,可以带一个 openrouter_sort 策略,取值为 price、throughput 或 latency,发送时写成 provider: {sort: <policy>}。它只对没有 pin 住上游的路由生效——用方括号 pin 住上游的路由发的是 order,以 pin 为准。如果路由不是 OpenRouter 路由,或取值无法识别,API 会返回 422,拒绝这个字段。

adapter 发出的内容

OpenRouterAdapter(apps/backend/serving/adapters/openrouter.py)是通用 OpenAI 兼容 adapter 的一个轻量子类。除了普通请求的内容,它还会额外加上:

  • 归属请求头。OpenRouter 会把流量记在这两个请求头所指明的站点名下,用于它的排行榜和免费额度限制。X-Title 携带你的站点名称,HTTP-Referer 携带站点的公开地址,取自 SITE_NAME 和 SITE_PUBLIC_BASE_URL,或已启用的发行版 manifest。没有公开地址时不发送 HTTP-Referer;没有站点名称时,X-Title 为 HybridInference。如果希望流量记在你自己的站点名下,两个都要设置。

  • usage: {include: true} 加在每一个请求上,好让 OpenRouter 返回它按请求计的 cost 字段。

  • stream_options: {include_usage: true} 加在流式请求上,好让最后一个 SSE 分块带上 usage 块。

  • provider: {...} 用于上文所说的 pin 住上游和排序两种情况。

成本核算

两个数字,刻意分开:

api_logs 中的列

含义

cost_usd

向调用方收取的费用:token 数 × 你在注册表里为该模型声明的价格。不受 OpenRouter 影响

upstream_cost_usd

OpenRouter 上报的、这次请求实际向你收取的费用

对非 OpenRouter 路由,upstream_cost_usd 是 NULL;对 OpenRouter 路由,如果响应里没有带成本数字,它同样是 NULL。

API key

路由的 api_keys: 是一个列表,哪怕只有一个 key,adapter 也把它当作 key 池。有多个 key 时会轮换:某个 key 遇到与 key 本身相关的失败或临时性失败(429、401/402/403、408/425、5xx 或超时)时,请求会交给下一个 key。只有请求已经没有别的 key 可换时,出错的 key 才会被暂停使用,而且只暂停 20 秒。400、422 这类由请求本身导致的失败,换哪个 key 都一样会失败,所以会立即返回,不会白白耗光整个池。

如果 api_keys: 里的某一项引用的环境变量没有设置,这一项就当作不存在。这时,标了 optional: true 的路由会被跳过,并记一条写明模型和 kind 的警告;其他路由会抛出 MissingEnvBackedKeyError,导致启动失败。无论哪种情况,网关都不会注册一条连鉴权都通不过的路由。

错误处理

OpenRouter 的错误和其他 OpenAI 兼容 provider 的错误走同一条处理路径,由两套互相独立的机制处理:

回退。发往某条 OpenRouter 分支的请求不论因为什么失败,router 都会记下这次失败,再按路由顺序尝试该模型的其余分支,跳过被管理员禁用、模态不兼容或熔断已打开的分支。唯一的例外是调用方用 X-Route-Pin 显式 pin 住的请求,这类请求从不回退。如果所有分支都失败,客户端看到的是第一个错误。回退还是直接返回错误,不看失败的状态码,只看还有没有别的分支可用。

熔断器。失败次数按 endpoint_id 累计;连续失败 CIRCUIT_FAILURE_THRESHOLD 次(默认 3)后,该端点在 CIRCUIT_COOLDOWN_SECONDS(默认 30)内不再接收流量,之后再放一个半开测试请求。客户端错误不计入:除 408、429、401 和 407 之外的 4xx 都不会触发熔断,因为一个调用方的错误请求不该让所有人都用不了这个端点,豁免正是为了防止这种连锁反应。408 和 429 说明 OpenRouter 过载,要计入。401 和 407 说明网关自己配置的凭据被拒了——401 是 OPENROUTER_API_KEY 被拒,407 是出网代理要求它自己的凭据——任何调用方都绕不过去,所以不仅计入,还会额外发告警。

402 和 403 本身不计入熔断。但如果路由用 api_keys: 列出了 key,网关会按 API key 一节所说,把它们当作 key 的问题处理:换下一个 key;没有 key 可换时,把这个 key 暂时搁置。没有余额的账号对所有请求都返回 402,所以很快就没有可用的 key,这时熔断就会打开。只写了一个 api_key: 的路由没有 key 池,所以 402 永远不会让它熔断。和 401、407 不同,403 不会发告警,因为服务本身正常时,provider 也会用 403 拒绝单个请求,比如出于内容策略或地区限制。

故障排查

所有 OpenRouter 请求都返回 401。被拒的是网关的 key,不是调用方的。检查后端进程实际拿到的环境里的 OPENROUTER_API_KEY。如果 key 根本没设置,是走不到这一步的——如上文所说,路由会被跳过,或者启动直接失败——所以 401 说明 key 有,只是被 OpenRouter 拒了。

/v1/models 里没有这个模型。确认网关加载的是哪份注册表:启动日志会打出 Registered N routes from <path>,这就是解析链选中的文件。如果那不是你改的文件,说明显式设置的 MODELS_CONFIG_PATH 或某个发行版 manifest 优先于它——见配置。

请求发到了错误的上游。用 GET /routing 查看每个模型实际生效的权重分布。注意这个端点不需要鉴权,而且会暴露上游的 base URL;见架构里的警告。