# Lua 适配器 API 文档 > **English**: [lua-adapters-en.md](./lua-adapters-en.md) 每个上游源(source)挂载一个 `.lua` 适配器,负责把统一 OpenAI 格式请求**转换**为上游原生 格式,并把上游响应/流式分块**转换**回统一格式。Go 层只负责调度、并发与透传——因此**新增 适配器、适配新协议,无需重编译 Go**。 适配器放两份(同构): - 内置:`internal/lua/adapters/.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` 钩子则作为请求头回退 | | `transform_error(status, body)` | function | 否 | 错误响应收敛:返回一行短原因;返回 nil / 未定义时客户端统一收到 `unknown error`(原始响应体只进服务端日志) | 这些字段在加载时静态提取(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. 可选钩子 ### `transform_error(status, body) -> string | nil` 把该源特有的错误响应收敛成一行短原因。每个上游的错误格式不同——这是适配器层 的职责:内置各适配器均实现了自己的信封解析(如 zen 的 `{error={type,message}}`、Anthropic 的 `{type="error",error={...}}`、Gemini 的 `{error={code,message,status}}`、Ollama 的字符串 `{error="..."}` 等)。 未定义本钩子或返回 nil 时,核心不猜测格式,客户端统一收到 `api error : unknown error`,原始响应体仅记录在服务端日志。 ```lua function adapter.transform_error(status, body) local ok, resp = pcall(json.decode, body) if not ok or type(resp) ~= "table" then return nil end if resp.error and resp.error.type == "FreeUsageLimitError" then return "zen free pool quota exhausted" end return resp.error and resp.error.message or nil end ``` ### `build_headers(meta) -> table` 动态生成/签名请求头(如 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/] ` 到 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" }` 并清理自身字段;其余适配器按各自协议处理(可自行实现或忽略)。