知行手记首页

AI 网关 防止429限流

分类技术笔记
防止上游 API 服务商(如 OpenAI、Bedrock)报 429 错而进行的智能排队、平滑削峰与高并发连接池调度,Bifrost 的微秒级代理调度能力会更优
AI 网关 防止429限流

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 重名openrouteropenaianthropic 等是保留名,自定义 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)的凭证;虚拟 keysk-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_idGET /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: xxxprovider_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=0concurrency=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:freeopenrouter-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 增长:长期运行会增长,可定期清理或设置日志保留策略。

以文字记录成长,以良知安顿内心

© 2026 知行手记