Files
ModelRouter/docs/lua-adapters.md

258 lines
8.9 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.

# Lua 适配器 API 文档
> **English**: [lua-adapters-en.md](./lua-adapters-en.md)
每个上游源source挂载一个 `.lua` 适配器,负责把统一 OpenAI 格式请求**转换**为上游原生
格式,并把上游响应/流式分块**转换**回统一格式。Go 层只负责调度、并发与透传——因此**新增
适配器、适配新协议,无需重编译 Go**。
适配器放两份(同构):
- 内置:`internal/lua/adapters/<name>.lua`(编译期 embed
- 可覆盖:配置 `adapter_dir` 目录下的同名脚本(优先级更高)
> **加载时机**:启动时由 `lua.NewVM(adapter_dir)` 一次性加载全部适配器;通过 WebUI
> 上传的适配器(`Core.UploadAdapter` → `vm.LoadAdapter`)与在线编辑的源配置即时生效,
> 无需重启。直接修改 `adapter_dir` 下的 `.lua` 文件需要重启进程才会重新加载。
## 目录
1. [脚本结构](#1-脚本结构)
2. [静态字段](#2-静态字段)
3. [转换钩子](#3-转换钩子)
4. [可选钩子](#4-可选钩子)
5. [内置辅助函数](#5-内置辅助函数)
6. [meta 约定](#6-meta-约定)
7. [一个完整的最小适配器](#7-一个完整的最小适配器)
8. [多模态与 disable_thinking](#8-多模态与-disable_thinking)
---
## 1. 脚本结构
脚本是一个返回 table 的 Lua 文件,必须 `return` 一个表:
```lua
local adapter = {}
adapter.name = "mysrc"
adapter.version = "1.0.0"
-- ... 字段与函数 ...
return adapter
```
脚本由 LuaJIT 解析golua 绑定cgo。每个适配器拥有**独立 VM + worker 池**,适配器之间
互不干扰;脚本中不允许跨 worker 共享可变状态(`log` 到 stdout 是唯一副作用)。
## 2. 静态字段
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `name` | string | 是 | 适配器名,用于 WebUI 展示 |
| `version` | string | 否 | 版本号,用于 WebUI 展示 |
| `endpoint` | string | 否 | 上游请求路径,默认 `/chat/completions`;可被 `source.endpoint` / `source.image_endpoint` 覆盖 |
| `headers` | table | 否 | 静态默认请求头;若未定义 `build_headers` 钩子则作为请求头回退 |
这些字段在加载时静态提取compile-once之后读它们不会占用池内 worker。
## 3. 转换钩子
### `transform_request(raw_body) -> string`
入参Go 层构造的统一 OpenAI 格式 JSON 字符串(`/v1/chat/completions` 请求体)。
返回:发送给上游的请求体字符串。
```lua
function adapter.transform_request(raw_body)
local ok, req = pcall(json.decode, raw_body)
if not ok then return raw_body end
req.model = "upstream-model-name" -- 改写模型名
req.stream = req.stream or false
return json.encode(req)
end
```
约定:
- 返回的字符串将被原样 POST 到 `base_url + (source.endpoint or adapter.endpoint)`
- 请求体中包含的 `disable_thinking``extra_body` 等网关内部字段,转换时应自行清理
`openai.lua` 即删掉这两个字段)。
### `transform_response(raw_body) -> string`
入参:上游非流式响应的原始 JSON 字符串。返回:统一格式 JSON 字符串。
统一格式字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `content` | string | 回复正文 |
| `finish_reason` | string | `stop` / `length` / `tool_calls` 等 |
| `reasoning_content` | string可选 | 推理内容DeepSeek 等) |
| `token_usage` | table | `{ prompt, completion, total }`tokens |
| `tool_calls` | array可选 | 工具调用:`{ id, type, name, arguments }``arguments` 为**已解码**的 table |
```lua
function adapter.transform_response(raw_body)
local ok, resp = pcall(json.decode, raw_body)
if not ok or resp == nil then return raw_body end
local unified = {
content = "",
finish_reason = "",
token_usage = { prompt = 0, completion = 0, total = 0 }
}
if type(resp.choices) == "table" and #resp.choices > 0 then
local ch = resp.choices[1]
unified.content = ch.message.content or ""
unified.finish_reason = ch.finish_reason or ""
if ch.message.reasoning_content then
unified.reasoning_content = ch.message.reasoning_content
end
end
return json.encode(unified)
end
```
### `transform_stream_chunk(raw_chunk) -> string`
入参SSE 流中每条 `data:` 的原始 JSON 字符串(不含 `data:` 前缀)。
返回:统一分块 JSON**返回 `""` 表示跳过该 chunk**。
统一分块格式:
| 字段 | 类型 | 说明 |
|------|------|------|
| `content` | string | 本次增量文本(可为 `""` |
| `done` | boolean可选 | `true` 表示流结束(对应 `finish_reason` 出现) |
```lua
function adapter.transform_stream_chunk(raw_chunk)
local ok, chunk = pcall(json.decode, raw_chunk)
if not ok then return "" end
if not chunk.choices or #chunk.choices == 0 then return "" end
local delta = chunk.choices[1].delta or {}
local fr = chunk.choices[1].finish_reason
return json.encode({
content = delta.content or "",
done = (fr ~= nil)
})
end
```
## 4. 可选钩子
### `build_headers(meta) -> table<string,string>`
动态生成/签名请求头(如 KimiCode 的 HMAC 签名。若脚本未定义此函数Go 层回退使用静态
`adapter.headers`(或 `source.headers`)。
```lua
function adapter.build_headers(meta)
local msg = meta.method .. meta.url .. meta.body
return {
["X-App-Sign"] = hmac_sha256_hex(meta.source.meta.app_secret, msg),
["X-Timestamp"] = meta.timestamp,
}
end
```
返回非 table 会报错;返回 `{}` 表示无自定义头(不会回退静态头)。
## 5. 内置辅助函数
所有适配器共享的全局函数(由 Go 注入):
| 函数 | 说明 |
|------|------|
| `json.encode(v)` | Lua 值 → JSON 字符串;失败返回 `"null"` |
| `json.decode(s)` | JSON 字符串 → Lua 值;失败返回 `nil` |
| `hmac_sha256_hex(key, data)` | HMAC-SHA256十六进制小写字符串 |
| `sha256_hex(data)` | SHA-256十六进制小写字符串 |
| `base64_encode(s)` | 标准 Base64 编码 |
| `tohex(s)` | 字节 → 十六进制小写字符串 |
| `log(level, msg)` | 打印 `[adapter/<name>] <msg>` 到 stdout |
另注:脚本由 LuaJIT 执行,标准库(`string`/`table`/`pcall` 等)完整可用,`os`/`io` 未暴露
(保持沙箱语义)。
## 6. meta 约定
`build_headers(meta)` 与请求转换共享的 meta 结构:
| 字段 | 说明 |
|------|------|
| `meta.url` | 完整请求 URL |
| `meta.method` | HTTP 方法(通常 `POST` |
| `meta.body` | 请求体字符串 |
| `meta.api_key` | 该源的 `api_key` |
| `meta.timestamp` | 请求时间戳 |
| `meta.source` | table`{ name, meta = { ... } }`,即 source 的 `meta` 字段(如 `app_secret` |
## 7. 一个完整的最小适配器
```lua
local adapter = {}
adapter.name = "mysrc"
adapter.version = "1.0.0"
adapter.endpoint = "/chat/completions"
adapter.headers = { ["X-Tenant"] = "prod" }
function adapter.transform_request(raw_body)
local ok, req = pcall(json.decode, raw_body)
if not ok then return raw_body end
req.disable_thinking = nil
req.extra_body = nil
return json.encode(req)
end
function adapter.transform_response(raw_body)
local ok, resp = pcall(json.decode, raw_body)
if not ok or resp == nil then return raw_body end
local unified = {
content = "",
finish_reason = "",
token_usage = { prompt = 0, completion = 0, total = 0 }
}
if type(resp.choices) == "table" and #resp.choices > 0 then
local ch = resp.choices[1]
unified.content = ch.message.content or ""
unified.finish_reason = ch.finish_reason or ""
end
return json.encode(unified)
end
function adapter.transform_stream_chunk(raw_chunk)
local ok, chunk = pcall(json.decode, raw_chunk)
if not ok then return "" end
if not chunk.choices or #chunk.choices == 0 then return "" end
return json.encode({
content = (chunk.choices[1].delta or {}).content or "",
done = (chunk.choices[1].finish_reason ~= nil)
})
end
return adapter
```
对应的 `config.yaml` source
```yaml
sources:
- name: mysrc
base_url: https://upstream.example.com
api_key: sk-xxx
adapter: mysrc # 或省略 = 内置同名适配器
endpoint: /chat/completions
models:
- id: my-model
priority: 50
kind: chat
```
## 8. 多模态与 disable_thinking
- **多模态**:请求的 `content` 若是数组(`[{type:"text"...},{type:"image_url"...}]`Go 层
原样透传给适配器。协议不支持的源Anthropic/Gemini/Ollama需在
`transform_request` 内转换;内置 `anthropic.lua`/`gemini.lua`/`ollama.lua` 已实现。
- **disable_thinking**:网关将请求体中的 `disable_thinking:true` 透传进
`transform_request`。DeepSeek 适配器将其映射为 `extra_body.thinking = { type = "disabled" }`
并清理自身字段;其余适配器按各自协议处理(可自行实现或忽略)。