# 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。 ### 异步日志持久化 模型请求完成后将元数据交给后台写入队列,不再等待该条日志的数据库提交。队列最多 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;必要时通过网络出口规则禁止访问内网和云元数据地址。 - 上游密钥保存在数据库中。配置文件、数据卷和备份应限制访问权限;备份应加密,不要将开发输出或真实配置上传到公开仓库。