安装

如何把一个 HybridInference 网关跑起来——可以用 Docker 服务栈,也可以用一份能直接改的源码检出。

如果你更想先看到网关真的应答一次请求、再去配置别的,那就从快速开始开始。它跑的是一个确定性的假 provider,不需要 provider 账号、不需要 API key,连 .env 都不需要。本页是下一步:用你自己的 provider,搭你自己的部署。

需要准备什么

用途

要求

Docker 服务栈

Docker Engine 24+ 与 Docker Compose v2+

源码检出

Python 3.10–3.13(按 pyproject.toml,推荐 3.12)与 uv

在 Docker 之外运行控制台

Node.js 22(CI 安装的版本)

Linux 或 macOS。所有依赖都来自 PyPI,所以一次普通的 uv sync 除了能访问索引之外别无要求。

用 Docker 快速开始

git clone <repository-url> hybridinference
cd hybridinference
cp .env.example .env

接下来编辑 .env这三个变量没有值时,make up 会在启动任何东西之前中止——deploy/docker/docker-compose.yml 用 Compose 的 :? 必填写法声明了它们,而 Compose 是在变量插值阶段失败,不是在运行时:

变量

.env.example 里的初始状态

DB_NAME

已设为 hybridinference——保留或改名都行

DB_USER

为空;必须自己填

DB_PASSWORD

为空;必须自己填

还有两个 Compose 不强制、但在有人注册之前就该设好:JWT_SECRET_KEYAPI_KEY_SECRET。两者出厂都是空的;如果一直空着,apps/backend/serving/servers/app.py 会打一条 CRITICAL 日志,然后带着不安全的 token 和不安全的 API key 哈希继续运行。各生成一个:

python3 -c "import secrets; print(secrets.token_urlsafe(48))"

然后启动这套服务栈:

make up     # creates the external volume, then brings up Compose
make ps     # show the services and their health
curl -s http://localhost:8080/health

make up 会先执行 Makefile 的 docker-volumes 目标,在 Docker 卷 hybridinference_postgres_data 不存在时创建它。这个卷在 Compose 文件里声明为 external: true,所以 Compose 自身从不创建、也不删除它——想清空数据库之前,先看部署

会启动三个容器:

服务

发布地址

说明

backend

127.0.0.1:8080

FastAPI 网关;用 BACKEND_HOST / BACKEND_PORT 覆盖监听地址

frontend

0.0.0.0:3001

Next.js 控制台;用 FRONTEND_HOST / FRONTEND_PORT 覆盖

postgres

127.0.0.1:5432

DB_PORT 覆盖

pgAdmin 和 Codex on-call relay 也在 Compose 文件里,但被 profile 挡着,不主动要求就不会有东西启动它们(见部署指南)。

全新的服务栈会提供哪些模型

一份没有自带模型注册表的克隆并不是空的。当 MODELS_CONFIG_PATH 和处于 active 的 distribution manifest 都没有点名某个文件时,后端会回退到内置的参考注册表 config/examples/models.openrouter.yaml,并搭配 config/examples/routing.minimal.yaml(见 apps/backend/serving/config/distribution.py_LEGACY_DEFAULTS)。这份注册表通过 OpenRouter 路由,只需要一个凭据:

OPENROUTER_API_KEY=...   # in .env

改完之后要再跑一次 make up,而不是 make restart:容器只在创建时读取自己的 env_file,所以 docker compose restart 会原样保留旧的环境变量,而 up 会把配置有变动的服务重建。要让网关改用你自己的注册表,见下面的配置添加新模型

开发用检出(不用 Docker)

git clone <repository-url> hybridinference
cd hybridinference

make setup-dev

make setup-dev 会用 Python 3.12 创建 .venv(如果已有的 .venv 是别的次版本,它会拒绝继续),以可编辑方式安装本项目,同步 dev 依赖组,并装好 pre-commit hooks。手动来做的话:

uv venv -p 3.12
source .venv/bin/activate
uv sync --group dev

在仓库根目录运行网关——后端的包在 apps/backend/ 下面,所以才要设置 PYTHONPATH

cp .env.example .env    # edit as above; a process started here reads it
PYTHONPATH=apps/backend uv run uvicorn serving.servers.app:app \
  --host 127.0.0.1 --port 8080

在另一个终端里运行控制台:

cd apps/frontend
npm ci
npm run dev            # listens on :3001

想在不启动整套服务栈的情况下对着 Postgres 开发,就只起数据库容器:

docker compose -f deploy/docker/docker-compose.yml --env-file .env up -d postgres

或者在 .env 里设 DB_ENABLED=false,完全不带数据库运行:网关照样路由请求,/health 会报告 "database_configured": false。账户、API key 和请求历史则需要数据库。

配置

环境变量

仓库根目录的 .env 就是那唯一的一个文件。有两处会读它:

  • backend 进程,当你从仓库根目录启动它时(apps/backend/serving/config/settings.py 里的 Settings.Config.env_file = ".env");

  • 各个容器,因为 Compose 以 --env-file .env 调用,而 backend 服务也把它列为 env_file

除了快速开始里那五个必填变量(DB_NAMEDB_USERDB_PASSWORDJWT_SECRET_KEYAPI_KEY_SECRET),以及默认注册表需要的 OPENROUTER_API_KEY.env.example 中的一切都是可选的。最可能用得上的是这些:

变量

作用

USER_AUTH_ENABLED

1(默认)要求 /v1/* 带 API key;0 放行所有请求

ADMIN_TOKEN

/admin/* 端点使用的 bearer token

DB_ENABLED

false 让网关不带数据库运行

DB_STORE_FULL_CONTENT

false(默认)对 prompt 和响应做哈希,而不是把原文存下来

FRONTEND_URL

用户在验证邮件和重置邮件里点击的绝对 URL

BASE_URL

本网关自己的公网 origin;留空则从请求中推导

LOG_LEVEL, LOG_FORMAT

日志详细程度,以及 json/纯文本输出

ALERTS_ENABLED, SLACK_ALERTS_WEBHOOK_URL

进程内告警,默认关闭

TRUST_PROXY_HEADERS

是否信任 X-Forwarded-For / X-Real-IP

Provider 凭据

代码里并没有一份固定的 provider 变量清单。模型注册表在加载时会对 ${VAR}${VAR:-default} 做插值(apps/backend/routing/config.py 里的 _expand_env_value),所以一个部署需要哪些 provider 凭据,正好就是它自己的注册表点名的那些变量:

route:
  - kind: openrouter
    base_url: https://openrouter.ai/api/v1
    api_keys:
      - ${OPENROUTER_API_KEY}

.env.example 为本项目有 adapter 或示例的那些 provider 提供了空白占位;你也可以随意添加自己的变量名。内置的默认注册表只需要 OPENROUTER_API_KEY

配置文件在哪里

本仓库里没有 config/models.yaml,也没有 config/routing.yamlapps/backend/serving/config/distribution.py 里的 resolve_config_path 按以下顺序解析每个配置文件:

  1. 显式的环境变量——MODELS_CONFIG_PATHROUTING_CONFIG_PATHALERTS_CONFIG_PATH(旧的 MODELS_CONFIGROUTING_CONFIG 写法仍然接受;两者都设置时以 *_CONFIG_PATH 为准);

  2. distribution manifest——DISTRIBUTION_CONFIG_PATH 指向某个 distributions/<name>/distribution.yaml,其中的 paths: 一节点名了各个文件。该 manifest 只在 DISTRIBUTION_CONFIG_MODE=active 时才生效;默认的 dark 只加载并校验它、记录它本来会改动什么,而解析结果保持原样;

  3. 内置默认值——config/examples/models.openrouter.yamlconfig/examples/routing.minimal.yaml。没有默认的告警文件,它的缺席意味着内置阈值生效。

路径相对于工作目录(容器里是 /app)。Compose 文件把 config/distributions/ 都以只读方式挂进 backend,所以在宿主机上改动其中任意一个再重启 backend 就够了——不必重新构建镜像。

这些文件里面该写什么,见配置;一个完整走通的 overlay 示例见快速开始

备注

直接跑在宿主机上的 backend 可以通过 localhost 访问宿主机上的推理服务器。而 Docker 里的 backend 必须用 Docker 能访问到的地址,例如 host.docker.internal,并在模型注册表里显式写出来;HybridInference 不会改写 provider 的 URL。

验证安装

make lint     # ruff format --check, ruff check, pydocstyle
make test     # pytest, excluding the external and dbtest tiers
make check    # lint + test
make format   # ruff format, then ruff check --fix --unsafe-fixes

对着运行中的网关做一次端到端验证:

curl -s http://localhost:8080/v1/models

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

如果设了 USER_AUTH_ENABLED=0,就去掉 Authorization 头。模型 id 要用 /v1/models 真的列出来的那些。

文档

这份开发者文档是 MyST Markdown,由 Sphinx 从 docs/developer/ 构建。本地构建方式以及为其把关的检查,见贡献指南

故障排查

required variable DB_USER is missing a value: DB_USER must be set in .env file——Compose 在变量插值阶段就停了。在 .env 里填上 DB_USERDB_PASSWORD,见上面的表格。

env file ... .env not found——backend 服务读的是相对 deploy/docker/../../.env,也就是仓库根目录的 .env。在 make up 之前先 cp .env.example .env

从源码运行时报导入错误——请在仓库根目录、带上 PYTHONPATH=apps/backend 运行,并确认虚拟环境已激活(或改用 uv run)。

所有请求都 404,且 /v1/models 为空——说明注册表什么都没加载进来。启动时 backend 要么打印 Registered N routes from <path>,要么打印一条指出它找不到的注册表路径的错误;把那个路径和上面的优先级列表对一下。

端口已被占用——在 .env 里覆盖 BACKEND_PORTFRONTEND_PORTDB_PORT