|
1 | 1 | # Auto Router |
2 | 2 |
|
3 | | -按请求特征、模型能力与策略自动选择模型与 Provider 的独立 LLM Router。 |
| 3 | +单用户 LLM 网关:管理上游提供商与模型,把推理请求转发到已选择的模型,并为每个推理密钥提供鉴权。 |
4 | 4 |
|
5 | | -**当前状态:阶段 1(基础架构)与阶段 2(Model Registry)已完成。** 模型 API(`/v1/chat/completions`、`/v1/responses`)和后台路径当前返回 `404`,不提供会误导客户端的假成功响应;Bifrost 代理、Jev、Policy 与管理后台尚未实现。 |
| 5 | +## 启动 |
6 | 6 |
|
7 | | -架构边界见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md),分阶段交付与验收门槛见 [docs/ROADMAP.md](docs/ROADMAP.md)。 |
8 | | - |
9 | | -## 快速开始 |
10 | | - |
11 | | -```bash |
12 | | -go build -o bin/auto-router ./cmd/auto-router |
13 | | -./bin/auto-router -config configs/example.json |
14 | | -``` |
15 | | - |
16 | | -不带 `-config` 时使用内置默认值。默认监听 `127.0.0.1:8080`,数据库位于 `data/auto-router.db`。 |
17 | | - |
18 | | -检查服务: |
19 | | - |
20 | | -```bash |
21 | | -curl -i http://127.0.0.1:8080/healthz # 进程存活 |
22 | | -curl -i http://127.0.0.1:8080/readyz # 配置与数据库可用 |
| 7 | +```sh |
| 8 | +go run ./cmd/auto-router |
23 | 9 | ``` |
24 | 10 |
|
25 | | -## 模型注册表 |
| 11 | +默认监听 `127.0.0.1:8080`,数据库为当前工作目录下的 `data/auto-router.db`。新建数据库使用 schema v8。 |
26 | 12 |
|
27 | | -注册表由三部分组成: |
| 13 | +如需在本机并行运行另一实例,可用 `-listen 127.0.0.1:8081` 指定监听地址。 |
28 | 14 |
|
29 | | -1. **models.dev 同步**(可选、显式触发):把白名单里的 `provider/model` 元数据与能力写入 SQLite; |
30 | | -2. **本地覆盖**(配置文件):提供 `base_url`、`api_key`、启停、优先级与上游模型标识; |
31 | | -3. **SQLite**:运行时唯一真相,路由只读不可变快照。 |
| 15 | +### Docker Compose(GHCR) |
32 | 16 |
|
33 | | -第一次使用: |
34 | | - |
35 | | -```bash |
36 | | -cp configs/registry.example.json config.local.json # 可用 ${VAR} 引用环境变量 |
37 | | -export OPENAI_API_KEY=sk-... |
38 | | -./bin/auto-router -config config.local.json -sync-models # 同步后退出,不启动 HTTP |
39 | | -./bin/auto-router -config config.local.json # 正常启动 |
| 17 | +```sh |
| 18 | +cp .env.example .env |
| 19 | +docker compose pull |
| 20 | +docker compose up -d |
| 21 | +docker compose logs -f auto-router |
40 | 22 | ``` |
41 | 23 |
|
42 | | -关键语义: |
43 | | - |
44 | | -- 同步只写元数据(`display_name`、能力、`upstream_model_id`、上下文/输出容量),**永不改写** `base_url`、`api_key`、`enabled`、`priority` 与本地条目;新建的同步 Provider 默认 `enabled=false` 且没有凭据,因此配置之前不可能被转发。 |
45 | | -- 白名单条目在源数据中找不到会让**整次同步失败并回滚**;“新增模型只改配置”指加一条白名单后重跑 `-sync-models`,零代码改动。 |
46 | | -- 从白名单或配置中移除的条目只置 `enabled=0`,永不删除,避免残留凭据被继续使用。 |
47 | | -- `registry.sync.include` 为空时 `-sync-models` 直接失败,不会把空目录当作成功。 |
48 | | -- 启动与请求路径不做任何网络访问,也没有后台同步;目录为空时只记录 `WARN`,服务照常提供(`/readyz` 只取决于数据库可达)。 |
49 | | -- 阶段 2 **不存价格**:models.dev 的 `cost` 等字段同步时丢弃,表结构中没有价格/成本列,Dashboard 成本与 Policy 成本输入已从 V1 移除(见 [ROADMAP 需求偏差](docs/ROADMAP.md))。 |
| 24 | +打开 <http://127.0.0.1:8080/admin/> 完成首次管理员密码设置。Compose 默认只将端口绑定到本机 loopback;需要从其他机器访问时,应先配置 TLS 反向代理,再调整 `.env` 中的 `AUTO_ROUTER_BIND`。数据保存在 `auto-router-data` 命名卷中;升级时运行 `docker compose pull && docker compose up -d`。不要用 `docker compose down -v`,否则会删除数据库卷。 |
50 | 25 |
|
51 | | -## 配置 |
| 26 | +镜像为 `ghcr.io/nextroad-dev/auto-router`,支持 `linux/amd64` 与 `linux/arm64`。`.env` 中的 `AUTO_ROUTER_VERSION` 可设为 `latest` 或具体版本(例如 `1.2.3`)。首次发布后,仓库维护者需在 GitHub Packages 将该镜像包可见性设为 **Public**,用户才能免登录拉取。 |
52 | 27 |
|
53 | | -加载顺序为:内置默认值 → JSON 文件 → 环境变量(环境变量优先)。未知 JSON 字段、非法值、空值环境变量都会让进程在监听前失败,而不是静默使用默认值。`configs/example.json` 与内置默认值保持一致;`configs/registry.example.json` 给出注册表段的完整示例(可直接作为 `-config` 使用)。 |
| 28 | +GitHub Actions 会在 PR 上构建验证、在 `main` 发布 `edge`,并在推送 `v*` 标签时发布版本标签和 `latest`: |
54 | 29 |
|
55 | | -标量字段: |
56 | | - |
57 | | -| JSON 字段 | 环境变量 | 默认值 | |
58 | | -| --- | --- | --- | |
59 | | -| `http.address` | `AUTO_ROUTER_HTTP_ADDRESS` | `127.0.0.1:8080` | |
60 | | -| `http.read_header_timeout` | `AUTO_ROUTER_READ_HEADER_TIMEOUT` | `5s` | |
61 | | -| `http.idle_timeout` | `AUTO_ROUTER_IDLE_TIMEOUT` | `60s` | |
62 | | -| `http.readiness_timeout` | `AUTO_ROUTER_READINESS_TIMEOUT` | `2s` | |
63 | | -| `http.shutdown_timeout` | `AUTO_ROUTER_SHUTDOWN_TIMEOUT` | `10s` | |
64 | | -| `database.path` | `AUTO_ROUTER_DATABASE_PATH` | `data/auto-router.db` | |
65 | | -| `database.busy_timeout` | `AUTO_ROUTER_DATABASE_BUSY_TIMEOUT` | `5s` | |
66 | | -| `log.level` | `AUTO_ROUTER_LOG_LEVEL` | `info` | |
| 30 | +```sh |
| 31 | +git tag v1.2.3 |
| 32 | +git push origin v1.2.3 |
| 33 | +``` |
67 | 34 |
|
68 | | -注册表字段(**只能来自配置文件**,环境变量不覆盖列表): |
| 35 | +首次启动时按管理页面提示设置一个 owner 密码。首次设置由 `GET /admin/v1/setup/status` 检查状态,并由 `POST /admin/v1/setup/password` 提交。密码至少 12 字节,可在系统设置中修改。 |
69 | 36 |
|
70 | | -| JSON 字段 | 默认值 | 说明 | |
71 | | -| --- | --- | --- | |
72 | | -| `registry.sync.url` | `https://models.dev/api.json` | 必须是 `https`;仅回环地址允许 `http`(测试用) | |
73 | | -| `registry.sync.timeout` | `30s` | 单次抓取超时;响应体上限 32 MiB | |
74 | | -| `registry.sync.include` | `[]` | 白名单,格式 `provider/model`,以第一个 `/` 分隔 | |
75 | | -| `registry.providers[]` | `[]` | `key`、`display_name`、`base_url`、`api_key`、`enabled`、`priority` | |
76 | | -| `registry.models[]` | `[]` | `provider`、`model`、`upstream_model_id`、`display_name`、`context_window`、`max_output`、`supports_tools`、`supports_vision`、`supports_reasoning`、`enabled`、`priority` | |
| 37 | +低于 v8 的旧数据库不会自动迁移至当前版本,也不会被自动删除。需要使用全新数据库时,请先停止服务,并自行备份或移走旧数据库文件,再启动服务。 |
77 | 38 |
|
78 | | -约定: |
| 39 | +忘记密码时,停止服务并通过环境变量提供新密码后运行离线恢复命令: |
79 | 40 |
|
80 | | -- `api_key` 支持 `${VAR}` 展开;变量未设置会让进程启动失败并只报变量名,不打印值。`$${` 转义为字面量 `${`。 |
81 | | -- `registry.models[].provider` 必须在本文件的 `registry.providers[]` 中声明;写成未声明的 Provider 直接报错(几乎都是拼写错误,且这样的 pair 永远无法启用)。 |
82 | | -- `enabled` 省略即 `false`(fail-closed);`priority` 数值越小越优先;`upstream_model_id` 省略时默认等于 `model`。 |
83 | | -- 标识约束:Provider key 为 `^[a-z0-9][a-z0-9._-]{0,63}$`;模型 ID 允许字母数字与 `._:/@~+-`(真实 models.dev 中存在 `meta-llama/Llama-3.3-70B`、`claude-sonnet-4@20250514` 这类 ID),最长 128 字符。 |
84 | | -- 容量约束:`context_window > 0`;`max_output` 省略或落在 `(0, context_window]`;违反约束的配置在启动前报错。 |
| 41 | +```sh |
| 42 | +AUTO_ROUTER_RECOVERY_PASSWORD='your-new-password' go run ./cmd/auto-router -recover-admin |
| 43 | +``` |
85 | 44 |
|
86 | | -## 命令与端点 |
| 45 | +PowerShell: |
87 | 46 |
|
88 | | -| 命令 / 端点 | 作用 | |
89 | | -| --- | --- | |
90 | | -| `-config <file>` | 指定 JSON 配置文件 | |
91 | | -| `-sync-models` | 从 models.dev 同步白名单后退出(不启动 HTTP) | |
92 | | -| `GET` / `HEAD` `/healthz` | 存活检查,仅表示进程在运行 | |
93 | | -| `GET` / `HEAD` `/readyz` | 就绪检查,会执行一次带超时的数据库探测 | |
94 | | - |
95 | | -就绪失败只返回 `{"status":"not_ready"}`,不暴露数据库路径、驱动错误或配置内容;响应带 `Cache-Control: no-store`。 |
96 | | - |
97 | | -## 安全 |
98 | | - |
99 | | -- 默认只监听回环地址。**当前没有身份认证**,不要直接暴露到公网或不受信任网络。 |
100 | | -- Provider 的 `api_key` **明文存储**在 SQLite 中。缓解措施:POSIX 下数据库与 `-wal`/`-shm` 为 `0600`、日志与错误信息一律掩码(`sk-…abcd`)、支持 `${VAR}` 避免明文入库版本、`.gitignore` 覆盖 `*.db*`。 |
101 | | -- **残余风险(未假装已解决)**:磁盘或备份可读、Windows ACL 未加固、无静态加密、无密钥轮换。静态加密与 Windows ACL 列为显式推迟项。 |
102 | | -- 普通运行日志不记录请求体、prompt 或凭据。 |
103 | | - |
104 | | -## 目录结构 |
105 | | - |
106 | | -```text |
107 | | -cmd/auto-router/ 进程入口、装配、生命周期、-sync-models |
108 | | -internal/api/ HTTP 边界与探针 |
109 | | -internal/config/ 启动配置、默认值、环境覆盖、registry 段与校验 |
110 | | -internal/models/ 注册表领域类型、校验、排序契约、只读快照 |
111 | | -internal/modelsdev/ models.dev 抓取、白名单映射与真实 fixture |
112 | | -internal/storage/ SQLite 连接、事务迁移、注册表持久化 |
113 | | -configs/ 示例配置 |
114 | | -docs/ 架构边界与阶段计划 |
115 | | -docs/tasks/ 各阶段的交付与验收记录 |
| 47 | +```powershell |
| 48 | +$env:AUTO_ROUTER_RECOVERY_PASSWORD = "你的新密码" |
| 49 | +go run ./cmd/auto-router -recover-admin |
| 50 | +Remove-Item Env:AUTO_ROUTER_RECOVERY_PASSWORD |
116 | 51 | ``` |
117 | 52 |
|
118 | | -## 开发 |
| 53 | +恢复命令要求数据库已存在且为受支持版本,并取得服务监听端口;成功后新密码立即生效。该环境变量只用于本次恢复操作。 |
119 | 54 |
|
120 | | -```bash |
121 | | -gofmt -l cmd internal # 应无输出 |
122 | | -go vet ./... |
123 | | -go test -count=1 ./... |
124 | | -go build ./cmd/auto-router |
125 | | -``` |
| 55 | +## 配置与使用 |
| 56 | + |
| 57 | +- **提供商**:在「提供商」中添加上游 URL、API 密钥与类型:`openai`、`openai_compatible`、`anthropic` 或 `gemini`。通过模型发现后选择要加入路由的模型。上游 API 密钥只写入,不会在读取响应中返回。 |
| 58 | +- **模型组**:在「模型组」中配置简单、中等、复杂三组。每组由已启用的提供商/模型组合组成,顺序就是 fallback 顺序;每组最多 8 项。 |
| 59 | +- **调用密钥**:在「调用密钥」创建具名 inference key。原始值只在创建或轮换时显示一次。客户端通过 `Authorization: Bearer <key>` 调用。 |
| 60 | +- **仪表盘**:展示请求成功率、时间窗口内各模型与模型组的实际上游尝试次数和 Token,以及最近 60 秒已完成尝试的输出 Token TPS。 |
126 | 61 |
|
127 | | -`go test -race` 需要 cgo 与 C 工具链;本机没有 C 工具链时该命令无法运行,`-race` 由 Linux CI 覆盖。 |
| 62 | +启用 Jev 后,请求内容会按默认的脱敏模式发送至 Jev 服务;选择 features-only 模式时不会发送对话文本。原始 Jev 请求与响应正文默认不捕获。 |
128 | 63 |
|
129 | | -测试不使用真实凭据或外网:使用临时 SQLite、`httptest` 与本地上游。`internal/modelsdev/testdata/api.json` 是从真实 `models.dev/api.json` 抓取并裁剪的固定 fixture(来源与裁剪方式见同目录 README),测试本身不访问网络。CI(`.github/workflows/ci.yml`)在 Linux 与 Windows 上运行 `go mod verify`、`go vet`、`go test` 与构建,并在 Linux 上额外运行 `-race`。 |
| 64 | +## API |
130 | 65 |
|
131 | | -已完成的验证: |
| 66 | +推理入口: |
132 | 67 |
|
133 | | -- 阶段 1:配置默认值、文件与环境变量优先级、非法/未知/空值拒绝;SQLite 持久化重开、含 `#` 与 `%` 的路径按字面量处理、WAL/foreign_keys/busy_timeout 在连接重建后仍生效、内存库彼此隔离;迁移幂等、失败批次整体回滚、历史校验和/名称不匹配与更新版本库被拒绝;探针语义、405/404 边界、就绪超时与取消传播;优雅排空在途请求、超时强制关闭、真实进程启动/探针/数据库落盘的正向冒烟。 |
134 | | -- 阶段 2:迁移 v1 建表幂等、外键与 CHECK 约束、事务失败整体回滚;同步幂等、白名单移除与本地条目移除只禁用不删除、本地覆盖不被同步改写;models.dev 映射(含共享模型、含 `/` 的模型 ID、Provider 前缀模型键、`max_output` 截断告警、缺上下文跳过、缺失白名单整体失败、`cost` 被丢弃);`httptest` 假源下的超时/4xx/5xx/畸形 JSON;真实进程 `-sync-models` → 启动 `/readyz` → 关闭 → 重开仍可加载;密钥不进日志与错误信息、POSIX 文件权限收紧。 |
| 68 | +| 方法与路径 | 协议 | |
| 69 | +| --- | --- | |
| 70 | +| `GET /v1/models` | 可路由逻辑模型列表 | |
| 71 | +| `POST /v1/chat/completions` | OpenAI Chat Completions | |
| 72 | +| `POST /v1/responses` | OpenAI Responses | |
| 73 | +| `POST /v1/messages` | Anthropic Messages | |
| 74 | +| `POST /v1beta/models/{model}:generateContent` | Gemini generateContent | |
| 75 | +| `POST /v1beta/models/{model}:streamGenerateContent` | Gemini 流式生成 | |
135 | 76 |
|
136 | | -平台限制:Windows 无法向当前进程投递 `os.Interrupt`,因此信号到优雅退出的真实链路测试在 Windows 上跳过,由 Linux CI 覆盖;优雅排空逻辑本身在两端都有测试。 |
| 77 | +Anthropic 与 Gemini 原生入口按对应协议直通,要求所选上游支持相同协议。OpenAI Chat 文本请求可以转换后转发至 Anthropic 或 Gemini,但仅支持有限的安全子集;不支持的参数或功能会在发送上游前被拒绝。 |
137 | 78 |
|
138 | | -## 后续阶段 |
| 79 | +管理端主要接口: |
139 | 80 |
|
140 | | -按 [docs/ROADMAP.md](docs/ROADMAP.md) 依次进行:Bifrost Proxy → Jev Client → Analyzer → Policy Engine → Auto Routing → Logging → Admin API → Dashboard。每个阶段单独实现并测试后再进入下一阶段。 |
| 81 | +| 方法与路径 | 用途 | |
| 82 | +| --- | --- | |
| 83 | +| `GET /admin/v1/setup/status` · `POST /admin/v1/setup/password` | 首次设置 owner 密码 | |
| 84 | +| `POST /admin/v1/session` · `GET/DELETE /admin/v1/session` | 密码登录、会话检查与退出 | |
| 85 | +| `PATCH /admin/v1/password` | 修改 owner 密码 | |
| 86 | +| `GET /admin/v1/providers` · `POST /admin/v1/providers` | 查看与新增提供商 | |
| 87 | +| `GET /admin/v1/providers/{key}/discover` | 从上游发现模型 | |
| 88 | +| `POST /admin/v1/providers/{key}/models` | 选择该提供商的模型 | |
| 89 | +| `GET/PUT /admin/v1/groups` | 读取或整体更新简单/中等/复杂模型组 | |
| 90 | +| `GET /admin/v1/dashboard?window=24h` | 仪表盘指标;窗口可选 `1h`、`24h`、`7d`、`30d` | |
| 91 | +| `GET /admin/v1/logs/stats?group_by=attempt_model` | 按实际模型汇总上游尝试;用 `attempt_group` 按模型组汇总 | |
| 92 | +| `GET/POST /admin/v1/keys` · `POST /admin/v1/keys/{name}/rotate` · `DELETE /admin/v1/keys/{name}` | 管理具名推理密钥 | |
| 93 | + |
| 94 | +管理 API 使用登录后签发的 `HttpOnly` 会话 Cookie;推理密钥只用于推理 API。完整请求、响应与错误结构见 [docs/openapi.yaml](docs/openapi.yaml)。 |
| 95 | + |
| 96 | +服务状态探针为 `GET /healthz` 和 `GET /readyz`。管理静态页面由服务自身提供。由于服务默认使用 HTTP 并绑定 loopback,若经反向代理对外提供访问,应由代理终止 TLS。 |
0 commit comments