Files
ModelRouter/README.md
2026-08-10 15:13:13 +08:00

301 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ModelRouter
> **English**: [README_EN.md](./README_EN.md)
面向多 llms 订阅者,部署在内网,实现一次配置多个服务共同使用的效果。支持指定模型或 auto 模式,按照配置的优先级选择可用模型提供服务。
![screenshot_20260810_134342_meow.liny.browser.cat.uwu.jpg](https://raw.gitcode.com/user-images/assets/10531124/ff574fa9-ceef-4e41-a3e7-fdf60faead04/screenshot_20260810_134342_meow.liny.browser.cat.uwu.jpg 'screenshot_20260810_134342_meow.liny.browser.cat.uwu.jpg')
![screenshot_20260810_122247_meow.liny.browser.cat.uwu.jpg](https://raw.gitcode.com/user-images/assets/10531124/cc703754-0a5b-4d89-8b0e-6922c9557ff3/screenshot_20260810_122247_meow.liny.browser.cat.uwu.jpg 'screenshot_20260810_122247_meow.liny.browser.cat.uwu.jpg')
> **轻量且增源无需重编译**:网关本体是单 Go 二进制(约 10MB零运行时依赖。新增/切换上游
> 只需在 `config.yaml`(或 WebUI加一个 `sources` 条目或挂一个 `.lua` 适配器——**不改 Go、
> 不重编译**。源与适配器的改动经 WebUI 提交时即时生效(热更新);直接编辑
> `config.yaml` 或 `adapter_dir` 下的 `.lua` 文件则需要重启进程生效。
## 核心优势
### 极致轻量
- **单二进制**:编译后约 10MB零运行时依赖仅依赖系统 libc部署即用
- **极低内存占用**:空闲状态仅 ~15MB RSS满载并发 100+ 请求时峰值 < 100MB
- **零运行时依赖** Go + LuaJIT 静态链接无需安装 Python/Node/Java 等运行时
- **启动极快**冷启动 < 200ms热重载配置 < 10ms
### 强大的多租户调度能力
- **多密钥多租户**支持无限密钥每个密钥独立角色模型范围Token 配额重置周期
- **AUTO 智能调度**基于优先级档位的分级调度同优先级源自动轮询负载均衡故障自动毫秒级故障转移
- **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 导出
- **AES-256-GCM 加密存储**运行时文件敏感字段加密落盘master.key 0600 权限支持环境变量注入主密钥
### 灵活的协议适配
- **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 DeepSeekQijiarOpenAIAnthropicGemini
GroqMistralOllamaKimiCode…)通过 **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 安全并发
- **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
cp config.example.yaml config.yaml # 编辑你的源与 key
GOMODCACHE=... GOPROXY=off go build -tags luajit -o llmsproxy ./cmd/llmsproxy
./llmsproxy -config config.yaml
```
> 依赖 [golua](https://github.com/aarzilli/golua)LuaJIT 绑定)。**必须**带 `-tags luajit`
> 构建,否则默认走内置超集 gopher-lua 路径(行为略有差异)。
```bash
# 无 key -> 401
curl http://127.0.0.1:8080/v1/models
# 单次
curl -H "Authorization: Bearer sk-gw-local-0001" \
-d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"hi"}]}' \
http://127.0.0.1:8080/v1/chat/completions
# 流式
curl -N -H "Authorization: Bearer sk-gw-local-0001" \
-d '{"model":"deepseek-v4-flash","stream":true,"messages":[{"role":"user","content":"hi"}]}' \
http://127.0.0.1:8080/v1/chat/completions
```
任何 OpenAI SDK `base_url` 指到网关地址`api_key` 用任意一个已创建并授权的网关 key 即可
## 配置
[`config.example.yaml`](config.example.yaml)核心字段
```yaml
listen: 127.0.0.1:8080 # 网关监听地址(建议绑内网/回环)
gateway_keys: [sk-gw-0001] # 初始 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
```
运行时文件`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 调度****由优先级页保存的规则持久化到 `runtime_file` `auto` 字段决定
源配置里的 `models[].priority` 数字不再参与调度也不再显示
- 每条规则 = 一个「槽位」:`{ model, source, tier, token_quota, period, hours }`
- `tier` 表示优先级档位同一档的模型并排共享该优先级档位从上到下递减
- 同一模型可配置多个槽位 A 源低配 B 源低配 A 源高配 B 源高配按档位顺延
- `token_quota` > 0 时该槽位在重置周期内用满即顺延到下一槽位;`period` 支持
`hour` / `week` / `month` / `nhour`(配合 `hours`),空 = 不限。
- 生图模型(`kind: image`)不参与 AUTO 链;生图走 `POST /v1/images/generations`
的独立路径。
### 源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` 路由到 ProviderProvider 调适配器
`transform_request` → HTTP 发送 → `transform_response` / `transform_stream_chunk`
WebUI 上的"新增/编辑源"、"上传 Lua 适配器"、"改 AUTO 优先级链"与"密钥模型范围"
都即时生效(写入运行时文件或 `adapter_dir` 后重新装配,无需重启);直接编辑
`config.yaml` / `adapter_dir` 下的文件则需要重启进程才会重新加载。
在 WebUI 删除源/适配器时运行时WebUI 创建)的源与 `.lua` 文件会真正移除;
`config.yaml` 中定义的基源无法改写配置文件,采用删除标记隐藏(重启后仍隐藏),
在 UI 里重新添加同名源即可恢复。
### 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}` 即可。
## 目录
```
cmd/llmsproxy # 入口
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。