# ModelRouter > **English**: [README_EN.md](./README_EN.md) 面向多 llms 订阅者,部署在内网,实现一次配置多个服务共同使用的效果。支持指定模型或 auto 模式,按照配置的优先级选择可用模型提供服务。 > **轻量且增源无需重编译**:网关本体是单 Go 二进制(约 10MB,零运行时依赖)。新增/切换上游 > 只需在 `config.yaml`(或 WebUI)加一个 `sources` 条目或挂一个 `.lua` 适配器——**不改 Go、 > 不重编译**。源与适配器的改动经 WebUI 提交时即时生效(热更新);直接编辑 > `config.yaml` 或 `adapter_dir` 下的 `.lua` 文件则需要重启进程生效。 ## 实现 统一 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 池,安全并发。 - **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]` 收尾。 ## 快速开始 ```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` 用 `gateway_keys` 之一即可。 ## 配置 见 [`config.example.yaml`](config.example.yaml)。核心字段: ```yaml listen: 127.0.0.1:8080 # 网关监听地址(建议绑内网/回环) gateway_keys: [sk-gw-0001] # 客户端访问网关的 key;留空=不鉴权 default_model: AUTO # model 无法路由时自动按 priority 选源 adapter_dir: adapters # Lua 适配目录,首启自动写入内置适配器 runtime_file: runtime.json # WebUI 编辑的源持久化到此文件 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:`。 - 主密钥来源:环境变量 `LLMS_PROXY_MASTER_KEY`(64 位 hex);否则读取 `runtime_file` 同目录的 `master.key`;都不存在时首次启动自动生成 `master.key`(0600)。 - 升级时旧明文文件自动兼容:首次运行正常读取,任何 UI 保存操作触发全文件加密迁移。 - 注意:**master.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` 路由到 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 里重新添加同名源即可恢复。 ### 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/.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。