内容
# API 密钥轮换器
多提供商 API 密钥轮换反向代理。支持 **Firecrawl** 和 **Tavily** 配置文件 — 每个配置文件都有自己的密钥池、上游路由前缀和轮换策略。
## 为什么
`firecrawl-mcp` 将所有上游调用转发到 `FIRECRAWL_API_URL`。指向此代理可添加密钥轮换,且无需对 `firecrawl-mcp` 进行任何更改 — 只需运行原版的 `npx -y firecrawl-mcp`。
Tavily 集成通过在 OpenWebUI 的源代码中替换 Tavily API URL 实现,使 Tavily 请求通过代理,并从单独的密钥池中选取密钥(参见下文 "OpenWebUI + Tavily")。
## 运行
```bash
docker compose up -d
```
使用 `docker-compose.yml`:
```yaml
api-key-rotator:
build: .
environment:
FIRECRAWL_API_KEYS: "fc-key1,fc-key2,fc-key3"
UPSTREAM: "https://api.firecrawl.dev"
PORT: "8788"
MAX_PASSES: "2"
firecrawl: # 现有的 mcpo + firecrawl-mcp 服务
environment:
FIRECRAWL_API_URL: "http://api-key-rotator:8788"
# FIRECRAWL_API_KEY 已移除 - 轮换器注入它
```
## 配置(环境变量)
| 变量 | 默认值 | 目的 |
| --- | --- | --- |
| `FIRECRAWL_API_KEYS` | (必填) | 以逗号分隔的密钥池。 |
| `UPSTREAM` | `https://api.firecrawl.dev` | 上游 Firecrawl API 基址。 |
| `UPSTREAM_PROXY` | (未设置) | 用于出口的显式转发代理(`http`/`https`/`socks5`/`socks5h`)。使用 `socks5h` 时,DNS 通过 SOCKS5 服务器解析(与 `socks5` 相同;`h` 形式用于 curl/SearXNG 样式的配置)。胜过系统变量。 |
| `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` | (未设置) | 系统/curl 样式的代理环境变量,在 `UPSTREAM_PROXY` 未设置时生效。 |
| `PORT` | `8788` | 监听端口。 |
| `HOST` | `0.0.0.0` | 监听地址。 |
| `MAX_PASSES` | `2` | 密钥池遍历次数上限,超过后放弃。 |
| `MAX_BODY_BYTES` | `16777216`(16 MiB) | 缓冲响应正文的大小上限。超过后,原样转发。`0` = 无上限。 |
| `PROXY_BASE_URL` | (来自 `Host` 头部) | 重写 `next` URL 时使用的基准。 |
| `CREDIT_RESET_DAY` | `1` | 密钥计费重置的回退日期(1-31,UTC),当无法自动检测密钥的计费重置时使用。参见下文 "信用禁用"。 |
| `LOW_CREDIT_THRESHOLD` | `10` | 当预测的 `remainingCredits` 下降到/低于此阈值时,切换密钥(旋转到下一个)。 |
| `STOP_CREDIT_THRESHOLD` | `2` | 当所有密钥均低于此阈值时停止接受请求。必须 <= `LOW_CREDIT_THRESHOLD`。 |
| `CREDIT_REFRESH_INTERVAL` | `300` | 低余额密钥的信用刷新间隔(秒)。参见 "信用感知选择"。 |
| `TAVILY_API_KEYS` | (未设置) | 以逗号分隔的 Tavily 密钥池。设置后启用 Tavily 配置文件。 |
| `TAVILY_UPSTREAM` | `https://api.tavily.com` | Tavily 配置文件的上游 Tavily API 基址。 |
| `TAVILY_ROUTE_PREFIX` | `/tavily` | 选择 Tavily 配置文件的路由前缀。转发前会剥离。 |
| `TAVILY_LOW_CREDIT_THRESHOLD` | `10` | 与 `LOW_CREDIT_THRESHOLD` 类似,但适用于 Tavily 密钥池。 |
| `TAVILY_STOP_CREDIT_THRESHOLD` | `2` | 与 `STOP_CREDIT_THRESHOLD` 类似,但适用于 Tavily 密钥池。 |
| `LOG_LEVEL` | `info` | `debug` 添加每个请求的日志行。 |
## 端点
- `GET /healthz` -> `200 {"ok":true}` 如果至少有一个密钥可用,否则 `503`。Docker 健康检查目标。
- `GET /status` -> 密钥池大小、当前索引、每个密钥的统计信息、禁用状态和 `remainingCredits`(密钥掩码到最后 4 位字符;`-1` = 未测量)。
## 信用感知密钥选择
轮换器选择具有 **最高剩余信用** 且高于停止阈值的密钥,以便流量集中在最健康的帐户上。它跟踪每个密钥的 `remainingCredits`,从 `GET /v2/team/credit-usage`(只读,不消耗信用):
- **启动**: 在后台获取每个密钥的实际余额(服务器立即启动;在此完成之前,未测量的密钥被视为充足)。
- **成功**: 根据响应中的 `creditsUsed`(或如果该字段不存在,则减少 1)减少已使用密钥的预测余额。
- **轮换**: 刷新我们切换的密钥。
- **低余额**: 当密钥的预测余额低于 100 时,它最多每 `CREDIT_REFRESH_INTERVAL`(默认 5 分钟)刷新一次,以纠正估计偏差。
- **每日**: 每个密钥每天刷新一次,作为万能捕获。
当当前密钥的预测余额达到 `LOW_CREDIT_THRESHOLD`(默认 10)时,它将被轮换(冷却约 30 秒)并由下一个最富有的密钥接管。当 **所有** 密钥均低于 `STOP_CREDIT_THRESHOLD`(默认 2)时,`/healthz` 返回 `503`,新请求返回 `503 {"success":false,"error":"所有密钥信用耗尽,直到计费重置"}`。
## 轮换和重试行为
两种类型的故障以不同方式处理:
**密钥级拒绝 -> 轮换到另一个密钥:** HTTP **402**(信用),**429**(速率限制),**401**(密钥错误),以及失败信封(`{"success":false,...}`),其文本匹配 `insufficient credits`,`rate limit`,`exceeded`,`payment required`,`unauthorized`,`forbidden`。密钥被冷却约 30 秒(或如果信用耗尽 - 参见下文),然后尝试下一个密钥,最多遍历 `MAX_PASSES` 次。
**暂时性错误 -> 同一密钥重试:** HTTP **403**(边缘/WAF),**408**,**5xx**,以及网络错误将在同一密钥上重试,指数回退 `500ms -> 1s -> 2s -> 4s -> 8s`(5 次尝试,总计约 15 秒),然后轮换。403 通常是网络/边缘层问题,而不是密钥特定问题,因此它不会立即禁用或轮换。
**成功** 响应(`status < 400` 且 `success:true`,或无 `success` 字段)**永远不会** 轮换 - 即使抓取的内容提到了“速率限制”或“付款要求”。仅检查 Firecrawl 失败信封的拒绝列表,不检查整个响应正文。
- `next` 字段的绝对上游 URL 被重写为代理,以便爬虫分页在轮换下继续。响应正文中其他出现的域名 **永远不会** 被重写(它们可能是实际抓取的内容)。
## 信用禁用
一个返回 **真正信用耗尽** 信号的密钥(HTTP 402,或失败信封提到了 `insufficient credits` / `payment required` / `exceeded`)将被 **禁用**,并在后续所有请求中跳过,直到其信用重置 - 它不会在每次遍历中重试(这将浪费上游调用并冒账户标记风险)。
- 重置时间 **按密钥** 从该密钥自己的 `GET /v2/team/credit-usage` -> `billingPeriodEnd` 读取(一个只读端点,不消耗信用)。这很重要,因为每个密钥属于一个单独的帐户,并在 **该帐户的计费周年** 重置,这通常是每个密钥不同的日期 - 而不是通用日期。
- 如果信用使用调用失败,则密钥被禁用,直到下一个 `CREDIT_RESET_DAY`(UTC)作为回退。
- **429(速率限制)** 和 **401(身份验证)** 轮换但不禁用 - 它们是暂时性或全局帐户问题,禁用它们会使一个好密钥离线。**403** 重试并带有回退,永远不会禁用。
- 背景循环在每个密钥的重置时间重新启用每个密钥;重启容器也会清除所有禁用。
## Tavily 配置文件
当设置 `TAVILY_API_KEYS` 时,代理为 Tavily 创建单独的密钥池。路径以 `TAVILY_ROUTE_PREFIX`(默认 `/tavily`)开头的请求被路由到 Tavily 池:
- **前缀剥离**: 前导 `/tavily` 在转发之前被移除。请求 `/tavily/search` 访问 `{TAVILY_UPSTREAM}/search`。
- **轮换策略**: HTTP **401** 和 **429** 轮换密钥(冷却约 30 秒)但不禁用。HTTP **432** 和 **433** 禁用密钥,直到其信用重置(与 Firecrawl 相同的重置机制 - 按密钥 `/api/usage` 或回退 `CREDIT_RESET_DAY`)。
- **基于正文的拒绝检测**: Tavily 拒绝纯粹通过状态码检测,从不扫描响应正文文本。Firecrawl 拒绝列表从不适用于 Tavily 响应。
- **使用跟踪**: 代理调用 `GET /usage` 上游 Tavily(按密钥)读取 `key.usage` / `key.limit`,`account.plan_usage` / `plan_limit` 和 `account.paygo_usage` / `paygo_limit`。有效剩余信用 = 层级的最小值(限制 - 使用量),跳过任何限制 <= 0(无限/未测量)。如果所有层级都是无限的,则密钥保持未测量。Tavily 不返回计费周期结束,因此禁用的密钥在 `CREDIT_RESET_DAY` 回退时重新启用,与 Firecrawl 回退路径相同。
- **信用阈值**: `TAVILY_LOW_CREDIT_THRESHOLD` 和 `TAVILY_STOP_CREDIT_THRESHOLD` 镜像 Firecrawl 阈值,但独立应用于 Tavily 密钥池。
## OpenWebUI + Tavily
要通过代理路由 OpenWebUI 的 Tavily 搜索,在容器启动时使用 `sed` 重写 Tavily API URL:
```yaml
services:
open-webui:
image: ghcr.io/open-webui/open-webui:main
# ... 现有配置 ...
command: >
bash -c "
sed -i \"s|https://api.tavily.com|http://api-key-rotator:8788/tavily|g\"
/app/backend/open_webui/retrieval/web/tavily.py
/app/backend/open-webui/retrieval/loaders/tavily.py
&& bash start.sh"
```
`/tavily` 前缀被代理剥离后转发到 `https://api.tavily.com`,因此不需要其他更改。
## 迁移说明
该项目最初名为 `firecrawl-rotator`。重命名为 `api-key-rotator` 影响:
| 项目 | 旧 | 新 |
| --- | --- | --- |
| Docker 镜像 | `ghcr.io/<you>/firecrawl-rotator` | `ghcr.io/<you>/api-key-rotator` |
| Compose 服务名称 | `firecrawl-rotator` | `api-key-rotator` |
| `FIRECRAWL_API_URL` 主机 | `http://firecrawl-rotator:8788` | `http://api-key-rotator:8788` |
| 二进制/健康检查 | `/rotator -healthcheck` | `/api-key-rotator -healthcheck` |
相应更新您的 `docker-compose.yml` 服务名称、镜像标签、健康检查路径和 `depends_on` / `FIRECRAWL_API_URL` 引用。
## 开发
```bash
go test ./...
go build -o api-key-rotator .
FIRECRAWL_API_KEYS=fc-x ./api-key-rotator
```
参见 `docs/superpowers/specs/2026-07-09-firecrawl-token-rotation-design.md` 获取完整设计。
连接信息
猜你喜欢
暂无推荐服务器