快速开始

刚 clone 下 HybridInference,第一件事就是走一遍本教程。整个过程在同一个本地网关上分三个阶段完成:

  1. 用一个固定回复的假 provider 跑通整条路由链路;

  2. 在这个正在运行的 Compose 项目上继续,加上 Web 控制台、管理控制台、Postgres、账号、API key 和请求历史;

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

每个阶段都在上一阶段的基础上继续,所以你会先看到一个请求跑通,再去加那些更容易出错的部分。

前两个阶段不需要 provider 账号、主机上的 .env、GPU、SMTP 服务或付费 API key。只要有改动涉及这两个阶段,CI 都会把同样的命令跑一遍。

如果你想不用 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 空闲磁盘,用来放后端、前端和 Postgres 的镜像,以及本地数据库卷。

  • 以下回环端口需要空闲:后端 18080、前端 13001、Postgres 15432。阶段 1 只用到 18080:在这个 overlay 上,make up 会带 --no-deps 启动后端,所以前端和 Postgres 要到阶段 2 才会启动。要改用别的端口,见端口已被占用。

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

阶段 1:确认路由能正常工作

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

git clone https://github.com/HarvardMadSys/hybridInference.git hybridinference
cd hybridinference

make up DISTRIBUTION=example

这会启动两个容器:example-provider 是一个总是返回固定回复的 OpenAI 兼容上游,backend 是 HybridInference 网关。Postgres 和前端仍然停着;在这个阶段,账号和鉴权都是关闭的。

运行自动检查:

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,后端通过发行版 manifest distributions/example/distribution.yaml 找到这个文件——具体怎么查找,见配置。

这个文件是挂载进容器的,没有打进镜像,所以改完只需要重启后端。想试一下的话,把模型的 name 从 Runnable Example Chat 改成 Reloaded Example Chat,运行 make restart s=backend DISTRIBUTION=example,再列一次模型。继续之前把它改回来,再重启一次。

发送一次 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

只有随附的假 provider 才会发这个回复,所以看到它,就说明请求确实走了网关的路由。客户端请求的是 example-chat,路由再把它换成 provider 自己的模型名。

流式用的是同一个端点:

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 事件的形式返回,最后以 data: [DONE] 这一行结束。

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

curl -s localhost:18080/routing

这个端点不需要鉴权,会返回每个模型的上游 base URL 和权重。在自己的笔记本上这正合适,放到公网主机上就不行了——见路由里的警告。

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

阶段 2:接入 Web 控制台和管理控制台

加上 Postgres 和前端,并重建后端、打开鉴权:

make demo DISTRIBUTION=example

这是在原有项目上原地升级,不是再部署一套。随附的 provider 继续在原来的项目和网络里运行。先加入 Postgres,使用示例专属的持久卷;然后重建后端,连上这个数据库;最后加入前端。

新数据库里还没有账号。第一次启动时,后端会生成签发会话、保护 API key 的密钥,存好示例的设置项,并在日志里打印一个一次性的设置码:

docker logs hybridinference-example-backend 2>&1 | grep 'setup code'

make logs s=backend DISTRIBUTION=example 也能看到同一行;按 Ctrl-C 停止跟踪日志。在设置完成之前,设置码保持不变,后端每次启动都会再打印一遍。

打开 http://localhost:13001/setup(如果改过 FRONTEND_PORT,就换成你的端口),在浏览器里完成下面的步骤:

  1. 输入设置码,然后创建管理员:用户名填 admin,密码至少八位,要包含大写字母、小写字母和数字。这个密码只在 demo 里用,不要和别处的密码重复。这个账号没有邮箱地址,用用户名登录。admin 适合这个只跑在回环地址上的示例,下面的全栈检查也要用它;真实部署则应该选一个难猜的名字,原因见第一个管理员账号。

  2. 下一步会列出部署通常要填写的设置项。这个示例一个都不需要:点 Save and continue,再点 Finish。管理控制台会打开它的 Configuration 标签页,那里列出网关存在数据库里的每一个设置项,示例自己的取值也在其中;见存储在数据库中的设置项。

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

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

  5. 再打开 Admin Console。这个账号是管理员,因为它是首次运行设置创建的。其他人现在可以在 http://localhost:13001/signup 注册成为普通用户;这个只跑在回环地址上的示例关闭了邮箱验证。

Providers 和 Routing 两个标签页就是给运行中的网关添加 provider、key 或模型的地方;从管理控制台做运行时配置会逐个讲。

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 区块。刚才发的请求稍等一会儿就会出现在那里。Playground 里发的消息不会记进这份历史,所以检查时要用像这样的 API 请求。

仪表盘上还可能出现 Agents、pgAdmin 这类可选服务的卡片。它们不属于本示例,除非你另外部署这些服务,否则对应的页面都打不开。

用上面选的密码运行全栈自动检查:

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

这个检查以 admin 登录。如果你跳过了浏览器里的步骤,它会自己完成首次运行设置,设置码从后端日志里读取。它复用或新建一个 API key,并通过控制台的地址发送普通请求和流式请求。它还会跑一遍 Playground 和 Admin API,确认第二个非管理员用户调用 Admin API 时拿到 403,并核对请求历史。最后它会重建后端,确认账号、它的登录会话和 API key 都仍然可用:它们背后的密钥存在数据库里,而不在容器里。它不会打印任何密钥,也不会重置数据库。

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

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

然后把同一个公开模型指向那台服务器。这个模型的路由从三个设置项读取上游,阶段 2 已经把它们存进了示例的数据库。在管理控制台里打开 Configuration,在 Providers 下找到它们,设置为:

设置项

取值

EXAMPLE_UPSTREAM_BASE_URL

http://host.docker.internal:8000/v1

EXAMPLE_UPSTREAM_API_KEY

local-placeholder

EXAMPLE_UPSTREAM_MODEL

你的服务器提供的模型名

点 Save。模型注册表只在后端启动时读取这些设置项,所以会出现 Restart pending 提示:点 Restart backend,等页面重新加载。(make demo DISTRIBUTION=example 也有同样的效果,它会重建后端容器。)账号、API key、前端和 Postgres 卷都不受影响。客户端和 Playground 请求的仍然是 example-chat,变的只是它背后那条路由。现在 curl -s localhost:18080/routing 会显示新的 base_url(如果改过 BACKEND_PORT,这里也要换成你的端口)。

阶段 2 开始之后,在 shell 里 export EXAMPLE_UPSTREAM_* 不会改变路由:存储的设置项优先于环境变量,Configuration 标签页会把这样的变量标为 Environment ignored。要换回自带的 provider,就对这三个设置项点 Reset,再重启一次后端。

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

用 Playground 或阶段 2 里那条带鉴权的 curl 测试真实模型。随附的两个 smoke 检查都不要拿来测它:它们都要求拿到假 provider 的固定回复,而真实模型不会给出这个回复。

示例里有什么

这个示例就是一个发行版——一个部署文件目录,网关读的是这里的文件,而不是源码里内置的任何东西:

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 描述同一个发行版在打开鉴权和前端之后的样子。两者用的是同一套模型和路由配置文件,不会再复制一份注册表。

两个 Compose 文件也是按同样的顺序递进的。第一个加入假 provider,只启动后端。第二个加入数据库和控制台的设置,以及一个示例自己专用的数据库卷;第一次启动时会把这些设置复制进这个数据库,此后要改就在那里改。示例里的任何东西都不会碰真实部署用的 hybridinference_postgres_data 卷。

EXAMPLE_OVERLAY 文件把这个目录标记为示例,所以不指定发行版的 make up 永远不会误选它;要用它,得传 DISTRIBUTION=example。它跟你处在哪个阶段无关。

下一步

  • 要搭建自己的部署,就把这个目录复制一份,删掉 EXAMPLE_OVERLAY 和假 provider,把所有只在本地用的名字和密钥都换掉;发行版还能设置什么,见发行版定制。

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

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

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

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

停止、恢复与重置

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

make demo-down DISTRIBUTION=example

用 make demo DISTRIBUTION=example 恢复。阶段 3 的上游设置也存在数据库里,所以无论你停在哪个阶段,都用这同一条命令恢复。要停掉整套服务并删除这个示例项目的数据:

make demo-reset DISTRIBUTION=example

demo-reset 会删掉示例数据——管理员、生成的密钥和每一个已存储的设置项都会一起删除,下一次 make demo 又会从首次运行设置开始——但删不到生产数据库卷。如果你本来就打算在阶段 1 之后停下,用 make down DISTRIBUTION=example。

阶段 2 启动之后,不要用单独的 make up DISTRIBUTION=example 把正在运行的项目退回阶段 1:这条命令只会应用阶段 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 setup>' \
make demo-smoke DISTRIBUTION=example

之后在阶段 1 用后端端口 28080,在阶段 2 和阶段 3 用前端端口 23001。站点生成的链接也跟着这些端口走:docker-compose.demo.yml 会用 FRONTEND_PORT 拼出 SITE_PUBLIC_BASE_URL、BASE_URL 和 FRONTEND_URL,第一次 make demo 会把它们存进示例的数据库。后面每一次 make demo 或 make demo-smoke 都要沿用这三个端口,因为这两条命令都可能重建容器。之后换一个 FRONTEND_PORT 也改不了已经存下的地址;要么在 Configuration 标签页上改,要么重置示例从头再来。

设置页面不接受设置码

再运行一遍阶段 2 里的 docker logs 命令,从最后一行复制设置码。即使更早的行里有你后来重置掉的示例数据库的设置码,最后一行的也才是当前的设置码。输错的设置码会被限流,但正确的设置码总会被接受。如果日志里根本没有设置码,说明设置已经完成,直接登录即可。

管理员没有邮箱地址,所以忘了密码也没法通过邮件重置。改用命令行重置:

docker exec -it hybridinference-example-backend \
  python -m serving.auth.reset_password admin

它会让你输入两次新密码,并让这个账号在所有地方退出登录;加上 --generate 则不再询问,直接打印一个强密码。

无法连接 Docker daemon

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

/v1/models 为空,且 routes_configured: 0

后端读不到示例的配置。在 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=example。不传的话,只要你的仓库里有真实的发行版,make up 就会用它,而且永远不会选这个示例。

跑到一半后重新开始

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

查看出问题的服务

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

make logs s=backend DISTRIBUTION=example

按需要把 backend 换成 frontend、postgres 或 example-provider。