The README claimed "~15 MB RSS" and, after the log-loading work, "~10 MB idle /
~19 MB with a 29 MB audit log". Those were TEST-INSTANCE numbers: one mock source
and one adapter. The real production config on this host (16 sources, 13
adapters, 59 models) sits at ~37-42 MB, and sat at ~105 MB before this series.
Quoting the single-source figure as the headline was misleading.
Both READMEs now state that memory scales with the number of configured sources
rather than with uptime, give a three-row measurement table (1 source / 1 source
with a 29 MB audit history / the 16-source production instance), and break the
production RSS down per region (Go heap, thread stacks + LuaJIT, mapped binary,
Go reservations, shared libs) so an operator can tell which part their own
deployment will grow.
Two runtime knobs are documented and now shipped by default in the desktop
build's core spawn (cmd/gui/main.js, overridable by exporting either variable):
* MALLOC_ARENA_MAX=2 — LuaJIT allocates through cgo into glibc malloc, and
glibc keeps up to 8*nproc per-thread arenas of ~1 MB that are never returned.
Measured 8-15 arenas (7-12 MB) -> 0.
* GOGC=50 — halves the Go heap target. Documented explicitly as useless ALONE
(measured 20.3 -> 21.5 MB, i.e. worse, because the saved heap is eaten by
more glibc arenas); only the pair cuts settled RSS, by ~19%.
Also corrects the binary size (8-12 MB, ~8 MB after the deploy script's -s -w)
and adds the elastic-pool / on-demand-log / self-healing-cooldown bullets that
README.md already had to README_EN.md.
28 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 源生产实例 ~40MB(实测分解与调优)
- 日志按需加载:审计日志不常驻内存——默认只加载首屏,下滚自动分页,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 | ~37–42 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 等无人值守场景。Linux 为
.tar.gz(二进制 +config.example.yamladapters/+ README),Windows 为.zip。
- Desktop(桌面版):Electron GUI 安装包,内嵌核心,适合个人桌面日常使用。
选择 GUI 还是纯后端
| 场景 | 推荐 | 理由 |
|---|---|---|
| 服务器 / 内网网关 / 无人值守常驻 | Headless(单二进制) | 极轻量(二进制 ~8MB;RSS 随源数量,单源 ~10MB、16 源 ~40MB,见内存占用),零依赖单进程,直接跑在 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 + 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镜像;已存在则直接复用。
# 全 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 类型
测试
go test -tags luajit ./...
覆盖:配置校验、适配器加载/变换、签名钩子、Gateway 鉴权、SDK 结算、SSE 流式、 模型路由、多模态透传与 disable_thinking。