package lua // 适配器共享前导(shared prelude) // // 问题:12 个 Lua 适配器里,**同一段代码被逐字复制多份**。实测(按钩子做 // 字节级哈希分组): // // transform_response github/groq/mistral 三份完全相同(2207B × 3) // transform_stream_chunk github/groq/kimicode/mistral 四份相同(2211B × 4) // transform_error deepseek/github/groq/kimicode 四份相同(293B × 4) // // 合计 12680 字节的**完全重复**;再加上"近似相同"的部分(所有 OpenAI 兼容 // 适配器的 transform_request 都要剥 v2 私有控制字段、transform_response 都要 // 组装同一个 unified 骨架),复制面更大。 // // openai.lua 自己的注释就写了这件事: // // (anthropic.lua carries the same helper; Lua adapters have no shared prelude.) // // —— safe_tool_id 在 openai.lua 与 anthropic.lua 各复制了一份。 // // 参考插件用 src/openai-compat.ts(1196 行)解决同一问题:12 个 adapter 里 // 8 个 import 它。我们没有对应物。 // // 做法:在**编译期把一份中性助手注入每个适配器脚本的前面**。这样: // // 1. 适配器源码不再复制粘贴,各平台只写**自己不同的那部分**; // 2. 注入的是**中立协议助手**(剥内核私有字段、统一信封骨架、SSE 归类), // 不含任何厂商名 —— 守住"内核零厂商名"纪律; // 3. 已经自己实现了钩子的适配器**继续能跑**(前导只是往全局塞助手, // 旧适配器不引用它们,行为逐字节不变)。 // // ⚠️ 为什么注入而不是让适配器 require:适配器脚本是**独立编译**的(各自的 // golua 状态),没有模块系统(见 setupGlobals —— 只注册基础原语,没有 // package/require)。参考插件的 import 靠宿主打包器,我们没有那一层。 // adapterPreludeSrc 是注入每个适配器脚本前的中性助手库。 // // 纪律:这里**不得**出现任何厂商名(openai 作为协议名是允许的 —— 它是中立的 // 行业协议,不是"某厂商")。平台差异留在各自的适配器文件里。 const adapterPreludeSrc = ` -- ── 共享前导(内核注入,勿在各适配器里重复定义)────────────── -- -- 这些助手只做**协议层面的中立清理与组装**,不含任何平台专属规则。 -- 单个适配器可以覆盖同名全局(后定义者胜),但通常不该这么做 —— -- 有差异时请在适配器里调用 ` + "`__prelude.`" + ` 拿到中性实现再改。 local __prelude = {} -- prelude.decode_body:宽松 JSON 解码。失败返回 nil(调用方回退原 body)。 -- 为什么不 pcall 在调用点展开:各适配器写法不一(pcall 的返回值形态、 -- 是否判 nil / type),统一成一处后行为一致。 function __prelude.decode_body(raw) if type(raw) ~= "string" or raw == "" then return nil end local ok, rec = pcall(json.decode, raw) if not ok or type(rec) ~= "table" then return nil end return rec end -- prelude.strip_control(req):剥掉内核私有控制字段。 -- -- 为什么是**内核**该管的事:这些字段(thinking / extra_body 的旧私有形态) -- 是网关→适配器之间的**内部约定**,不是厂商概念。v2 改成中立 thinking 字段 -- 承载意图后,这批"删掉旧私有字段"的代码被复制进了每个适配器 —— 于是内核 -- 再改一次约定就要改 12 个文件。集中在这里,约定变化只改一处。 -- -- 各适配器仍可自行 req.thinking = nil(上游不支持思考时),那是**厂商语义**, -- 留在各自文件里是对的;这里只负责"私有字段不该出现"。 function __prelude.strip_control(req) if type(req) ~= "table" then return req end req.extra_body = nil if type(req.messages) == "table" then for _, msg in ipairs(req.messages) do if type(msg) == "table" then msg.reasoning_content = nil msg.reasoning = nil end end end return req end -- prelude.unified_skeleton():统一的响应信封骨架。 -- -- 内核下游只认这一个形状(content / finish_reason / token_usage / -- tool_calls / reasoning)。空骨架集中定义,避免每个适配器各写一遍 -- (写法不一会在下游产生"某些字段缺失"的隐蔽差异)。 function __prelude.unified_skeleton() return { content = "", reasoning = "", finish_reason = "", token_usage = { prompt = 0, completion = 0, total = 0 }, } end -- prelude.finish_reason(s):把上游的结束原因映射到中立取值。 -- 只处理**协议级**同义词;厂商特有的取值映射留在各自适配器。 function __prelude.finish_reason(s) if type(s) ~= "string" or s == "" then return "" end local t = string.lower(s) if t == "stop" or t == "end_turn" then return "stop" end if t == "length" or t == "max_tokens" then return "length" end if t == "tool_calls" or t == "tool_use" or t == "function_call" then return "tool_calls" end if t == "content_filter" then return "content_filter" end return s end -- prelude.usage(rec):从 OpenAI 风格的 usage 块取计数(含 cache 细分)。 -- 各上游把缓存命中放在不同字段(prompt_cache_hit_tokens / -- prompt_tokens_details.cached_tokens / cache_read_input_tokens), -- 这是**协议族的已知变体**,不属于任何单一厂商。 function __prelude.usage(rec) local out = { prompt = 0, completion = 0, total = 0 } if type(rec) ~= "table" then return out end local u = rec.usage or rec.token_usage or rec if type(u) ~= "table" then return out end out.prompt = tonumber(u.prompt_tokens or u.input_tokens or u.prompt) or 0 out.completion = tonumber(u.completion_tokens or u.output_tokens or u.completion) or 0 out.total = tonumber(u.total_tokens or u.total) or (out.prompt + out.completion) local hit = tonumber(u.prompt_cache_hit_tokens or (type(u.prompt_tokens_details) == "table" and u.prompt_tokens_details.cached_tokens) or u.cache_read_input_tokens) or 0 local miss = tonumber(u.prompt_cache_miss_tokens) or 0 if hit > 0 or miss > 0 then out.cache_hit = hit out.cache_miss = miss end return out end -- prelude.choices(rec):取 choices 数组的首元素(OpenAI 兼容族的统一位置)。 function __prelude.choices(rec) if type(rec) ~= "table" then return nil end local c = rec.choices if type(c) ~= "table" or #c == 0 then return nil end return c[1] end -- prelude.tool_id(id):把工具调用 id 规整到 Anthropic 可接受的形式 -- ^[a-zA-Z0-9_-]{1,64}$。 -- -- 为什么在共享层:本网关是**扇出路由器** —— 一个 OpenAI 兼容模型产生 -- "bash:0" 这样的 id 会被回放到每个 Claude 上游,一个坏 id 就能让整条 -- AUTO 链路失败。这是**路由器的性质**,不是某个厂商的规则,所以归前导。 function __prelude.tool_id(id) if type(id) ~= "string" or id == "" then return id end local clean = string.gsub(id, "[^A-Za-z0-9_-]", "_") if clean == id and #clean <= 64 then return clean end -- 保留原 id 的 8 位摘要,避免不同 id 坍缩成同一个 --("a:b" 与 "a_b" 会归一化成同一个,导致工具调用失配) local digest = string.sub(sha256_hex(id), 1, 8) local room = 64 - 1 - 8 if #clean > room then clean = string.sub(clean, 1, room) end return clean .. "_" .. digest end -- prelude.derive_key:由 base 派生一个稳定的、长度受限的标识(如 client_session)。 function __prelude.derive_key(base, n) n = n or 32 return string.sub(sha256_hex(tostring(base or "")), 1, n) end _G.__prelude = __prelude -- ── 共享前导结束 ──────────────────────────────────────────── ` // withAdapterPrelude 把共享前导拼在适配器源码之前。 // // 为什么要拼到**源码字符串**而不是运行时注入全局:适配器是通过 inspectScript // 和 worker 两条路径各自 DoString 编译的,拼源码是唯一能同时覆盖两条路径、 // 且不改变 Lua 作用域语义的做法(运行时 SetGlobal 会在热重载/池重建时丢失)。 func withAdapterPrelude(code string) string { return adapterPreludeSrc + code }