mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 17:07:59 +00:00
README: new "冷却与半冷却探测(自愈调度)" section with the per-class cooldown table (5xx exponential to 5min / 401-403 10min / 429 30s fixed / quota aligned to its window), the probe-slot formula and the ordering rule that probes are tried last. The LuaJIT section now says the pool is an elastic ceiling rather than a preallocation and documents both step formulas. Headline figures replaced with measured ones: idle ~10 MB, ~19 MB starting with a 29 MB audit log, and a new bullet for on-demand log loading. package.json also loses a `\u2014` escape and the broken indentation that an earlier edit left in the electron-builder block.
446 lines
25 KiB
Markdown
446 lines
25 KiB
Markdown
# ModelRouter
|
||
|
||
> **English**: [README_EN.md](./README_EN.md)
|
||
>
|
||
> **AI-assisted**: 本项目使用 AI 辅助编程(代码与文档由 AI 协作完成,经人工复核)。
|
||
|
||
面向多 llms 订阅者,部署在内网,实现一次配置多个服务共同使用的效果。支持指定模型或 auto 模式,按照配置的优先级选择可用模型提供服务。
|
||
|
||
> **轻量且增源无需重编译**:网关本体是单 Go 二进制(约 10MB,零运行时依赖)。新增/切换上游
|
||
> 只需在 `config.yaml`(或 WebUI)加一个 `sources` 条目或挂一个 `.lua` 适配器——**不改 Go、
|
||
> 不重编译**。源与适配器的改动经 WebUI 提交时即时生效(热更新);直接编辑
|
||
> `config.yaml` 或 `adapter_dir` 下的 `.lua` 文件则需要重启进程生效。
|
||
|
||
## 核心优势
|
||
|
||
### 极致轻量
|
||
|
||
- **单二进制**:编译后约 10MB,零运行时依赖(仅依赖系统 libc),部署即用
|
||
- **极低内存占用**:空闲 ~10MB RSS,带 29MB 历史审计日志启动仅 ~19MB,满载并发 100+ 请求时峰值 < 100MB
|
||
- **日志按需加载**:审计日志不常驻内存——默认只加载首屏,下滚自动分页,CSV 导出流式写出(O(1) 内存),离页即释放
|
||
- **零运行时依赖**:纯 Go + LuaJIT 静态链接,无需安装 Python/Node/Java 等运行时
|
||
- **启动极快**:冷启动 < 200ms,热重载配置 < 10ms
|
||
|
||
### 强大的多租户调度能力
|
||
|
||
- **多密钥多租户**:支持无限密钥,每个密钥独立角色、模型范围、Token 配额、重置周期
|
||
- **AUTO 智能调度**:基于优先级档位的分级调度,同优先级源自动轮询负载均衡,故障自动毫秒级故障转移
|
||
- **自愈冷却**:冷却上限 5 分钟,过半后放行 1 个探测请求,上游/额度恢复即刻回归轮询,无需等满冷却窗口
|
||
- **Token 配额管理**:精确到模型级别的 Token 配额控制,支持小时/周/月/自定义小时周期自动重置
|
||
- **同优先级源负载均衡**:同一优先级档位的多个源,请求自动 Round-Robin 均匀分发,故障毫秒级故障转移
|
||
- **Source-Model 前缀路由**:支持 `source-model`/`source:model`/`source/model` 精确指定上游源
|
||
|
||
### 生产级可靠性
|
||
|
||
- **热加载配置**:WebUI 修改源/密钥/适配器/AUTO链即时生效,无需重启
|
||
- **密钥加密落盘**:AES-256-GCM 加密存储 `api_key`、网关密钥、请求头,master.key 0600 权限保护
|
||
- **实时源探测**:`GET {base}/models` 定期探测,毫秒级感知上游状态,不污染调度退避状态
|
||
- **全链路审计**:HTTP 访问日志、登录/配置变更审计、请求记录 CSV 导出、密钥用量统计 CSV 导出
|
||
|
||
### 灵活的协议适配
|
||
|
||
- **LuaJIT VM**:每个适配器独立 VM + **弹性 worker 池**(按需扩缩容,空闲时自动回收),安全并发,Lua 脚本热加载无需重启
|
||
- **Lua 适配器协议**:`transform_request` / `transform_response` / `transform_stream_chunk` 双向转换
|
||
- **动态请求头钩子**:`build_headers(meta)` 支持 HMAC 签名、动态 Header 注入
|
||
- **多模态透传**:`image_url` 等多模态内容在多源间无损透传,Anthropic/Gemini/Ollama 自动转换
|
||
|
||
## 实现
|
||
|
||
统一 OpenAI 兼容网关:把多个上游 LLM 源(DeepSeek、Qijiar、OpenAI、Anthropic、Gemini、
|
||
Groq、Mistral、Ollama、KimiCode…)通过 **Lua 适配器** 做协议转换,对内网暴露
|
||
一个标准 `OpenAI Chat Completions` 接口(`/v1/chat/completions` + `/v1/models`),
|
||
支持单次结算、SSE 流式、**AUTO 模型路由**与**多模态**透传。
|
||
|
||
从 [HomeAgent](https://gitcode.com/JianFeeeee/HomeAgent) 的多源 LLM 适配层
|
||
(`internal/agent/api/provider.go` + `internal/lua/adapters/*`)抽离而来并独立演进。
|
||
|
||
## 特性
|
||
|
||
- **多源**:一个进程内配置任意多个上游源,按请求的 `model` 自动路由。
|
||
- **AUTO 模式**:`default_model: AUTO` 时按优先级页保存的 AUTO 链档位逐档调度并发请求。
|
||
- **统一输出**:所有源对外都是 OpenAI 格式(含 `reasoning_content`、`tool_calls`、`usage`)。
|
||
- **生图**:`POST /v1/images/generations`,`kind: image` 的模型独立路由。
|
||
- **多模态**:`content` 数组(`image_url` 等)在多源间无损透传,Anthropic/Gemini/Ollama 自动转换。
|
||
- **LuaJIT VM**:基于 golua 绑定的 LuaJIT,每个适配器独立 VM + worker 池,安全并发。
|
||
池是 **弹性** 的:`Σ max_concurrent` 只是上限而非预分配,状态按需创建、空闲时自动回收,
|
||
因此空载网关几乎不持有 Lua 状态(启动 0 个)。
|
||
- 扩容步长由适配器最大并发决定:`clamp(ceil(max/8), 1, 8)`,且仅在**真实并发争用**
|
||
(所有现有状态均已占用)时批量预热,顺序流量始终只用 1 个状态。
|
||
- 缩容步长由当前连接数决定:`clamp(ceil(冗余/(1+占用)), 1, 冗余)`——无连接时一轮收到
|
||
常驻下限,忙时每轮只释放 1 个,保护热路径。WebUI「适配器」页展示实时池指标。
|
||
- **disable_thinking**:请求 `disable_thinking:true`(或上游对应字段)动态开关推理。
|
||
- **Lua 适配协议**:每个源挂一个 `.lua` 适配器,完成 `transform_request` /
|
||
`transform_response` / `transform_stream_chunk` 双向转换,协议差异全在 Lua 层。
|
||
- **签名 / 请求头钩子**:适配器可定义 `build_headers(meta)`,在 Go 发 HTTP 前
|
||
动态注入/签名请求头——用于云端 API 校验调用方 app(如 KimiCode 只放行特定
|
||
agent)。提供 `hmac_sha256_hex` / `sha256_hex` / `base64_encode` 等签名辅助。
|
||
- **WebUI**:内置管理界面,可在线查看/新增/编辑上游源与模型、配置 AUTO 优先级链、
|
||
管理密钥模型范围,写入运行时文件持久化。
|
||
- **密钥加密存储**:运行时文件中的上游 `api_key`、自定义请求头值、网关 key 均以
|
||
AES-256-GCM 加密落盘(`master.key` 独立 0600,或 `LLMS_PROXY_MASTER_KEY`)。
|
||
- **实时源探测**:状态页探测各源可达性(`GET {base}/models`,失败回退最小请求),
|
||
不污染正常调度的退避状态,错误信息在 UI 可悬停查看。
|
||
- **鉴权**:网关自身用 `gateway_keys` 校验客户端 Bearer key;与上游各自的 key 相互独立。
|
||
- **流式**:SSE `chat.completion.chunk`,含角色首包与 `[DONE]` 收尾。
|
||
- **全链路审计**:HTTP 访问日志、登录/配置变更审计、请求记录 CSV 导出、密钥用量统计 CSV 导出
|
||
- **AES-256-GCM 加密存储**:运行时文件敏感字段加密落盘,master.key 0600 权限,支持环境变量注入主密钥
|
||
|
||
## 快速开始
|
||
|
||
```bash
|
||
GOMODCACHE=... GOPROXY=off go build -tags luajit -o llmsproxy ./cmd/llmsproxy
|
||
./llmsproxy -config config.yaml # 首次运行自动生成默认配置并打印随机 admin key
|
||
```
|
||
|
||
> 依赖 [golua](https://github.com/aarzilli/golua)(LuaJIT 绑定)。**必须**带 `-tags luajit`
|
||
> 构建,否则默认走内置超集 gopher-lua 路径(行为略有差异)。
|
||
|
||
> **配置不入库**:仓库不携带任何 config 文件(配置文件含密钥)。二进制首次运行时
|
||
> 会在 `-config` 指定的位置生成默认配置:随机 admin key(打印在启动日志中)、
|
||
> 仅绑定 `127.0.0.1:8080`。首次登录后请在 WebUI「密钥」页更换管理员密钥。
|
||
|
||
```bash
|
||
# 无 key -> 401
|
||
curl http://127.0.0.1:8080/v1/models
|
||
|
||
# 单次($KEY 换成首次启动日志打印的 admin key)
|
||
curl -H "Authorization: Bearer $KEY" \
|
||
-d '{"model":"AUTO","messages":[{"role":"user","content":"hi"}]}' \
|
||
http://127.0.0.1:8080/v1/chat/completions
|
||
|
||
# 流式
|
||
curl -N -H "Authorization: Bearer $KEY" \
|
||
-d '{"model":"AUTO","stream":true,"messages":[{"role":"user","content":"hi"}]}' \
|
||
http://127.0.0.1:8080/v1/chat/completions
|
||
```
|
||
|
||
任何 OpenAI SDK 把 `base_url` 指到网关地址、`api_key` 用任意一个已创建并授权的网关 key 即可。
|
||
|
||
## 配置
|
||
|
||
首次运行在 `-config` 路径自动生成默认配置(默认 `./config.yaml`),核心字段:
|
||
|
||
```yaml
|
||
listen: 127.0.0.1:8080 # 网关监听地址(建议绑内网/回环)
|
||
gateway_keys: [sk-gw-<随机>] # 初始 admin 密钥(每次安装随机生成,仅首启写入运行时存储用)
|
||
default_model: AUTO # model 无法路由时自动按优先级链选源
|
||
adapter_dir: adapters # Lua 适配目录;目录不存在时首启创建并 seed 内置,存在则只读
|
||
runtime_file: runtime.json # WebUI 编辑的源/密钥/AUTO 链持久化到此文件
|
||
|
||
sources:
|
||
- name: deepseek
|
||
base_url: https://api.deepseek.com
|
||
api_key: sk-... # 也可用 api_key_env: SOME_ENV 引用环境变量(不落盘明文)
|
||
adapter: deepseek
|
||
max_concurrent: 8
|
||
models:
|
||
- id: deepseek-v4-flash
|
||
priority: 100 # YAML 源首次启动会 seed 进 AUTO 链的初值
|
||
kind: chat
|
||
- id: deepseek-v4-pro
|
||
priority: 60
|
||
kind: chat
|
||
# 静态请求头(比适配器默认优先)
|
||
headers: { X-Tenant: prod }
|
||
# 传给 Lua build_headers 的透传元数据
|
||
meta: { app_id: x, app_secret: y }
|
||
temperature: 0.7
|
||
max_tokens: 4096
|
||
timeout: 120s # 请求超时,默认 120s
|
||
```
|
||
|
||
`config.yaml`、`runtime.json`、`master.key`、`adapters/` 均不提交版本库(见
|
||
`.gitignore`),请随备份带走——`master.key` 丢失后运行时密文无法解密。
|
||
|
||
运行文件生成在 `-config` 所在目录:`adapter_dir` / `runtime_file` 自动指向
|
||
配置文件旁边的 `adapters/` 与 `runtime.json`,从任意工作目录启动都可用。
|
||
|
||
运行时文件(`runtime_file`)中的敏感字段自动加密:
|
||
|
||
- 加密算法 AES-256-GCM,格式 `enc:v1:<base64>`。
|
||
- 主密钥来源:环境变量 `LLMS_PROXY_MASTER_KEY`(64 位 hex);否则读取
|
||
`runtime_file` 同目录的 `master.key`;都不存在时首次启动自动生成 `master.key`(0600)。
|
||
- 升级时旧明文文件自动兼容:首次运行正常读取,任何 UI 保存操作触发全文件加密迁移。
|
||
- 注意:**master.key 丢失后密文无法解密**,请随配置一起备份;切勿提交到版本库。
|
||
|
||
### 网关密钥(多密钥)
|
||
|
||
网关鉴权采用「多密钥 + 角色 + 模型范围」架构,密钥持久化在 `runtime_file` 的
|
||
`keys` 字段(加密存储):
|
||
|
||
- `gateway_keys` 配置只是**初始 admin 密钥种子**:首次启动迁移为运行时 admin
|
||
key,之后不再参与鉴权管理。
|
||
- **重要:首次启动后请在 WebUI「密钥」页更换管理员密钥**——初始密钥明文写在
|
||
`config.yaml` 里(每次安装随机生成),继续使用存在被盗风险;用新密钥登录后删除初始密钥。
|
||
- WebUI **密钥页**可创建/删除密钥;每个密钥可指定 `admin`(管理全部)或
|
||
`user`(仅看自己的 key)角色,并配置**模型范围**(模型 + 源 + token 配额 +
|
||
重置周期)。
|
||
- 客户端用任意一个已授权的密钥明文作为 Bearer(`Authorization: Bearer <key>`)。
|
||
- 删除密钥即从运行时存储移除,立即失效。
|
||
|
||
### 模型路由
|
||
|
||
`/v1/chat/completions` 的 `model` 解析顺序:
|
||
|
||
1. `source/model` 或 `source:model` 前缀 → 指定源;
|
||
2. 精确匹配某个源的 `model`;
|
||
3. 配置 `default_model: AUTO` 时 → 走优先级页保存的 AUTO 链(见下);
|
||
4. 否则回落(返回错误)。
|
||
|
||
任何 OpenAI 客户端,只要 `model` 设为某个源的 `name/任意名`,即可锁定走该源;
|
||
设为 `AUTO`(或网关配了 `default_model: AUTO`)即自动按优先级链选源。
|
||
|
||
### AUTO 链(优先级页)
|
||
|
||
AUTO 调度**只**由优先级页保存的规则决定,源配置里的 `models[].priority` 数字不再
|
||
参与调度、也不再显示。**聊天与生图是两套独立的链**,在优先级页面通过「聊天 / 生图」
|
||
按钮切换编辑:
|
||
|
||
- **聊天链**(`runtime_file` 的 `auto` 字段):服务 `POST /v1/chat/completions`
|
||
的 `model: AUTO` 请求;只接受 `kind: chat` 模型,配置在链里的生图槽位会被忽略。
|
||
- **生图链**(`runtime_file` 的 `auto_image` 字段):服务 `POST /v1/images/generations`
|
||
的 `model: AUTO` 请求;只接受 `kind: image` 模型。未配置生图链时,AUTO 生图回落
|
||
到「所有含生图模型的源、按注册顺序」的旧行为,兼容旧配置。
|
||
|
||
每条规则 = 一个「槽位」:`{ model, source, tier, token_quota, period, hours }`。
|
||
|
||
- `tier` 表示优先级档位:同一档的模型并排、共享该优先级;档位从上到下递减。
|
||
- 同一模型可配置多个槽位(如 A 源低配 → B 源低配 → A 源高配 → B 源高配),按档位顺延。
|
||
- `token_quota` > 0 时该槽位在重置周期内用满即顺延到下一槽位;`period` 支持
|
||
`hour` / `week` / `month` / `nhour`(配合 `hours`),空 = 不限。
|
||
- 同 tier 内按「偏好分」排序,失败槽位冷却后自动跳过;冷却 / 配额耗尽 / hard
|
||
错误会顺延到下一 tier,全部失败时返回 503 并附上每档失败摘要。
|
||
|
||
### 冷却与半冷却探测(自愈调度)
|
||
|
||
槽位失败后进入指数退避冷却(5s → 10s → 20s …,上限 **5 分钟**)。冷却不是全时段
|
||
硬闸:
|
||
|
||
- **前半段完全静默**:刚失败的槽位不接任何流量,避免对故障上游持续施压。
|
||
- **后半段放行 1 个探测**:窗口过半后,同一 `(源, 模型)` 最多允许 **一个** 请求作为
|
||
探测通过。探测槽数 = `clamp(max_concurrent / 10, 1, 2)`——`max_concurrent: 10`
|
||
的源恰好放行 1 个探测请求。
|
||
- **探测排在最后**:同 tier 内探测候选永远排在健康候选之后,只有没有正常槽可用时
|
||
才承接真实流量,因此探测不会抢走可用容量。
|
||
- **探测即真实请求**:成功则立即清零失败计数与冷却,槽位当场回到正常轮询——
|
||
上游恢复/额度恢复后不必等满整个冷却窗口。失败则重开一个新窗口,下次探测顺延到
|
||
新窗口的中点,不会连续重试。
|
||
|
||
分类冷却策略:
|
||
|
||
| 情况 | 冷却 | 说明 |
|
||
|---|---|---|
|
||
| 传输错误 / 5xx | 5s→10s→20s…,上限 5min | 指数退避 |
|
||
| 401 / 403 凭据错误 | 10min | 换 key 后由下一次探测自动接回 |
|
||
| 429 限流 | 30s 固定 | 「太快」不等于「坏了」,不进指数阶梯 |
|
||
| 配额耗尽(`insufficient_quota` / `余额不足` 等) | 对齐配额窗口,上限 30min | **不进指数阶梯、不涨失败计数**,额度恢复即可用 |
|
||
|
||
WebUI「优先级」页的槽位健康标签会区分 `冷却` / `待探测` / `探测中`,悬停可看窗口
|
||
起点、探测放行时刻与完全恢复时刻。
|
||
|
||
### 源(Source)与适配器(Adapter)的关系
|
||
|
||
- **源** 是到某个上游的连接描述:`name`、`base_url`、`api_key`、模型列表与优先级。
|
||
- **适配器** 是协议转换逻辑(Lua 脚本):把统一 OpenAI 格式请求转成上游原生格式,
|
||
再把上游响应/流式分块转回统一格式。
|
||
- 一个适配器可被多个源复用(如 `openai.lua` 同时服务多个 OpenAI 兼容站点);
|
||
同一个源也可以切换不同适配器(改 `adapter` 字段即可)。
|
||
- 源决定“连谁、暴露哪些模型”,适配器决定“怎么对话”——二者在 `sources[]` 条目中
|
||
通过 `adapter` 字段关联。
|
||
|
||
加载流程(`internal/core` 的 `Core` 负责装配):
|
||
|
||
1. 启动时 `lua.NewVM(adapter_dir)` 加载全部适配器:内置适配器(编译期 embed)+
|
||
`adapter_dir` 下的同名覆盖文件;内置文件写在代码内,覆盖文件要求更高优先级。
|
||
2. `config.Load` 读取 `config.yaml`,`config.NewStore(runtime_file)` 读 WebUI 改动的
|
||
运行时源,二者按名称合并成完整源列表。
|
||
3. `rebuildRegistry` 为每个源创建 `provider.Provider`(持有目标适配器),并按源的
|
||
并发上限配置适配器 worker 池**上限**(不预分配,见下)。
|
||
4. 请求进来时 `Registry` 按 `model` 路由到 Provider,Provider 调适配器
|
||
`transform_request` → HTTP 发送 → `transform_response` / `transform_stream_chunk`。
|
||
|
||
WebUI 上的"新增/编辑源"、"上传 Lua 适配器"、"改 AUTO 优先级链"与"密钥模型范围"
|
||
都即时生效(写入运行时文件或 `adapter_dir` 后重新装配,无需重启);直接编辑
|
||
`config.yaml` / `adapter_dir` 下的文件则需要重启进程才会重新加载。
|
||
|
||
在 WebUI 删除源/适配器时:运行时(WebUI 创建)的源与 `.lua` 文件会真正移除;
|
||
`config.yaml` 中定义的基源无法改写配置文件,采用删除标记隐藏(重启后仍隐藏),
|
||
在 UI 里重新添加同名源即可恢复。
|
||
|
||
### 源模板(多 key 负载均衡)
|
||
|
||
多个上游 key 共享同一套配置(URL / 适配器 / 模型列表 / 并发 / rpm)时,逐一复制
|
||
整段 source 太冗余。**源模板**保存除 `name` 和 `api_key` 以外的全部源字段,
|
||
从模板一键创建多个仅 key 不同的源,天然支持多 key 负载均衡(每个展开源独立调度、
|
||
独立冷却、独立健康状态)。
|
||
|
||
- WebUI「源」页右上角「**模板管理**」按钮:列出全部模板,可编辑 / 删除 / 新建。
|
||
- 添加源弹窗右上角两个按钮:
|
||
- **从模板创建** → 选模板 → 表单自动填充(name + key 仍需你填);
|
||
- **存为模板** → 把当前表单非 key/name 字段存为新模板。
|
||
- 模板持久化在 `runtime_file` 的 `source_templates` 字段,与运行时源一样热管理,
|
||
改完即时生效。模板本身不含密钥,可以安全共享 / 纳管。
|
||
|
||
模板不会自己变成源——它只是「配方」。真正承担流量的是从模板创建出来的、带 key
|
||
的具体源。
|
||
|
||
### disable_thinking
|
||
|
||
请求体带 `"disable_thinking": true` 时,网关透传给各适配器;DeepSeek 适配器将其
|
||
映射为 `extra_body.thinking.type = "disabled"` 关闭推理,其余源按各自协议处理。
|
||
|
||
### WebUI
|
||
|
||
内置管理界面(`GET /`),登录后可在浏览器完成:
|
||
|
||
- **状态页**:源在线状态(实时探测 + 悬停看错误)、模型/源/key 用量统计、请求记录,
|
||
支持按时间范围导出 CSV;点击模型可生成 pin 到该模型的连接配置。
|
||
- **对话页**:流式/非流式调试。
|
||
- **密钥页**:创建/编辑网关 key,为每个 key 配模型范围(模型 + 源 + token 配额 + 周期),
|
||
管理员管理全部 key,用户只看到自己的 key。
|
||
- **优先级页**:拖拽积木配置 AUTO 链档位。
|
||
- **源页**:在线增删改上游源(API key 等敏感字段加密落盘)。
|
||
- **适配器页**:上传 / 删除 Lua 适配器脚本。
|
||
|
||
改动写入 `runtime_file`(重启仍生效)。
|
||
|
||
## Lua 适配器协议
|
||
|
||
完整 API 见 **[Lua 适配器 API 文档](docs/lua-adapters.md)**([English](docs/lua-adapters-en.md))。
|
||
|
||
每个适配器是一个返回 table 的 Lua 脚本(`internal/lua/adapters/<name>.lua`),
|
||
加载时可被 `adapter_dir` 下的同名脚本覆盖——**改 Lua 脚本无需重编译**,
|
||
重启即生效;通过 WebUI 上传的适配器与在线编辑的源配置则即时生效。
|
||
|
||
```lua
|
||
return {
|
||
name = "mysrc",
|
||
version = "1.0.0",
|
||
endpoint = "/chat/completions", -- 上游路径(可被 source.endpoint 覆盖)
|
||
headers = { ["X-Static"] = "v" }, -- 静态默认请求头(build_headers 缺省时使用)
|
||
|
||
-- 请求转换:把 OpenAI 格式 req 转成上游原生格式,返回字符串
|
||
transform_request = function(raw_json) ... end,
|
||
|
||
-- 响应转换:把上游原生响应转成统一格式字符串
|
||
-- { content, reasoning_content, finish_reason, token_usage{...}, tool_calls[{...}] }
|
||
transform_response = function(raw_json) ... end,
|
||
|
||
-- 流式分块转换:把上游 SSE data 转成 { content, done, ... },返回 "" 则跳过
|
||
transform_stream_chunk = function(raw_chunk) ... end,
|
||
|
||
-- [可选] 动态请求头/签名钩子
|
||
-- meta = { url, method, body, api_key, timestamp, source={ name, meta={...} } }
|
||
build_headers = function(meta) return { ["X-App-Sign"] = sign } end,
|
||
}
|
||
```
|
||
|
||
内置信号辅助:`hmac_sha256_hex(key, data)`、`sha256_hex(data)`、`base64_encode(s)`、
|
||
`tohex(s)`、`json.encode/decode`、`log(level, msg)`。
|
||
|
||
### 内置适配器
|
||
|
||
`openai` `deepseek` `anthropic` `gemini` `github` `groq` `mistral` `ollama` `kimicode`。
|
||
|
||
`anthropic`/`gemini`/`ollama` 适配器内置多模态转换(`image_url` → 各自上游格式);若
|
||
源启用 `disable_thinking`,`deepseek` 适配器会把 `extra_body.thinking.type` 置为
|
||
`disabled`。
|
||
|
||
**kimicode** 是展示 `build_headers` 的样例:云端校验调用方 app,需要按
|
||
`meta.app_secret` 对时间戳+URL+请求体哈希做 HMAC 签名并附 `X-App-Sign` 等头。
|
||
配好 `sources[].meta.{app_id, app_secret, app_agent}` 即可。
|
||
|
||
## 桌面 GUI(Electron)
|
||
|
||
可选独立产物,面向内网桌面端用户:**内嵌 ModelRouter 核心**(Clash Verge 形态),
|
||
一键启停、系统托盘常驻、开机自启、静默启动,内容区一比一内嵌完整 WebUI
|
||
(状态/对话/密钥/优先级/源/适配器 六个页面,免登录)。服务器用户继续使用纯 Go 二进制。
|
||
|
||
### 两种安装包:Headless 与 Desktop
|
||
|
||
发布到 Release 的安装包分为两类,面向不同用户:
|
||
|
||
- **Headless(服务器版)**:纯核心二进制(无 Electron),配置驱动,适合服务器、
|
||
容器、systemd 等无人值守场景。Linux 为 `.tar.gz`(二进制 + `config.example.yaml`
|
||
+ `adapters/` + README),Windows 为 `.zip`。
|
||
- **Desktop(桌面版)**:Electron GUI 安装包,内嵌核心,适合个人桌面日常使用。
|
||
|
||
### 选择 GUI 还是纯后端
|
||
|
||
| 场景 | 推荐 | 理由 |
|
||
| ------ | ------ | ------ |
|
||
| 服务器 / 内网网关 / 无人值守常驻 | **Headless**(单二进制) | 极轻量(~10MB、~15MB RSS),零依赖单进程,直接跑在 systemd/任意容器里,远程管理 |
|
||
| 个人桌面日常使用 / 多设备内网共享 | **Desktop**(GUI) | 免登录内嵌 WebUI、系统托盘一键启停、开机自启、静默后台,适合不懂命令行的使用者 |
|
||
| Windows 桌面 | **Desktop** | 纯后端在 Windows 上需自行注册服务,GUI 提供原生托盘/自启体验 |
|
||
| CI 一键出三平台安装包 | **Desktop 打包脚本** | `make gui-*` 系列出 deb/AppImage/NSIS,可进发行流水线 |
|
||
|
||
两者完全同源:GUI 内嵌的就是纯后端的同一份 `llmsproxy` 核心(LuaJIT 版),
|
||
配置/适配器格式完全一致,可随时互换。
|
||
|
||
### 构建与运行
|
||
|
||
```bash
|
||
cd cmd/gui
|
||
npm install
|
||
make gui # 或 npm run dev —— 开发运行(需先 make build 生成 bin/llmsproxy)
|
||
make gui-deb # 本机构建 deb(分享给其它 Linux 用户)
|
||
make gui-dist # 构建 deb + AppImage(linux)
|
||
make gui-win # 构建 Windows nsis 安装包(本机 mingw + wine)
|
||
make gui-win-docker # 构建 Windows nsis 安装包(全 docker 自足,无需本机 mingw)
|
||
make gui-dist-dir # 仅解包产物(调试用,不产安装包)
|
||
```
|
||
|
||
产物在 `cmd/build/gui-dist/`:`ModelRouter-<ver>.AppImage`、
|
||
`modelrouter-gui_<ver>_amd64.deb`、`ModelRouter Setup <ver>.exe`(win)。rpm 需系统 `rpmbuild`。
|
||
发布时会把产物重命名为 `ModelRouter-Desktop-*`(GUI)与 `ModelRouter-Headless-*`(纯后端)
|
||
两类上传到 GitCode Release。
|
||
|
||
#### Windows 打包(docker 固化方案,推荐)
|
||
|
||
`make gui-win-docker` 一步完成:交叉编译核心 + NSIS 打包,宿主机**无需安装 mingw**。
|
||
|
||
- 构建镜像:`cmd/gui/docker/win-builder/Dockerfile`(`golang:1.25` + mingw-w64 +
|
||
预编译 LuaJIT windows 版 `lua51.dll` 与 import lib)。LuaJIT 源码取自 gitcode
|
||
镜像(国内可达),github 兜底;Go 依赖走 `https://goproxy.cn`。
|
||
- 核心编译:`cmd/gui/scripts/win-core-docker.sh` —— 在容器内产出
|
||
`cmd/gui/bin/{llmsproxy.exe,lua51.dll}`。
|
||
- 一键发布:`cmd/gui/scripts/dist-win-docker.sh` —— 核心编译后调用
|
||
electron-builder(宿主需有 `wine` 用于 NSIS)。
|
||
- 首次运行自动构建 `modelrouter/win-builder` 镜像;已存在则直接复用。
|
||
|
||
```bash
|
||
# 全 Windows 发布(核心重编 + NSIS):
|
||
make gui-win-docker
|
||
```
|
||
|
||
### 特性
|
||
|
||
- **内嵌核心**:自动拉起捆绑的 `llmsproxy`(LuaJIT 版),配置/密钥/adapter 存于
|
||
用户数据目录 `<userData>/profile/`,首次运行自动生成随机 admin key 写入 `keys`
|
||
(非 seed)并自注入,WebUI 免登录、不会提示"更换初始密钥"。端口可在设置中更改(默认 8787)。
|
||
- **系统托盘**:内核状态点、开机自启/静默启动开关、启停/重启内核、退出。
|
||
- **静默启动**:`--silent` 参数或设置项,启动仅驻托盘不弹窗;首次运行始终显示窗口。
|
||
- **开机自启**:Windows/macOS 用 `setLoginItemSettings`;Linux 写
|
||
`~/.config/autostart/modelrouter-gui.desktop`,静默开启时自动附加 `--silent`。
|
||
|
||
## 目录
|
||
|
||
```
|
||
cmd/llmsproxy # 入口(纯 Go 服务端)
|
||
cmd/gui # Electron 桌面 GUI(内嵌核心 + 托盘 + 打包)
|
||
internal/config # YAML 配置加载/校验
|
||
internal/lua # LuaJIT VM + worker 池 + AdapterCache + 内置适配器 (embed)
|
||
internal/provider # Provider(HTTP) + Registry(路由)
|
||
internal/gateway # OpenAI 兼容 HTTP 服务 + 鉴权 + SDK/流式 + WebUI
|
||
internal/types # 统一格式 & OpenAI wire 类型
|
||
```
|
||
|
||
## 测试
|
||
|
||
```bash
|
||
go test -tags luajit ./...
|
||
```
|
||
|
||
覆盖:配置校验、适配器加载/变换、签名钩子、Gateway 鉴权、SDK 结算、SSE 流式、
|
||
模型路由、多模态透传与 disable_thinking。
|