快速开始

刚 clone 下 HybridInference 之后,第一件要做的事就是这个。它把一个本地网关依次带过三个阶段:

  1. 用一个结果确定的假 provider 验证整条路由链路;

  2. 在同一个正在运行的 Compose 项目上继续,接入 Web Console(Web 控制台)、Admin Console(管理控制台)、Postgres、账号、API key 与请求历史;

  3. 把假 provider 换成本地的 OpenAI 兼容服务器——vLLM、SGLang 或 Ollama。

不需要在「纯路由」和「全栈」两种产品之间做选择。小 router 只是同一条路径上的第一个检查点:先看到一次成功的请求,再去添加那些失败方式更多的部件。

前两个阶段不需要 provider 账号、主机上的 .env、GPU、SMTP 服务或付费 API key。仓库的 CI 会跑同一串 make upmake smokemake demomake demo-smoke.github/workflows/ci.yml 里的 Tutorial E2E job),因此下面这些命令在每一次改动到它们的提交上都被真正执行过。

如果更想不用 Docker、直接从源码检出运行网关并对接真实模型,见安装——但如果还没见过这个网关真正服务一次请求,先回到这里。

需要准备什么

  • Docker Engine 24+,带 Compose v2。用 docker compose version 确认。

  • 一个正在运行的 Docker daemon。在 macOS 上先启动 Docker Desktop 或 Colima;docker info 必须成功,并且 clone 进的目录必须是 Docker 允许共享进容器的目录。

  • Git、curl、GNU Make 和 Python 3.10–3.13(推荐 3.12)。smoke 客户端只用 Python 标准库,所以没有 pip install 这一步。

  • 几 GB 的空闲磁盘,用于 backend、frontend、Postgres 镜像和本地数据库卷。

  • 以下回环端口需要空闲:backend 18080、frontend 13001、Postgres 15432。阶段 1 只会用到 18080——这个 overlay 上的 make up 是带 --no-deps 启动 backend 的,所以 frontend 和 Postgres 一直到阶段 2 之前都不会起。要覆盖它们,见端口已被占用

不需要 Node.js,因为 frontend 是在 Docker 里构建的。只有在阶段 3 使用带 GPU 的本地服务器时才需要 GPU。

阶段 1:证明 router 能正常工作

clone 仓库,启动这个可运行的 distribution:

git clone <repository-url> hybridinference
cd hybridinference

make up DISTRIBUTION=example

会启动两个容器:example-provider——一个结果确定的 OpenAI 兼容上游,以及 backend——HybridInference 网关。Postgres 和 frontend 保持停止;在这个检查点上账号与鉴权都是关闭的。

运行自动检查:

make smoke DISTRIBUTION=example
EXAMPLE_SMOKE_OK

smoke 会等待启动完成,然后验证 /health/site-config/v1/models 以及一次经过路由的 completion。

自己动手检查网关

查看健康状态:

curl -s localhost:18080/health
{
  "status": "healthy",
  "routes_configured": 1,
  "database_configured": false,
  "database_connected": false
}

列出模型:

curl -s localhost:18080/v1/models

响应里含有对外的模型 id example-chat。它的注册表条目在 distributions/example/config/models.yaml,后端通过 distribution manifest distributions/example/distribution.yaml 找到这个文件——这套解析过程见配置

该文件是只读挂载进 backend 的,并没有烤进镜像。要验证重新加载这条路径,可以临时把模型的 name(随仓库分发的值是 Runnable Example Chat)改成 Reloaded Example Chat,运行 make restart s=backend DISTRIBUTION=example,再列一次模型。不用重建镜像,新的名字就会出现。继续之前把它改回 Runnable Example Chat,再重启一次 backend。

发送一次 completion:

curl -s localhost:18080/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"example-chat","messages":[{"role":"user","content":"Say hello."}]}'

assistant 的内容是:

RUNNABLE_EXAMPLE_OK

这个固定回复证明请求确实经过 HybridInference 的路由到达了随附的上游。客户端寻址的是 example-chat;路由把它映射到上游自己的模型 id。

流式用的是同一个端点:

curl -sN localhost:18080/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"example-chat","messages":[{"role":"user","content":"Say hello."}],"stream":true}'

同一个响应里的每个 chat.completion.chunk 帧都重复同一个 completion id,流以字面量 data: [DONE] 哨兵结束。

想知道网关刚才走了哪条路由,直接问它:

curl -s localhost:18080/routing

这是一个无需鉴权的端点,会报出每个模型的上游 base URL 和权重。在笔记本上这正是你想要的,在公网主机上则不是——见路由里的警告。

这里不要运行 make down。阶段 2 是在同一个 Compose 项目上原地扩展的。

阶段 2:继续接入 Web Console 与 Admin Console

加上 Postgres 和 frontend,并在开启鉴权的情况下重建 backend:

make demo DISTRIBUTION=example

这是一次原地过渡,不是第二次部署。随附的 provider 继续在原有项目和网络里运行。Postgres 带着一个归 example 所有的持久卷被加进来,backend 针对它重建,frontend 最后加入。

打开 http://localhost:13001/signup——如果你覆盖过 FRONTEND_PORT,就换成你自己的那个——在浏览器里走完整个流程:

  1. admin@local.dev、一个用户名,以及一个至少八位、含大写、小写和数字的密码注册。用一个只给这个 demo 用、不会在别处复用的密码,然后接受条款。

  2. Back to Login,再用同一个邮箱和密码登录。这个只跑在回环地址上的示例关闭了邮箱验证。

  3. 在仪表盘上创建一个 API key,然后显示并复制它,下面的 API 调用要用。

  4. 打开 API Playground,选择 example-chat,发一条消息。回复是 RUNNABLE_EXAMPLE_OK

  5. 打开 Admin Console。这个账号之所以是管理员,是因为它的邮箱匹配了 distributions/example/deploy/docker-compose.demo.yml 里 example 显式设置的 ADMIN_EMAILS

UI 和 API 共用一个 origin。发往 13001 端口上 /v1/auth/user/admin 的请求,由前端重写到 Compose 网络内部的后端(apps/frontend/next.config.js)。

用刚复制的 key,通过这个 origin 发一次正常的带鉴权请求:

export HYBRIDINFERENCE_API_KEY='<your copied key>'

curl -s localhost:13001/v1/chat/completions \
  -H "Authorization: Bearer ${HYBRIDINFERENCE_API_KEY}" \
  -H 'Content-Type: application/json' \
  -d '{"model":"example-chat","messages":[{"role":"user","content":"Say hello."}]}'

现在回到仪表盘的 Recent Requests 区块。异步日志写完后,这次用 API key 发出的 completion 会出现在历史里。Playground 是另一条独立的 UI 路由验证,它有意绕过了正常的 completion 日志器,所以不要拿它发的消息来检查历史。

仪表盘上还可能出现 Agents、pgAdmin 这类可选服务的卡片。它们不属于本示例;除非单独部署这些服务,否则它们的路由是不可用的。

用上面选的密码运行这个结果确定的全栈检查:

EXAMPLE_DEMO_ADMIN_PASSWORD='<the same password>' \
make demo-smoke DISTRIBUTION=example
EXAMPLE_FULL_SMOKE_OK

这个检查会登录已有账号(如果跳过了浏览器流程就先创建它),创建或复用一个 API key,通过前端 origin 调用普通和流式 completion,跑一遍 Playground 与 Admin API,验证第二个本地用户从 Admin API 拿到 403,核对请求历史,重建 backend,再证明同一个账号、refresh cookie 会话和 API key 仍然可用。它不会打印任何密钥,也不会重置数据库。

阶段 3:把假 provider 换成本地推理

在主机上启动一个 OpenAI 兼容的 vLLM、SGLang、Ollama 或同类服务器。它必须监听一个 Docker 能访问到的地址,例如 0.0.0.0:8000;只绑定在主机 127.0.0.1 上的服务器,backend 容器访问不到。绑定 0.0.0.0 可能把一个无鉴权的模型服务器暴露到局域网,因此要用主机防火墙限制该端口;如果运行时支持,也可以改为绑定一个 Docker 能访问的内网接口。

然后把同一个对外模型指向那台服务器:

export EXAMPLE_UPSTREAM_BASE_URL=http://host.docker.internal:8000/v1
export EXAMPLE_UPSTREAM_API_KEY=local-placeholder
export EXAMPLE_UPSTREAM_MODEL='<served-model-name>'
make demo DISTRIBUTION=example

make demo 会重建 backend,让新的上游设置生效,同时保留账号、API key、frontend 和 Postgres 卷。客户端和 Playground 请求的仍然是 example-chat,变的只是它背后那条路由。curl -s localhost:18080/routing 现在会报出新的 base_url(如果你覆盖过 BACKEND_PORT,这里也要换成你自己的)。

EXAMPLE_UPSTREAM_API_KEY 是网关向那个 provider 出示的凭据。它不是阶段 2 里签发的 HYBRIDINFERENCE_API_KEY——后者是客户端向网关出示的凭据。

用 Playground 或阶段 2 里那条带鉴权的 curl 测试真实模型。不要拿随附的两个 smoke 去测它:这两个检查都刻意断言假 provider 那个精确的哨兵,而真实模型不应被期待产出它。

示例里有什么

这个示例就是一个 distribution 目录:

distributions/example/
├── EXAMPLE_OVERLAY
├── distribution.yaml
├── distribution.demo.yaml
├── config/
│   ├── models.yaml
│   └── routing.yaml
├── deploy/
│   ├── backend.env
│   ├── docker-compose.yml
│   └── docker-compose.demo.yml
├── fixtures/fake-openai-provider/
├── smoke.py
└── full_smoke.py

distribution.yaml 描述阶段 1 的能力,公开注册是关闭的。distribution.demo.yaml 描述同一个 distribution 在开启鉴权和 frontend 之后的样子。两者复用同一套模型与路由文件,不会复制出第二份注册表。

两个 Compose overlay 遵循同样的递进。第一个加入假 provider,让只启动 backend 的这一步结果确定。第二个加入数据库/frontend 设置和一个 example 作用域的数据库卷。示例里没有任何东西会用到生产的 hybridinference_postgres_data 卷。

EXAMPLE_OVERLAY 把这个目录标记为教学用产物,这样裸的 distribution 发现流程就不会把它误认成一个真实部署。它不是阶段 1/阶段 2 的开关;只有显式的 demo 目标才会加上第三层 Compose。

接下来去哪里

  • 要搭建自己的部署,照着这个目录形状复制一份,去掉教学标记和假 provider,替换掉每一个仅本地可用的身份与密钥,再加上你的环境需要的部署控制项。

  • 配置——设置项、环境变量,以及一个部署如何提供自己的文件。

  • 添加新模型——模型注册表条目及其 route: 列表。

  • 路由——加权选择、回退、熔断、会话亲和,以及如何加入自己的路由策略。

  • 安装——从源码检出运行网关,对接真实 provider。

停止、恢复与重置

在阶段 2 或阶段 3 之后,停掉全部四个服务,同时保留账号、API key 和请求历史:

make demo-down DISTRIBUTION=example

make demo DISTRIBUTION=example 恢复阶段 2。要恢复阶段 3,先把那一阶段的三个 EXAMPLE_UPSTREAM_* export 重新设置好,再运行同一条命令;这些 shell 覆盖值不会存进数据库。要停掉整套栈并删除这个示例项目的数据:

make demo-reset DISTRIBUTION=example

demo-reset 对示例数据是破坏性的,但它删不掉生产数据库卷。如果是有意在阶段 1 之后就停下,用 make down DISTRIBUTION=example

一旦阶段 2 已经启动,就不要再用单独的 make up DISTRIBUTION=example 试图把正在运行的项目降回去:那条命令只会应用阶段 1 的 Compose 输入,可能让全栈服务停在混合状态。改用 make demo-reset DISTRIBUTION=example,然后从阶段 1 重新开始。

故障排查

端口已被占用

在这条线性路径的每一条命令上重复同样的覆盖设置:

BACKEND_PORT=28080 FRONTEND_PORT=23001 DB_PORT=25432 \
make up DISTRIBUTION=example

BACKEND_PORT=28080 make smoke DISTRIBUTION=example

BACKEND_PORT=28080 FRONTEND_PORT=23001 DB_PORT=25432 \
make demo DISTRIBUTION=example

BACKEND_PORT=28080 FRONTEND_PORT=23001 DB_PORT=25432 \
EXAMPLE_DEMO_ADMIN_PASSWORD='<the password from signup>' \
make demo-smoke DISTRIBUTION=example

之后在阶段 1 用 backend 端口 28080,在阶段 2 和阶段 3 用 frontend 端口 23001。对外发布的站点身份跟随的是当前挡在最前面的那个端口:阶段 1 是 backend 的,从阶段 2 起是 frontend 的——在那里 docker-compose.demo.yml 会把 SITE_PUBLIC_BASE_URLBASE_URLFRONTEND_URL 改指向 FRONTEND_PORT。后面每一次 make demomake demo-smoke(包括阶段 3 的那条命令)都要保持这三个端口分配一致;这两条命令都可能重建容器。

无法连接 Docker daemon

先启动 Docker Desktop 或 Colima。docker info 必须能打印出 server 段,make up 才能工作。

/v1/models 为空,且 routes_configured: 0

backend 读不到示例的配置。在 macOS 上最常见的原因是检出落在 Docker 的共享文件路径之外:这时 distributions/ 这个 bind mount 在虚拟机里解析成一个空目录,/app/distributions/example/distribution.yaml 这份 manifest 不存在,make logs s=backend DISTRIBUTION=example 会明确说出这一点。把检出移到共享路径之下(或者在 Docker Desktop 的 File sharing 设置里把你的路径加进去),再运行一次 make up DISTRIBUTION=example

make up 选中了别的 distribution

跟着本教程走时,始终要传 DISTRIBUTION=example。裸的 make up 可能发现检出里真实的部署 overlay;示例标记就是为了不让这个教程被隐式选中。

跑到一半后重新开始

make demo-reset DISTRIBUTION=example,然后从阶段 1 重新开始。不要按宽泛的名字去删 Docker 卷,也不要 prune 无关的项目。

查看出问题的服务

make logs DISTRIBUTION=example 在每个阶段都能看到共享 Compose 项目里正在运行的服务。阶段 2 之后要单看某一个服务,用:

make logs s=backend DISTRIBUTION=example

按需要把 backend 换成 frontendpostgresexample-provider