Files
ai-router/README.md
T
m1saka 578afe509c feat: add permanent deletion for revoked keys
DELETE /admin/api/keys/{id}/purge removes the key record; it requires the key to be revoked first and keeps historical logs. The keys page swaps the revoke button for a delete button once a key is revoked.
2026-10-25 18:40:32 +08:00

180 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AI Router
个人使用的 Go AI API 网关,React 管理台内嵌在单个二进制中,数据保存在 SQLite。
## 构建与启动
需要 Go 1.26+、Node.js 22+ 和 npm(Node.js 仅用于构建前端)。
```sh
make build
cp config.example.yaml config.yaml
chmod 600 config.yaml
# 编辑 config.yaml,替换 admin.password(至少 12 个字符)
./ai-router -config config.yaml
```
打开 http://127.0.0.1:8080,使用配置中的密码登录,无需用户名。示例密码不能用于启动。默认监听本机;远程使用应配置 HTTPS 反向代理,禁止代理缓冲 SSE。
HTTPS 反向代理部署时,在配置中设置 `server.public_origin: "https://router.example.com"`(仅 origin,不含路径或末尾斜线)。这用于管理请求的同源检查并启用 Secure Cookie,比较不区分大小写;不会盲目信任客户端传入的转发头。
数据库路径相对于进程工作目录。备份时建议停止服务后复制整个 data 目录(包含可能存在的 WAL 文件)。上游凭据保存在数据库中,务必保护数据库及备份;不要提交 config.yaml 到 Git。下游 API Key 仅保存哈希,创建时完整显示一次。
## Docker Compose 部署
需要 Docker Engine 和 Docker Compose v2。Compose 使用现有多阶段 Dockerfile
构建前后端,容器以 UID/GID 10001 运行,SQLite 保存在宿主机 ./data 目录中。
首次部署时创建配置(如果已有 config.yaml,请备份并编辑,不要覆盖):
```sh
cp config.docker.example.yaml config.yaml
openssl rand -hex 32
# 将生成的随机值填入 config.yaml 的 admin.password
sudo chown 10001:10001 config.yaml
sudo chmod 600 config.yaml
sudo mkdir -p data
sudo chown 10001:10001 data
docker compose config --quiet
docker compose up -d --build
docker compose ps
```
配置只读挂载到容器,不会打入镜像。容器进程必须有权限读取该文件;
上面的权限命令适用于普通 Linux Docker Engine,rootless/user namespace 环境
需按实际 UID 映射设置。不要将真实密码提交到版本库。
默认访问 http://127.0.0.1:8080。容器内监听必须是 0.0.0.0:8080,
不能沿用本地运行示例的 127.0.0.1。宿主机端口已被占用时,例如已有本地服务:
```sh
AI_ROUTER_PORT=8081 docker compose up -d --build
```
后续 Compose 命令也应使用相同的 AI_ROUTER_PORT 值,或将它写入本地 .env。
默认仅发布到宿主机回环地址,不直接暴露公网。远程访问建议在宿主机配置
HTTPS 反向代理,同时设置 server.public_origin 并禁用 SSE 代理缓冲。
如果反向代理也在容器中,应加入同一 Docker 网络,连接 ai-router:8080。
常用命令:
```sh
docker compose logs -f --tail=100
docker compose up -d --build # 更新源码后重新构建和部署
docker compose restart # 修改配置文件后重启
docker compose down # 停止并删除容器,保留宿主机 data 目录
```
目录挂载的数据不会被 docker compose down -v 删除,仍需自行备份。
不会自动导入旧命名卷 router-data 中的数据。
迁移时先停止旧服务,备份原数据库目录(含可能存在的 WAL/SHM 文件),
再将数据复制到 ./data 并确保 UID/GID 10001 可读写;不要让两套服务同时写同一数据库。
### 国内镜像源构建
默认无需指定参数,直接运行 `docker compose up -d --build` 即可自动回退:
- npm:优先官方源,安装失败后使用 npmmirror 重试一次。
- Go:依次尝试官方代理、goproxy.cn、直接获取;使用 `|` 分隔,任意代理错误均可回退。
- Alpine:优先官方源,安装失败后使用阿里云源重试一次。
这不是按地区或测速选择;npm / apk 的单次网络等待有限时,Go 使用自身网络超时。
若所有来源均失败,构建仍会报错,不跳过依赖。可通过构建参数固定依赖源(不再自动切换):
```sh
docker compose build \
--build-arg NPM_REGISTRY=https://registry.npmmirror.com \
--build-arg GOPROXY=https://goproxy.cn,direct \
--build-arg ALPINE_MIRROR=https://mirrors.aliyun.com/alpine
docker compose up -d
```
这些参数也适用于 docker build。Go 仍保留模块校验,不关闭 GOSUMDB。
镜像源可用性取决于网络环境,请只使用可信源。
FROM 基础镜像默认使用 1Panel 镜像源 docker.1panel.live/library/,
分别拉取 node:24-alpine、golang:1.26-alpine、alpine:3.23。
可通过 NODE_IMAGE、GO_IMAGE、ALPINE_IMAGE 构建参数覆盖完整镜像地址,
例如 `--build-arg NODE_IMAGE=node:24-alpine` 改回 Docker Hub 的 Node 镜像。
基础镜像拉取失败不会自动回退,需手动替换地址;依赖软件包仍使用上述自动回退策略。
## 渠道与模型映射
在管理台创建渠道,选择协议并填写 Base URL。OpenAI Chat/Responses 填 API 前缀,例如 https://api.openai.com/v1,程序分别追加 /chat/completions、/responses;Anthropic 填服务根地址,例如 https://api.anthropic.com(不带 /v1),程序追加 /v1/messages,获取模型追加 /v1/models。自定义路径前缀会保留。不要填写完整接口端点。不兼容 Anthropic 旧的带 /v1 配置,也不会自动去重或迁移;已有渠道需手动修改 Base URL。下游接口路径不变。
点击“获取模型列表”读取上游模型(新建渠道无需先保存,已有渠道密钥留空时使用原密钥),随后弹窗选择要加入列表的模型。支持搜索、多选、全选搜索结果和清空选择;已有模型标记为已加入,不会重复添加或覆盖展示名称。确认后只将所选模型加入编辑草稿,取消不改变原列表。可修改展示名称;上游不支持模型列表时,可手动添加。点击“保存渠道”后,勾选展示的模型才会保存并汇总到“模型”页面。展示名称即下面的模型别名,修改后下游调用名也随之改变。
管理台模型页汇总所有渠道已保存的展示模型,并标注渠道是否停用;下游 `/v1/models` 进一步按渠道启用状态及当前 Key 权限过滤。只允许调用已配置的模型,未映射模型不能调用;历史渠道的透传设置也不再生效。
下游模型格式为 `渠道名/模型别名`,按第一个 `/` 拆分。别名在渠道内查询一次,得到的真实模型名原样发给上游:
| 渠道 | 模型别名 | 上游真实模型 | 下游调用 |
| --- | --- | --- | --- |
| abc | gpt-4 | abc/gpt-4 | abc/gpt-4 |
| abc | gpt-5 | gpt-4 | abc/gpt-5 |
不会再次拼接渠道前缀,也不会递归路由。未配置的别名一律拒绝,不支持未映射模型透传。创建下游 API Key 时可授权全部渠道或选择渠道白名单。空白名单不允许任何渠道。
```sh
curl http://127.0.0.1:8080/v1/chat/completions \
-H "Authorization: Bearer YOUR_ROUTER_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"abc/gpt-5","messages":[{"role":"user","content":"hello"}],"stream":true}'
```
## API 与兼容范围
模型页面支持“手动测试”和“一键测试全部”:手动测试可填写最多 2000 字符的提示词,一键测试按顺序检查当前所有启用渠道的已展示模型,跳过停用渠道。每次发送一条限制输出 Token 的非流式请求,最长等待 30 秒,展示成功/失败、耗时及上游 HTTP 状态。支持停止测试,离开模型页也会停止后续请求;已发送的请求仍可能计费。
测试属于管理员诊断,使用已保存的渠道密钥和模型映射,不创建下游 Key,不计入正式调用日志及统计,也不验证某个下游 Key 的授权。结果仅保留在当前页面,不返回上游响应正文;成功表示接口返回有效协议响应,不代表回答质量、所有协议转换或流式能力均通过验证。测试可能产生上游 Token 费用,一键测试开始前会要求确认。
- POST /v1/chat/completions — OpenAI Chat Completions。
- POST /v1/responses — OpenAI Responses。
- POST /v1/messages — Anthropic Messages,支持 x-api-key。同协议透传时,客户端提供的 `anthropic-version`/`anthropic-beta` 只有通过校验(短且仅含字母数字与 `-`、`_`、`.`、`,`、空格的 token 列表)才会转发上游,其余一律丢弃并由服务端默认版本兜底。
- GET /v1/models — 当前 Key 有权限访问的已配置模型别名。
同协议保留原生字段;跨协议转换通用文本、图片、工具调用、工具结果及 SSE。Responses 输出中的 phase 元数据不会阻断转换;转换为 Chat/Claude 时省略 reasoning 项及相关推理事件,不将其混入回答正文,不伪造 Claude thinking 签名,正文、工具调用和用量继续正常转换。需要保留原生推理内容时应使用 Responses 接口。其它厂商独有的服务端工具、思考块、会话引用等不保证可跨协议转换,不支持的内容应明确报错。强制工具选择是否可用取决于上游支持,路由器不会静默降级为自动选择。编程代理建议优先选择其原生协议渠道。此项目的本地自动化测试不等于所有 Claude Code/Codex 版本的真实上游兼容认证;使用自己的渠道后仍需联调。
跨协议转为 Anthropic 时必须提供 token 上限(Chat 的 `max_tokens` / `max_completion_tokens`,或 Responses 的 `max_output_tokens`);不会猜测额度。详细限制见 [协议说明](internal/protocol/README.md)。不提供 Responses 的服务端会话存储、WebSocket 或 Anthropic token-counting 扩展端点。
## 按 Key 统计
统计包含请求数、成功/失败数、输入、输出、缓存读取、缓存写入 Token,支持按 Key 和时间范围筛选,以及按天、渠道/模型汇总。调用记录保存请求模型与真实模型,不保存提示词、响应正文或工具参数。
输入 Token 统一为包含缓存的总输入,缓存读取/写入为细分项,不能再重复相加。OpenAI 的缓存通常包含在输入内;Anthropic 普通输入与缓存字段相加得到总输入。缺失 usage 标记未知;流中断时已收到的用量可能不完整。撤销或删除 Key 都不删除历史记录。
后端统一保存 UTC 时间。页面记录、Key 创建/到期时间按浏览器本地时区展示;时间筛选、今天/近 7/30 天和有效期输入按浏览器本地时间解释,并转换为 UTC 提交。无需时区配置。每日用量也按浏览器时区的自然日汇总:前端自动传递 IANA 时区名称,后端仅在查询时调整分组边界,支持夏令时且不改变 UTC 存储。统计 API 的 `timezone` 参数省略时默认 UTC,非法时区返回 400。
汇总仅累计完整已知用量,未知/中断请求另行计数。统计由数据库聚合,不再将全部原始日志载入应用内存;查询有超时及结果规模限制,大量历史数据建议按时间范围筛选。可通过 `storage.log_retention_days` 显式开启日志清理,默认关闭。Key 创建后支持停用与撤销;已撤销的 Key 可进一步删除(永久删除 Key 记录,历史日志保留),修改授权需重新创建 Key。
### 异步日志持久化
模型请求完成后将元数据交给后台写入队列,不再等待该条日志的数据库提交。队列最多 1024 条,后台每批最多 100 条,或每 100ms 提交一次事务。统计页面采用最终一致性,刚完成的请求可能稍后才出现;数据库繁忙时延迟可能超过 100ms。队列满时请求处理会等待可用位置,不静默丢弃日志。
正常退出会先等待在途请求结束,再排空日志队列后关闭数据库。写入失败进行有限重试,并输出不含请求内容的错误提示;持续数据库故障仍可能导致日志丢失,不能保证恰好一次持久化。进程崩溃、SIGKILL 或断电时,队列及尚未提交批次可能丢失。此队列是内存缓冲,不是持久化消息队列。
## 开发与验证
```sh
make test
# 前端开发服务器(后端须先启动)
cd web && npm run dev
```
管理写接口要求同源请求和 `X-Requested-With: ai-router` 请求头;不要对管理接口开放宽泛 CORS。生产构建使用内嵌前端,不需要单独启动 Vite。
## 生产安全配置
- 管理台建议仅通过 VPN 或 IP 白名单访问。公网入口必须使用 HTTPS,设置正确的 `server.public_origin`,并保持后端端口不直接对公网开放。
- `server.trusted_proxies` 默认是空列表,不信任任何客户端转发头。使用反向代理时,填写**应用实际看到的代理 IP/CIDR**,例如宿主机本地代理可用 `["127.0.0.1/32", "::1/128"]`;Docker 场景须按实际网络填写,不能照抄。代理必须覆盖或正确追加 `X-Forwarded-For`。应用从右向左剥离可信代理,按首个非可信地址执行登录限流;未配置时仍按直接连接地址限流,多个用户可能共享额度。不要配置 `0.0.0.0/0` 或 `::/0`。
- `limits.requests_per_minute`、`limits.per_key_concurrency`、`limits.global_concurrency` 分别控制每个 Key 的请求速率、并发数和全部 Key 的总并发,默认是 120、8、64,必须为正整数。超过限制返回 `429` 和 `Retry-After`。这些限制保存在单进程内存中,重启会重置,多副本需要网关侧统一限流。
- 登录限速在**校验密码之前**执行:同一 IP 每分钟 10 次失败、或全部 IP 合计每分钟 300 次失败后,锁定期间**一切登录尝试(包括正确密码)统一返回 429** 且不做密码比对——响应不泄露候选密码的正确性,爆破速率被硬性封顶(每 IP 10 次/分、全局 300 次/分)。代价是攻击进行中管理员也可能收到 429:已有会话(24 小时有效)与数据面不受影响,稍候重试或换网络即可,这也是建议管理面仅经 VPN 或 IP 白名单访问的原因之一。成功登录不消耗失败配额,并会清空本 IP 的失败计数,自己的手误不会累积锁定。
- `/v1` 数据面在鉴权**之前**按 IP 对鉴权失败限速:同一 IP 每分钟 60 次「无效 Key」之后,该 IP 的后续请求在查询数据库前即返回 429,用于保护单一 SQLite 连接不被无效 Key 洪泛挤占。注意该门限发生在 Key 校验之前,因此与管理员不同出口共享 IP 的合法 Key 也可能被连坐至多一分钟。
- 请求体读取限时 30 秒;登录请求体上限 16 KiB,模型请求体上限 16 MiB(超限返回 413,无法读取返回 400)。上游模型请求最长 10 分钟。反向代理也应设置请求体读取超时、连接数和请求大小限制,同时关闭 SSE 响应缓冲。
- 请求与响应的 JSON 解析有节点数上限(262144 个 token):由海量微型节点构成的敌意 JSON 会在解码前被拒绝,防止内存放大耗尽进程;正常的对话请求与响应远低于该上限。
- SSE 单事件最多 16 MiB,跨协议累计保留内容最多 16 MiB、块/索引最多 4096 个,累计 usage 最多 16 KiB;同协议透传模式另有整条流 64 MiB 总量上限,超限都会中断流。所有下游响应的单次写入/刷新限时 30 秒,防止停止读取的客户端长期占用并发槽。上游请求不发送 `Accept-Encoding: gzip`(禁用透明解压),避免被控上游以压缩流消耗代理 CPU。
- 所有响应默认携带 `Content-Security-Policy`(`default-src 'none'` + 按需放开 connect/script/style,管理台脚本全部同源、无内联 script)、`X-Frame-Options: DENY`、`Referrer-Policy: no-referrer` 与 `X-Content-Type-Options: nosniff`;反代不应移除这些头。
- `storage.log_retention_days` 默认 0,即不自动删除历史记录。需要自动清理时先备份,再设置正数(例如 90);**启用后会分批删除早于保留期限的日志,历史统计也会相应减少**。删除并不保证 SQLite 文件立即缩小,仍需监控数据卷空间。
- 请求限流不是费用硬额度。仍需在上游供应商设置预算、费用告警和密钥权限,并在密钥泄露时及时撤销。上游渠道只配置可信目标,公网目标使用 HTTPS;必要时通过网络出口规则禁止访问内网和云元数据地址。
- 上游密钥保存在数据库中。配置文件、数据卷和备份应限制访问权限;备份应加密,不要将开发输出或真实配置上传到公开仓库。