Adopt GitHub Flow + release branches, replacing "everything straight to main plus a tag" which caused the 1.4.2 pain (a fix had to be retro-fitted to the released version, forcing a remote-tag delete + full re-upload). - main: only long-lived branch, always deployable, accumulates the next version - feature/<desc>: born from main, merged back when done - release/vX.Y.Z: cut from main, tagged, installers built from the tag - hotfixes land on the release branch AND are cherry-picked back to main so main never loses a fix - end of lifecycle = retire the release branch (delete; or keep for long-term maintenance), no wholesale merge back — hotfixes already flowed - explicitly no rebase of main, no release-branch-merge, no quick edits on main Companion release checklist includes the upload lessons (PUT --http1.1) and the replace-artifacts-by-deleting-the-tag catch. Docs in docs/git-workflow.md (zh) and docs/git-workflow-en.md, linked from both READMEs.
29 KiB
ModelRouter
English: README_EN.md
AI-assisted: 本项目使用 AI 辅助编程(代码与文档由 AI 协作完成,经人工复核)。
面向多 llms 订阅者,部署在内网,实现一次配置多个服务共同使用的效果。支持指定模型或 auto 模式,按照配置的优先级选择可用模型提供服务。
轻量且增源无需重编译:网关本体是单 Go 二进制(约 10MB,零运行时依赖)。新增/切换上游 只需在
config.yaml(或 WebUI)加一个sources条目或挂一个.lua适配器——不改 Go、 不重编译。源与适配器的改动经 WebUI 提交时即时生效(热更新);直接编辑config.yaml或adapter_dir下的.lua文件则需要重启进程生效。
核心优势
极致轻量
- 单二进制:编译后 8~12MB(
-s -wstrip 后约 8MB),零运行时依赖(仅依赖系统 libc),部署即用 - 低内存占用:与源数量相关,非与运行时长相关——单源 ~10MB、16 源生产实例 ~32–35MB(实测分解与调优)
- 日志按需加载:统计数字来自全量审计日志(启动时流式扫一遍即释放,29MB/22 万行约 260ms),但原始记录不常驻内存——默认只加载首屏,下滚自动分页,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 的多源 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 权限,支持环境变量注入主密钥
快速开始
GOMODCACHE=... GOPROXY=off go build -tags luajit -o llmsproxy ./cmd/llmsproxy
./llmsproxy -config config.yaml # 首次运行自动生成默认配置并打印随机 admin key
依赖 golua(LuaJIT 绑定)。必须带
-tags luajit构建,否则默认走内置超集 gopher-lua 路径(行为略有差异)。
配置不入库:仓库不携带任何 config 文件(配置文件含密钥)。二进制首次运行时 会在
-config指定的位置生成默认配置:随机 admin key(打印在启动日志中)、 仅绑定127.0.0.1:8080。首次登录后请在 WebUI「密钥」页更换管理员密钥。
# 无 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),核心字段:
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 解析顺序:
source/model或source:model前缀 → 指定源;- 精确匹配某个源的
model; - 配置
default_model: AUTO时 → 走优先级页保存的 AUTO 链(见下); - 否则回落(返回错误)。
任何 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 负责装配):
- 启动时
lua.NewVM(adapter_dir)加载全部适配器:内置适配器(编译期 embed)+adapter_dir下的同名覆盖文件;内置文件写在代码内,覆盖文件要求更高优先级。 config.Load读取config.yaml,config.NewStore(runtime_file)读 WebUI 改动的 运行时源,二者按名称合并成完整源列表。rebuildRegistry为每个源创建provider.Provider(持有目标适配器),并按源的 并发上限配置适配器 worker 池上限(不预分配,见下)。- 请求进来时
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 的具体源。
内存占用(实测与调优)
内存不是一个定数:它随配置的源数量增长(每个源一个 http.Transport 与连接池、
一组健康状态),与运行时长无关。本机实测(Linux x86_64,12 核):
| 部署形态 | 启动 RSS | 稳态 RSS |
|---|---|---|
| 1 源 / 1 适配器(最小配置) | ~4 MB | ~10 MB |
| 1 源 + 29 MB 历史审计日志 | ~19 MB | ~20 MB |
| 16 源 / 13 适配器 / 59 模型(本机生产) | ~28 MB | ~32–35 MB |
历史参考:本项优化前同一生产配置为 ~105 MB。降幅来自三处:审计日志的原始记录 不再常驻内存(约 25 MB;统计聚合仍扫全量,但扫完即释放)、Lua 状态池不再单调增长、 以及下面两个运行时开关。
内存构成(生产实例分段测量,/proc/<pid>/smaps):
| 区域 | RSS | 说明 |
|---|---|---|
| Go 堆 | ~14 MB | provider/registry/scheduler 结构 + 连接池缓冲 |
| 其他匹名(线程栈 / LuaJIT chunk / runtime) | ~12 MB | 与线程数、已加载适配器数相关 |
| 二进制 text+rodata | ~8 MB | 映射的可执行文件页(只读、可被内核回收) |
| Go runtime 预留 | ~4 MB | VSZ 上看到的 2 GB+ 是地址空间预留,不占物理内存 |
| 共享库 | ~3 MB | libc / libluajit / libm |
两个推荐的部署开关(只适用于环境变量,无需改代码):
# /etc/systemd/system/llmsproxy.service
Environment=GOGC=50
Environment=MALLOC_ARENA_MAX=2
MALLOC_ARENA_MAX=2:LuaJIT 的分配走 cgo → glibc malloc,glibc 默认允许8×nproc个 per-thread arena,每个碰到 malloc 的 OS 线程会占用一个(各约 1 MB,且不归还给系统)。 实测从 8–15 个 arena(约 7–12 MB)降到 0。GOGC=50:把 Go 堆增长目标减半。单独使用无效(省下的堆会立即被更多 glibc arena 吃掉,实测 20.3 → 21.5 MB 反而变大),必须与MALLOC_ARENA_MAX配合, 两者同时开启才降 ~19%。网关是 I/O 密集型(本机 9 小时仅消耗 1min10s CPU), 多出的 GC 周期与它的空闲 CPU 相比可忽略。
桌面版(Electron)已在
cmd/gui/main.js里默认为内嵌核心注入这两个开关;手动 export 同名 环境变量可覆盖。如果你用自己的 systemd unit / 容器,建议照上面加上。
可选:GOMEMLIMIT=48MiB 作软上限,实测再省 ~1 MB;代价是逐近上限时 GC 转激进,
温和场景不必开。
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 文档(English)。
每个适配器是一个返回 table 的 Lua 脚本(internal/lua/adapters/<name>.lua),
加载时可被 adapter_dir 下的同名脚本覆盖——改 Lua 脚本无需重编译,
重启即生效;通过 WebUI 上传的适配器与在线编辑的源配置则即时生效。
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 等无人值守场景。发布包含
deb+rpm(含 systemd unit、内存调优 与示例配置)与tar.gz(二进制 +config.example.yaml+adapters/+ README), Windows 为.zip。 - Desktop(桌面版):Electron GUI 安装包,内嵌核心,适合个人桌面日常使用。
选择 GUI 还是纯后端
| 场景 | 推荐 | 理由 |
|---|---|---|
| 服务器 / 内网网关 / 无人值守常驻 | Headless(单二进制) | 极轻量(二进制 ~8MB;RSS 随源数量,单源 ~10MB、16 源 ~32–35MB,见内存占用),零依赖单进程,直接跑在 systemd/任意容器里,远程管理 |
| 个人桌面日常使用 / 多设备内网共享 | Desktop(GUI) | 免登录内嵌 WebUI、系统托盘一键启停、开机自启、静默后台,适合不懂命令行的使用者 |
| Windows 桌面 | Desktop | 纯后端在 Windows 上需自行注册服务,GUI 提供原生托盘/自启体验 |
| CI 一键出三平台安装包 | Desktop 打包脚本 | make gui-* 系列出 deb/AppImage/NSIS,可进发行流水线 |
两者完全同源:GUI 内嵌的就是纯后端的同一份 llmsproxy 核心(LuaJIT 版),
配置/适配器格式完全一致,可随时互换。
构建与运行
cd cmd/gui
npm install
make gui # 或 npm run dev —— 开发运行(需先 make build 生成 bin/llmsproxy)
make gui-deb # 本机构建 deb(分享给其它 Linux 用户)
make gui-dist # 构建 deb + rpm + AppImage(linux,rpm 需系统 rpmbuild)
make core-dist # 构建核心 deb + rpm + tar.gz(服务器版,需 nfpm)
make gui-win # 构建 Windows nsis 安装包(本机 mingw + wine)
make gui-win-docker # 构建 Windows nsis 安装包(全 docker 自足,含 wine)
make gui-dist-dir # 仅解包产物(调试用,不产安装包)
产物在 cmd/build/gui-dist/:ModelRouter-<ver>.AppImage、
modelrouter-gui_<ver>_amd64.deb、ModelRouter Setup <ver>.exe(win);
核心包在 cmd/build/dist/:llmsproxy_<ver>_amd64.deb、llmsproxy-<ver>.x86_64.rpm。
rpm 需系统 rpmbuild。每次打包后都会跑 packaging/verify-dist.sh 产物体检
(大小下限门禁——1.3.0 曾因宿主 wine 损坏产出 264KB 的残缺 exe 却没有被拦住)。
发布时会把产物重命名为 ModelRouter-Desktop-*(GUI)与 ModelRouter-Headless-*(纯后端)
两类上传到 GitCode Release。
Windows 打包(docker 固化方案,推荐)
make gui-win-docker 一步完成:交叉编译核心 + NSIS 打包全在 docker 内,宿主机
无需 mingw、无需 wine、无需 node。镜像内置 wine(含 i386 运行时),修复了
此前宿主 wine 损坏(缺 syswow64 → c0000135)导致产出 264KB 残缺 exe 的问题。
- 构建镜像:
cmd/gui/docker/win-builder/Dockerfile(golang:1.25+ mingw-w64 + node + wine32/wine64 + 预编译 LuaJIT windows 版lua51.dll与 import lib)。 LuaJIT 源码取自 gitcode 镜像(国内可达),github 兜底;Go 依赖走https://goproxy.cn。 - 一键发布:
cmd/gui/scripts/dist-win-docker.sh—— 核心编译 + electron-builder NSIS 全部在容器内执行(USE_SYSTEM_WINE=true走镜像内的 wine);electron-builder 的 ~400MB 工具链缓存挂到 docker volume,二次构建增量复用。 - 首次运行自动构建
modelrouter/win-builder镜像(约 5–10 分钟);已存在则直接复用。 - 打包完成后自动跑
packaging/verify-dist.sh门禁:exe 低于 5MB / deb·rpm·7z 低于 10MB 即失败——残缺安装包不可能再溜进 Release。
# 全 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 类型
开发协作
- Git 分支工作流:主分支 / 特性分支 / 发布分支的规范见
docs/git-workflow.md(English)。
主线:
main永可部署 → 特性合入main→ 切release/vX.Y.Z打 tag 发布 → hotfix 提交发布分支并 cherry-pick 回main。 - Lua 适配器协议:见 docs/lua-adapters.md (English)。
测试
go test -tags luajit ./...
覆盖:配置校验、适配器加载/变换、签名钩子、Gateway 鉴权、SDK 结算、SSE 流式、 模型路由、多模态透传与 disable_thinking。