Files
ModelRouter/README.md

176 lines
7.5 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
面向多 llms 订阅者,部署在内网,实现一次配置多个服务共同使用的效果。支持指定模型或 auto 模式,按照配置的优先级选择可用模型提供服务。
## 实现
统一 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` 时按各源模型的 `priority` 自动选最高可用源。
- **统一输出**:所有源对外都是 OpenAI 格式(含 `reasoning_content``tool_calls``usage`)。
- **多模态**`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**:内置管理界面,可在线查看/新增/编辑上游源与模型,写入运行时文件持久化。
- **鉴权**:网关自身用 `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-...
adapter: deepseek
max_concurrent: 8
models:
- id: deepseek-v4-flash
priority: 100 # 越大越优先被 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
```
### 模型路由
`/v1/chat/completions``model` 解析顺序:
1. `source/model``source:model` 前缀 → 指定源;
2. 精确匹配某个源的 `model`
3. 配置 `default_model: AUTO` 时 → 按各源模型的 `priority`(数字大优先)选最高可用源;
4. 否则回落到 `default_source`
任何 OpenAI 客户端,只要 `model` 设为某个源的 `name/任意名`,即可锁定走该源;
设为 `AUTO`(或网关配了 `default_model: AUTO`)即自动按优先级选源。
### disable_thinking
请求体带 `"disable_thinking": true`网关透传给各适配器DeepSeek 适配器将其
映射为 `extra_body.thinking.type = "disabled"` 关闭推理,其余源按各自协议处理。
### WebUI
内置管理界面(`GET /`),登录后可在浏览器查看/新增/编辑上游源与模型,
改动写入 `runtime_file`(重启仍生效)。
## Lua 适配器协议
每个适配器是一个返回 table 的 Lua 脚本(`internal/lua/adapters/<name>.lua`
加载时可被 `adapter_dir` 下的同名脚本覆盖。
```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。