Skip to content

Commit f82217f

Browse files
committed
feat: add admin console and GHCR Docker deployment
1 parent d93459d commit f82217f

192 files changed

Lines changed: 46686 additions & 4348 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.dockerignore‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.git
2+
.github
3+
.runtime-v8
4+
bin
5+
data
6+
internal/api/dashboard/static/web
7+
webui/node_modules
8+
webui/test-results
9+
webui/playwright-report
10+
webui/coverage
11+
.env
12+
.env.*
13+
!.env.example
14+
*.db
15+
*.db-*
16+
*.sqlite
17+
*.sqlite-*
18+
*.exe
19+
*.test
20+
*.iso
21+
*.iso.sha256

‎.env.example‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Published release tag, or latest.
2+
AUTO_ROUTER_VERSION=latest
3+
4+
# Keep the admin UI local by default. Put a TLS reverse proxy in front before
5+
# binding to 0.0.0.0 or exposing this port to an untrusted network.
6+
AUTO_ROUTER_BIND=127.0.0.1
7+
AUTO_ROUTER_PORT=8080

‎.github/workflows/ci.yml‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,28 @@ jobs:
2626
- run: go test -count=1 ./...
2727
- run: go build ./cmd/auto-router
2828

29+
frontend:
30+
runs-on: ubuntu-latest
31+
steps:
32+
- uses: actions/checkout@v4
33+
- uses: actions/setup-go@v5
34+
with:
35+
go-version-file: go.mod
36+
cache: true
37+
- uses: actions/setup-node@v4
38+
with:
39+
node-version: "22.12.0"
40+
cache: npm
41+
cache-dependency-path: webui/package-lock.json
42+
- run: npm ci
43+
working-directory: webui
44+
- run: npm run typecheck
45+
working-directory: webui
46+
- run: npm run build
47+
working-directory: webui
48+
- run: git diff --exit-code -- ../internal/api/dashboard/static/web
49+
working-directory: webui
50+
2951
race:
3052
runs-on: ubuntu-latest
3153
steps:
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: Docker image
2+
3+
on:
4+
push:
5+
branches: [main]
6+
tags: ['v*']
7+
pull_request:
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
build-and-publish:
15+
runs-on: ubuntu-latest
16+
permissions:
17+
contents: read
18+
packages: write
19+
steps:
20+
- name: Check out source
21+
uses: actions/checkout@v4
22+
23+
- name: Set up QEMU
24+
uses: docker/setup-qemu-action@v3
25+
26+
- name: Set up Docker Buildx
27+
uses: docker/setup-buildx-action@v3
28+
29+
- name: Generate image metadata
30+
id: meta
31+
uses: docker/metadata-action@v5
32+
with:
33+
images: ghcr.io/nextroad-dev/auto-router
34+
tags: |
35+
type=semver,pattern={{version}}
36+
type=semver,pattern={{major}}.{{minor}}
37+
type=semver,pattern={{major}}
38+
type=ref,event=tag
39+
type=ref,event=branch
40+
type=ref,event=pr
41+
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
42+
type=raw,value=edge,enable=${{ github.ref == 'refs/heads/main' }}
43+
44+
- name: Log in to GHCR
45+
if: github.event_name != 'pull_request'
46+
uses: docker/login-action@v3
47+
with:
48+
registry: ghcr.io
49+
username: ${{ github.actor }}
50+
password: ${{ secrets.GITHUB_TOKEN }}
51+
52+
- name: Build and publish multi-architecture image
53+
uses: docker/build-push-action@v6
54+
with:
55+
context: .
56+
platforms: linux/amd64,linux/arm64
57+
push: ${{ github.event_name != 'pull_request' }}
58+
tags: ${{ steps.meta.outputs.tags }}
59+
labels: ${{ steps.meta.outputs.labels }}
60+
build-args: |
61+
VERSION=${{ github.ref_name }}
62+
cache-from: type=gha
63+
cache-to: type=gha,mode=max
64+
sbom: true
65+
provenance: true

‎Dockerfile‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# syntax=docker/dockerfile:1.7
2+
3+
FROM node:22-alpine AS webui-build
4+
WORKDIR /src/webui
5+
COPY webui/package.json webui/package-lock.json ./
6+
RUN npm ci
7+
WORKDIR /src
8+
COPY docs/openapi.yaml ./docs/openapi.yaml
9+
COPY webui ./webui
10+
RUN cd webui && npm run api:types && npm run typecheck && npm run build
11+
12+
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS go-build
13+
ARG TARGETOS
14+
ARG TARGETARCH
15+
ARG VERSION=dev
16+
WORKDIR /src
17+
COPY go.mod go.sum ./
18+
RUN go mod download
19+
COPY . .
20+
COPY --from=webui-build /src/internal/api/dashboard/static/web ./internal/api/dashboard/static/web
21+
RUN mkdir -p /out/data \
22+
&& CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \
23+
go build -trimpath -ldflags="-s -w -X main.buildVersion=${VERSION}" \
24+
-o /out/auto-router ./cmd/auto-router \
25+
&& chown -R 65532:65532 /out/data
26+
27+
FROM gcr.io/distroless/static-debian12:nonroot
28+
WORKDIR /app
29+
COPY --from=go-build --chown=65532:65532 /out/auto-router /app/auto-router
30+
COPY --from=go-build --chown=65532:65532 /out/data /app/data
31+
USER 65532:65532
32+
EXPOSE 8080
33+
STOPSIGNAL SIGTERM
34+
ENTRYPOINT ["/app/auto-router", "-listen", "0.0.0.0:8080"]

‎README.md‎

Lines changed: 66 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,140 +1,96 @@
11
# Auto Router
22

3-
按请求特征、模型能力与策略自动选择模型与 Provider 的独立 LLM Router。
3+
单用户 LLM 网关:管理上游提供商与模型,把推理请求转发到已选择的模型,并为每个推理密钥提供鉴权。
44

5-
**当前状态:阶段 1(基础架构)与阶段 2(Model Registry)已完成。** 模型 API(`/v1/chat/completions`、`/v1/responses`)和后台路径当前返回 `404`,不提供会误导客户端的假成功响应;Bifrost 代理、Jev、Policy 与管理后台尚未实现。
5+
## 启动
66

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
239
```
2410

25-
## 模型注册表
11+
默认监听 `127.0.0.1:8080`,数据库为当前工作目录下的 `data/auto-router.db`。新建数据库使用 schema v8。
2612

27-
注册表由三部分组成:
13+
如需在本机并行运行另一实例,可用 `-listen 127.0.0.1:8081` 指定监听地址。
2814

29-
1. **models.dev 同步**(可选、显式触发):把白名单里的 `provider/model` 元数据与能力写入 SQLite;
30-
2. **本地覆盖**(配置文件):提供 `base_url`、`api_key`、启停、优先级与上游模型标识;
31-
3. **SQLite**:运行时唯一真相,路由只读不可变快照。
15+
### Docker Compose(GHCR)
3216

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
4022
```
4123

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`,否则会删除数据库卷。
5025

51-
## 配置
26+
镜像为 `ghcr.io/nextroad-dev/auto-router`,支持 `linux/amd64` 与 `linux/arm64`。`.env` 中的 `AUTO_ROUTER_VERSION` 可设为 `latest` 或具体版本(例如 `1.2.3`)。首次发布后,仓库维护者需在 GitHub Packages 将该镜像包可见性设为 **Public**,用户才能免登录拉取。
5227

53-
加载顺序为:内置默认值 → JSON 文件 → 环境变量(环境变量优先)。未知 JSON 字段、非法值、空值环境变量都会让进程在监听前失败,而不是静默使用默认值。`configs/example.json` 与内置默认值保持一致;`configs/registry.example.json` 给出注册表段的完整示例(可直接作为 `-config` 使用)。
28+
GitHub Actions 会在 PR 上构建验证、在 `main` 发布 `edge`,并在推送 `v*` 标签时发布版本标签和 `latest`:
5429

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+
```
6734

68-
注册表字段(**只能来自配置文件**,环境变量不覆盖列表):
35+
首次启动时按管理页面提示设置一个 owner 密码。首次设置由 `GET /admin/v1/setup/status` 检查状态,并由 `POST /admin/v1/setup/password` 提交。密码至少 12 字节,可在系统设置中修改。
6936

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 的旧数据库不会自动迁移至当前版本,也不会被自动删除。需要使用全新数据库时,请先停止服务,并自行备份或移走旧数据库文件,再启动服务。
7738

78-
约定:
39+
忘记密码时,停止服务并通过环境变量提供新密码后运行离线恢复命令:
7940

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+
```
8544

86-
## 命令与端点
45+
PowerShell:
8746

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
11651
```
11752

118-
## 开发
53+
恢复命令要求数据库已存在且为受支持版本,并取得服务监听端口;成功后新密码立即生效。该环境变量只用于本次恢复操作。
11954

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。
12661

127-
`go test -race` 需要 cgo 与 C 工具链;本机没有 C 工具链时该命令无法运行,`-race` 由 Linux CI 覆盖。
62+
启用 Jev 后,请求内容会按默认的脱敏模式发送至 Jev 服务;选择 features-only 模式时不会发送对话文本。原始 Jev 请求与响应正文默认不捕获。
12863

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
13065

131-
已完成的验证:
66+
推理入口:
13267

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 流式生成 |
13576

136-
平台限制:Windows 无法向当前进程投递 `os.Interrupt`,因此信号到优雅退出的真实链路测试在 Windows 上跳过,由 Linux CI 覆盖;优雅排空逻辑本身在两端都有测试。
77+
Anthropic 与 Gemini 原生入口按对应协议直通,要求所选上游支持相同协议。OpenAI Chat 文本请求可以转换后转发至 Anthropic 或 Gemini,但仅支持有限的安全子集;不支持的参数或功能会在发送上游前被拒绝。
13778

138-
## 后续阶段
79+
管理端主要接口:
13980

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

Comments
 (0)