Maxim 团队开发的一个企业级开源 AI 网关(AI Gateway)。它的核心定位是作为企业/开发者应用与底层各大 AI 大模型之间的“中间件”,通过统一接口、高性能路由和故障容错机制,确保 AI 服务的稳定性与高可用。
Bifrost AI 网关在 RAX3000M 路由器上的部署指南
一份照着执行即可成功部署的步骤文档。所有命令已在目标设备验证。
⚠️ 标注的是「这一步容易写错的地方」,按标注的正确做法执行即可,无需排障。
1. 部署背景与环境
| 项目 | 值 |
|---|---|
| 设备 | CMCC RAX3000M(中国移动定制路由器) |
| 固件 | ImmortalWrt 24.10.4(LuCI openwrt-24.10 branch) |
| CPU | ARMv8 Processor rev 4 (v8l) × 2(Cortex-A53,aarch64) |
| 目标平台 | mediatek/filogic(arm64) |
| 内存 | 1GB(可用约 587MB) |
| 内置闪存 | 仅 22MB 可用 → Docker 必须装 USB |
| USB 盘 | /dev/sda1(ext4),挂载 /mnt/sda1,剩余约 3.2GB |
| 部署对象 | bifrost AI 网关(maximhq/bifrost:latest = v2.0.0,Go 单容器,SQLite 存储) |
| 上游 | OpenRouter(API Key 接入) |
| 访问入口 | http://192.168.10.1:8080(LAN 可访问) |
网络链路
客户端
│ DNS: dnsmasq:53 ──► AdGuardHome:5333 ──► OpenClash:7874 ──► 公网
│ HTTP: bifrost:8080 ──► OpenClash mixed-port:7892(fake-ip 出口)──► OpenRouter
OpenClash 为 fake-ip 模式(代理域名返回 198.18.x.x)。容器使用 host 网络与路由器共用网络栈,天然走这条链路出网,无需额外端口映射与防火墙配置。
2. 前置检查
free -m # 内存 ≥512MB 可用
df -h / # 内部剩余 ≤ ~100MB 就必须装 USB(本机 22MB)
ls /dev/sd* # USB 盘已挂载(本例 /mnt/sda1)
cat /proc/filesystems | grep -E "overlay|^nodev\tcgroup" # 需有 overlay + cgroup
cat /sys/fs/cgroup/cgroup.controllers # 需有 memory/pids/cpu
镜像架构核查(拉取前确认存在 arm64 版本):
curl -s "https://hub.docker.com/v2/repositories/maximhq/bifrost/tags?page_size=25" \
| grep -o '"architecture":"[^"]*"' | sort | uniq -c # 必须含 arm64
3. 阶段一:安装 Docker(装入 USB)
3.1 opkg 配置 usb dest 并安装
grep -q "dest usb" /etc/opkg.conf || cat >> /etc/opkg.conf <<'EOF'
dest usb /mnt/sda1/opkg
EOF
mkdir -p /mnt/sda1/opkg
opkg install --dest usb dockerd docker # 自动带 containerd/runc/tini/libseccomp
3.2 软链二进制与依赖库到系统路径
B=/mnt/sda1/opkg/usr/bin; S=/mnt/sda1/opkg/usr/sbin
for b in dockerd docker containerd containerd-shim containerd-shim-runc-v2 ctr tini docker-init docker-proxy; do
ln -sf $B/$b /usr/bin/$b
done
ln -sf $S/runc /usr/bin/runc; ln -sf $S/runc /usr/sbin/runc
ln -sf /mnt/sda1/opkg/etc/init.d/dockerd /etc/init.d/dockerd
for f in $(find /mnt/sda1/opkg/usr/lib -name "libseccomp*.so*"); do ln -sf "$f" /usr/lib/$(basename "$f"); done
dockerd --version && runc --version | head -1 # 两条命令都应成功输出版本
⚠️ libseccomp 必须软链:runc 依赖
libseccomp.so.2,它装在 USB 的 lib 目录,不软链到/usr/lib会导致 dockerd 无法启动。
3.3 编写 daemon.json
写 /etc/docker/daemon.json:
{
"data-root": "/mnt/sda1/docker",
"iptables": false,
"ip6tables": false,
"bridge": "none",
"storage-driver": "overlay2",
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
⚠️
bridge:"none"+iptables:false必须成对出现:此配置让 dockerd 不创建 docker0 桥、不碰 iptables,规避精简内核未编译的 veth/iptables-nat 模块依赖。漏掉任一项都可能启动失败。⚠️
data-root必须指向 USB:写在内部闪存会把仅有的 22MB 撑爆。
3.4 通过官方 init 脚本启动
OpenWrt 的 /etc/init.d/dockerd 默认不读取 /etc/docker/daemon.json,需在 UCI 配置里指定 alt_config_file。
写 /etc/config/dockerd:
config globals 'globals'
option alt_config_file '/etc/docker/daemon.json'
启动并验证:
mkdir -p /mnt/sda1/docker
/etc/init.d/dockerd enable
/etc/init.d/dockerd start
sleep 6
docker info | grep -E "Server Version|Docker Root Dir|Storage Driver"
# 期望输出:Docker Root Dir=/mnt/sda1/docker、Storage Driver=overlay2
⚠️ 开机自启前提:USB 必须先于 dockerd 挂载。确认
uci show fstab已有fstab.@mount[].target='/mnt/sda1'且 enabled;若没有,用block detect生成 fstab 并 enable。
4. 阶段二:部署 bifrost 容器
4.1 拉取镜像
docker pull maximhq/bifrost:latest # 236MB,多架构自动取 arm64
docker image inspect --format '{{.Architecture}}' maximhq/bifrost:latest # 输出 arm64
4.2 初始化数据卷
mkdir -p /mnt/sda1/bifrost/data && chown -R 1000:1000 /mnt/sda1/bifrost/data
4.3 运行容器(最终命令)
docker run -d --name bifrost --network host --restart unless-stopped \
--entrypoint /app/main \
-v /mnt/sda1/bifrost/data:/app/data \
-e LOG_LEVEL=info -e LOG_STYLE=json \
maximhq/bifrost:latest \
-app-dir /app/data -host 0.0.0.0 -port 8080
⚠️ 必须带
-host 0.0.0.0:不加则只监听127.0.0.1,局域网内无法访问。⚠️ 必须带
-app-dir /app/data:不加则数据写入容器层,删除容器后配置与日志全部丢失。数据目录必须挂载 USB 卷并属主设为 uid 1000(bifrost 以该用户运行)。⚠️ 为什么用
--entrypoint /app/main:本设备上镜像自带的官方入口脚本存在崩溃问题,直接执行二进制/app/main可稳定运行。这是本环境下的既定正确做法,不要去掉。⚠️ 改配置用 API,不要反复删建容器:容器运行后数据卷内会生成 SQLite 数据库文件,反复
docker rm+docker run重建会引入文件损坏风险。后续调整 provider/虚拟 key/限流配置一律通过 HTTP API 完成(见阶段三、四、五)。
4.4 确认容器健康
docker ps --filter name=bifrost # 状态 Up
curl -s http://127.0.0.1:8080/health # {"status":"ok"}
5. 阶段三:注册 OpenRouter provider
⚠️ API 语义:
provider字段传的是字符串(provider 名),不是对象;注册分两步——先建 provider,再单独添加 key。
5.1 方案 A:原生 openrouter provider(推荐)
原生 provider 使用内置的默认接口地址,配置最简,且会自动拉取完整模型目录。
# 建 provider(不要写 network_config.base_url,原生类型自带默认地址)
curl -X POST http://127.0.0.1:8080/api/providers -H "Content-Type: application/json" \
-d '{"provider":"openrouter","provider_type":"openrouter"}'
# 添加 key(把 sk-or-v1-... 换成你的真实 key)
curl -X POST http://127.0.0.1:8080/api/providers/openrouter/keys -H "Content-Type: application/json" \
-d '{"name":"or-free","value":"sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxx","models":["*"],"weight":1.0}'
⚠️ 原生 provider 不要写
base_url:显式覆盖 base_url 会指向错误地址,导致模型列表拉取失败。不写时 key 校验返回status: success,模型目录自动拉满。
5.2 方案 B:自定义 OpenAI 兼容 provider(需要指定 base_url 时)
curl -X POST http://127.0.0.1:8080/api/providers -H "Content-Type: application/json" \
-d '{"provider":"openrouter-free","network_config":{"base_url":"https://openrouter.ai/api"},"custom_provider_config":{"base_provider_type":"openai"}}'
curl -X POST http://127.0.0.1:8080/api/providers/openrouter-free/keys -H "Content-Type: application/json" \
-d '{"name":"openrouter-free","value":"sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxx","models":["*"],"weight":1.0}'
⚠️ base_url 不要带
/v1:bifrost 会对自定义 openai provider 自动追加/v1/chat/completions。若 base_url 写成https://openrouter.ai/api/v1,实际请求会变成.../api/v1/v1/chat/completions,返回 404。正确写法:https://openrouter.ai/api。⚠️ 名称不能与内置标准 provider 重名:
openrouter、openai、anthropic等是保留名,自定义 provider 需另取名字(如openrouter-free)。
5.3 验证 provider 状态
curl -s http://127.0.0.1:8080/api/providers
# 期望:provider_status=active,且 key 的 status=success
curl -s http://127.0.0.1:8080/api/providers/openrouter/keys
6. 阶段四:配置虚拟 key(客户端鉴权与白名单)
区分两种 key:provider key 是 bifrost 访问上游厂商(OpenRouter)的凭证;虚拟 key(
sk-bf-前缀)是客户端调用网关用的凭证,在 Dashboard 的 Virtual Keys 里创建。虚拟 key 决定该客户端能用哪些 provider 和模型(白名单),并可按 key 挂限流。生产环境建议客户端一律走虚拟 key。
6.1 管理 API 端点(管理 API 当前无鉴权)
| 操作 | 端点 |
|---|---|
| 列表 | GET /api/governance/virtual-keys |
| 详情 | GET /api/governance/virtual-keys/{id} |
| 创建 | POST /api/governance/virtual-keys |
| 更新 | PUT /api/governance/virtual-keys/{id} |
| 删除 | DELETE /api/governance/virtual-keys/{id} |
⚠️ 必须带
/api前缀:/governance/virtual-keys(不带/api)会被 SPA 前端兜底页吞掉,返回 HTML 而不是 JSON。
6.2 provider_configs 的正确写法
provider_configs 数组里每个条目声明「允许哪个 provider、绑定哪个 key、放行哪些模型」:
{
"name": "my-key",
"description": "for my app",
"is_active": true,
"provider_configs": [
{
"provider": "openrouter",
"weight": null,
"key_ids": ["<provider key 的 key_id>"],
"allowed_models": ["*"],
"blacklisted_models": []
}
],
"mcp_configs": []
}
provider key 的 key_id 从 GET /api/keys 获取(注意是 key_id 字段,不是 name)。
⚠️
allow_all_keys是只读回显字段,不要靠它:POST/PUT 传allow_all_keys:true会被服务端忽略并强制为false,导致请求报no keys found。「允许该 provider 的所有 key」只能在 Dashboard UI 里勾选;通过 API 必须用key_ids显式绑定。
⚠️ 两个典型报错:
403 Provider 'xxx' is not allowed for this virtual key→ 该 provider 没写进provider_configs(白名单缺失),补一条即可;400 no keys found for provider: xxx→provider_configs存在但没绑定 key(key_ids为空),用key_ids重新绑定。
6.3 客户端调用规范
curl -s -X POST http://192.168.10.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-bf-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
-d '{"model":"openrouter/aion-labs/aion-2.0","messages":[{"role":"user","content":"hi"}]}'
⚠️ Authorization 头要写完整:
-H "Authorization: Bearer <key>"是一整个参数,容易漏写-H或在中间加逗号,导致 curl 解析错误。
⚠️ 未设 admin key 时鉴权可选:当前部署没设
BIFROST_ADMIN_KEY,不带Authorization头也能直达网关(不做虚拟 key 校验)。要强制所有客户端走虚拟 key,需在 Dashboard 设置 admin key。虚拟 key 自身的限流(如 20 次/分钟)在 Dashboard 的 Virtual Keys 里配置,与 admin key 无关。
7. 阶段五:配置 429 限流防护
OpenRouter 免费模型有严格的并发与频率限制,超出即返回 429。bifrost 默认配置为 零重试 + 无限并发(max_retries=0、concurrency=1000),不调整时一打就 429。按下面保守值配置即可:
curl -X PUT http://127.0.0.1:8080/api/providers/openrouter -H "Content-Type: application/json" \
-d '{
"name":"openrouter",
"network_config":{
"default_request_timeout_in_seconds":600,
"max_retries":5,
"retry_backoff_initial":2000,
"retry_backoff_max":30000,
"stream_idle_timeout_in_seconds":120,
"keep_alive_timeout_in_seconds":30,
"max_conns_per_host":100
},
"concurrency_and_buffer_size":{
"concurrency":2,
"buffer_size":50
},
"proxy_config":null,
"send_back_raw_request":false,
"send_back_raw_response":false,
"store_raw_request_response":false
}'
参数说明
| 参数 | 默认值 | 建议值 | 作用 |
|---|---|---|---|
max_retries |
0 | 5 | 429/5xx 时自动重试次数 |
retry_backoff_initial |
500ms | 2000ms | 重试前初始等待,让限流窗口滑动 |
retry_backoff_max |
5000ms | 30000ms | 退避上限 |
concurrency |
1000 | 2 | 并发上限,主动避免打爆免费额度 |
buffer_size |
5000 | 50 | 排队缓冲,超出即拒绝(背压) |
⚠️ 必须主动压低
concurrency:429 是 OpenRouter 侧限流,网关只能通过「低并发 + 退避重试」来规避和吸收,靠默认配置达不到效果。
8. 部署前须知:OpenRouter :free 免费模型
实测 google/gemma-4-31b-it:free 经 openrouter-free/google/gemma-4-31b-it:free 已成功返回完整对话(HTTP 200)。
调用格式:<provider前缀>/<模型名>,例如:
curl -s -X POST http://192.168.10.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-bf-xxxxxxxx" \
-d '{"model":"openrouter-free/google/gemma-4-31b-it:free","messages":[{"role":"user","content":"hi"}]}'
建议:免费模型逐个实测,能通的直接经网关调用;个别解析失败的模型(如 z-ai/glm-5.2:free)再考虑直连 OpenRouter 或换模型,不必整体放弃免费模型。付费(非 :free)模型始终可用(返回 402 表示仅差额度)。
9. 端到端验证清单
# 服务健康
curl -s http://127.0.0.1:8080/health # {"status":"ok"}
# provider 与 key 状态
curl -s http://127.0.0.1:8080/api/providers # provider_status=active
curl -s http://127.0.0.1:8080/api/providers/openrouter/keys # key status=success
# 虚拟 key 列表与白名单
curl -s http://127.0.0.1:8080/api/governance/virtual-keys # 查看 provider_configs
# 模型目录已拉取
curl -s "http://127.0.0.1:8080/api/models?limit=1000" | grep -c '"name"' # 数百个即正常
# 真实聊天请求(带虚拟 key;非 free 模型返回 402 = 链路全通仅差额度)
curl -s -X POST http://192.168.10.1:8080/v1/chat/completions -H "Content-Type: application/json" \
-H "Authorization: Bearer sk-bf-xxxxxxxx" \
-d '{"model":"openrouter/aion-labs/aion-2.0","messages":[{"role":"user","content":"hi"}]}'
# 路由器本机出网(host 网络下等价容器出网)
curl -s -o /dev/null -w "%{http_code}\n" --max-time 20 https://openrouter.ai/api/v1/models \
-H "Authorization: Bearer sk-or-v1-xxxxxxxx"
10. 运维建议
- 数据位置:
/mnt/sda1/bifrost/data(config.db + logs.db)。调整配置前先cp -r备份。 - 看日志:
docker logs --tail 50 bifrost。 - 开机自启:
--restart unless-stopped已生效,前提是 USB 先于 dockerd 挂载(检查 fstab)。 - Dashboard 安全:按需设置访问密码(
Settings → Security,或容器环境变量BIFROST_ADMIN_KEY)。 - 资源占用:USB 约 395MB、内存余约 570MB,对 1GB 内存路由器很轻量。
- logs.db 增长:长期运行会增长,可定期清理或设置日志保留策略。
