Files
ModelRouter/internal/lua/plugins
JianFeeeee 8e2d384a7b feat(channels): Gemini 用插件自装的私有协议适配器(channels-gemini)
Cloud Code 凭据**不能**打公开的 /v1beta/models/{model}:generateContent。
参考 gemini.ts 点名三件必须保留,我们此前**一件都没有**(gemini.lua 产的是
公开的 contents/parts 裸体、一个身份头都不发,端点还指着公开路径):

1. **身份五头逐字写死、不许随机**,且**不带** x-goog-api-key /
   x-goog-api-client(User-Agent/x-client-name/x-client-version/
   x-machine-id/x-vscode-sessionid)
2. **流式刻意不带 Accept**(抓包一致)
3. **信封逐层字母序序列化**(上游按字节比对;Go 的 encoding/json 对 map
   自动字母序,而我们用 json.encode,故须自己排——实现 sorted_json)

信封形状(私有端点要的不是裸 contents):
  {model, project, request:{contents, systemInstruction, tools,
   generationConfig, sessionId}, requestId, userAgent}
响应也是信封(candidates 嵌在 response 里)→ 必须**剥一层**,否则下游
拿不到 candidates,表现为"模型没回话"(静默失败)。

另加前导助手 gemini_response / gemini_stream_chunk(candidates/parts/
usageMetadata → 统一信封;STOP→stop 等 finishReason 映射)。

★ 踩到的两个坑:
- **Lua 长字符串里不能有字面 NUL 字节**(Python heredoc 写的 "\0" 变成了真
  字节)→ Lua 报 "unfinished long string near '<eof>'",且报在文件末尾,
  完全指不到现场。
- **本 Lua 构建不支持按位异或 `~`**(unexpected symbol near '~')→ FNV-1a
  的 XOR 改用算术等价形式。参考自己明言 sessionId 的哈希本体未反推出来、
  数值不与原版逐字相同,所以这里要的是**确定性 + 依赖维度正确**
  (吃 project 与首条文本、不吃 model),不是数值一致。
- 另给三个自装适配器统一加**本地预编译护栏**:内核装载失败只回"源码无法
  编译"不给行号;先 load 一次就能拿到行号(上面两个坑都是靠它定位的)。

⛔ 仍不做:gemini-sigstore(签名持久化,268 行)——推理走无状态适配器,
插件侧缓存会成为死代码。理由已记在 docs/channels-parity.md。

判据(internal/lua/channels_gemini_adapter_test.go):走真实路径断言
身份五头逐值、**反向断言** x-goog-api-key/x-goog-api-client/Accept 不得出现、
信封五个顶层字段齐全、顶层键字母序(解析式校验,不是字符串匹配)、
响应剥信封后 content/finish_reason/token_usage 都到位。

回归:internal/{lua,core,provider,gateway} 全绿。
2026-10-13 23:42:04 +08:00
..