面向多llms订阅者,部署在内网,实现一次配置多个服务共同使用的效果。支持指定模型或auto模式,按照配置的优先级选择可用模型提供服务
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
feat(types): 统一线格式 v2 最大集合 + 对外输出 golden 基线 Phase 1:内核定义市面所有主流协议字段的**并集**作为唯一内部格式, 适配器按需取用;内核不再携带任何协议形状。 - internal/types/maxwire.go:UnifiedRequest(21 字段,v1 的 10)、 UnifiedResult(11,v1 的 8)、UnifiedDelta。覆盖 openai/anthropic/ gemini/ollama/deepseek 各自必需字段(system 顶层、top_k、stop_sequences、 thinking 预算、parallel_tool_calls、response_format、safety/citations…)。 - 只带**语义**不带**容器**:generationConfig/systemInstruction/options/ num_predict/maxOutputTokens 等协议专属形状一律不进统一格式,由适配器 自行装填。测试 TestNoProtocolContainerLeak 钉死这条。 - 修掉 v1 的私有字段泄漏:disable_thinking/extra_body 曾是内核私有字段, 却被序列化进交给适配器的 body,导致 10 个适配器各自写 req.disable_thinking = nil 清理。v2 用 thinking 语义字段替代, TestNoKernelPrivateFieldLeak 为回归。 Phase 0:建立对外输出的**字节级** golden 基线(.snapshots/), 因为现有 e2e 只有 strings.Contains 宽松断言,无法证明 Phase 2 把 OpenAI wire 移交给适配器 serializer 后输出字节未变。 - e2e/wire_golden_test.go:非流式/流式/生图三个 golden, 比对前 scrub 掉 id 与 created(每请求变化),其余逐字节比对。 - 反向验证:注入 chat.completion.chunk.DRIFTED 漂移后 golden 确实失败 (证明它不是永远通过的空壳),随后完整恢复。 - 稳定性:连跑 3 次全绿。 验证:go test -tags luajit ./internal/types/ ./e2e/ -run 'TestMax|TestNo|TestWireGolden' 全绿。 | 25 天前 | |
chore(gui): 版本号 2.0.46(与内核同版本) (cherry picked from commit 02c01582f14cefaab6fefa8d9c959c9c42e07be0) | 15 天前 | |
fix(channels): 商汤扫码链路重建 + 双区登录 + 凭据过期口径 + 四条老化判据 ① 商汤(Raccoon)扫码「链接无效」——两个真根因 - 登录页 /login/mp 已被站点删除(Next 路由 404,实测只有 404 页); - 旧实现本地随机造 qrcode_code,而上游早已不认自造码:旧端点 /api/web/auth/v1/login_with_qrcode_code 对任意 code 恒回 pending (服务端没有该会话)⇒ 扫码永远完不成,账号一直是空凭据。 修法:改为上游现行 IAM 流程(逐行对齐其网页客户端 auth chunk)—— POST /api/iam/v1/auth/mp/qrcode 建会话 → 链接用 /login?code=…&appname=…&source=platform(实测 200 + 微信扫码面板) → 轮询 /api/iam/v1/auth/mp/qrcode/login(IAM code:"OK" 与旧 code:0 都认)。 服务端建码失败才退回旧本地码并打 warn。UI 对扫码流程改文案为 「用微信扫码」(原本文案会让人以为点开就自动登录)。 生产实测:链接 HTTP 200、轮询回 pending(会话真实存在)。 ② 双区登录(P7) - 凭据增加 region(intl|cn,缺省=历史行为);start_login 接受 region, 会话按区域选端点,完成登录写进凭据; - CodeBuddy:cn=copilot.tencent.com(platform=ide)/ intl=www.workbuddy.ai(platform=workbuddy-ai,X-Domain、 X-Product-Code、UA 按区域;实测 auth/state 回 200 信封); - Qoder:intl=openapi.qoder.sh+qoder.com(client_id e883ade2-…)/ cn=openapi.qoder.com.cn+qoder.cn(client_id 不同 732aef47-…,照参考 QODER_CN)。CN 无公开 chat 端点 ⇒ CN 只做登录/领取; - UI:buddy/qoder 详情页增加「+国际区」按钮。 ③ 凭据过期时刻的时区口径(P6 真根因) credentialExpiresAtMs 把 ISO 过期时刻按本地时区解析,而 codearts 给的是 UTC ISO(2026-10-19T16:42:43.06Z)⇒ 本机 +08 下过期时刻虚早 8 小时 ⇒ refresh_creds 每 30 分钟都判"已过期"并对华为云真打一次 STS(token 本还有 23 小时寿命)。改为按声明时区解析(Z / ±hh:mm / 无后缀=UTC 三种形态同一 epoch),补 TestCredentialExpiresAtMsParsesISOAsUTC。 ④ 四条老化/假红判据 - TestCreditsPoolReason*:account_add 触发的选号会用构造期默认余额 (0,0) 先查一次并缓存 60s ⇒ 之后改假上游读不到("余额 100 却报 exhausted")。 harness 改余额后清 TTL 缓存(__ttl_reset_probe,与生产同一实现)。 - TestAccountReorderActually... / TestPoolListOrderIsDeterministic:随 「一通道一源」失效(按 channels-buddy-<acct> 过滤恒得空)。改写为—— 重排必须切换服务账号(新增 __chosen_account_probe),顺序确定性改为断言 选号顺序(__rank_accounts_probe)由 id 稳定决定。 - channels.lua 顶层 local 余量:区域表与 5 份适配器源码常量收进 plugin.K (TestChannelsLuaHasLocalHeadroom 需要 ≥10 槽位);ZCode 适配器源码里的 CLI_PREFIX 必须留在源码内(适配器沙箱没有 plugin 全局)。 ⑤ 错误文案 openai_error 增补腾讯 codebuddy 三层嵌套 {error:{data:{code,msg}}}:实测 429 code=14018「额度已用尽」此前落到"不识别",客户端只看到通用文案。 验证:core 434s 全绿、lua 62s 全绿、provider 全绿;生产已部署并实测 raccoon 链接 200 / trae 非流式 200 / zcode 适配器加载。 (cherry picked from commit 662f8f02b6282d71d243ecd01a0bbfbcface88d4) | 16 天前 | |
chore(main): 回流 release/v2.0.x(v2.0.12)—— 内容对齐,main 无独有内容 按发布纪律回流。**注意这是内容对齐而非 merge**,理由与依据: 1. 纪律禁止 merge 回主干,因为 merge 会把发布分支的**版本号变更**带进 main。 本项目版本号**只由 git describe --tags 推导**(Makefile VERSION), 仓库里没有任何文件存放版本号 ⇒ 回流不存在版本污染风险。 2. 逐项核对确认 **main 是 release 的内容严格子集**: - main 独有的**文件**:0 个; - internal/lua/plugins.go / internal/core/kernel_actions.go 等内核文件 main 独有行数:0; - deploy.sh 那 14 行只是 emoji 清理(release 刻意去掉了 emoji); - channels.lua 那 333 行是**旧版内容**(旧 exports 表、旧模型名单)。 - main 独有的 9 个提交按 patch-id **全部已在 release 中等效存在** (git cherry 逐条列出均为 -,即已合入)。 3. 故回流 = 把 release 的内容带到 main,等价于"只捡拾修复、不带版本号", 且不会丢任何 main 独有的修复。 本次带回 v2.0.12 的全部内容(37 个提交): - 源串行 + 按模型节流(照 model-gate) - 已失效模型存储 + 远端目录闸门 - 余额分档选号 - 模型能力元数据(models.dev,isFree 权威判定) - **凭据续期修复**(此前恒报 ok:true 却一个请求都不发) - 通用件 TTL 缓存 + 串行锁;account_test/account_adopt/account_delete 修正 - 内核级:出站授权持久化重启后不生效、__llmsproxy_plugin_dir ground truth 校验:git diff release/v2.0.x = 0 行;go build -tags luajit ./... 通过。 | 21 天前 | |
test(retry): 钉住声明式重试判据与流式握手重试语义 - config:ShouldRetry/DelayDuration 表驱动(nil/次数=1/状态不匹配/body 不匹配/ 空子串/多信号/非法间隔) - provider:命中策略就重试直到 200、body 不匹配**不许**重试(400 也常用于真错误)、 流式只重试握手(首帧之后不重试,避免重复输出) (cherry picked from commit 0ae7d2c8be4e15c134d67594f127d39577916813) | 15 天前 | |
chore(main): 回流 —— VNC 兜底登录真正可用(放开 browser-stack 不需要的加固 + 修 3 个插件缺陷) 按纪律先在 release/v2.0.x 提交,再内容对齐回 main。 校验:git diff release/v2.0.x = 0 行。 | 21 天前 | |
chore(main): 回流 release/v2.0.x(v2.0.12)—— 内容对齐,main 无独有内容 按发布纪律回流。**注意这是内容对齐而非 merge**,理由与依据: 1. 纪律禁止 merge 回主干,因为 merge 会把发布分支的**版本号变更**带进 main。 本项目版本号**只由 git describe --tags 推导**(Makefile VERSION), 仓库里没有任何文件存放版本号 ⇒ 回流不存在版本污染风险。 2. 逐项核对确认 **main 是 release 的内容严格子集**: - main 独有的**文件**:0 个; - internal/lua/plugins.go / internal/core/kernel_actions.go 等内核文件 main 独有行数:0; - deploy.sh 那 14 行只是 emoji 清理(release 刻意去掉了 emoji); - channels.lua 那 333 行是**旧版内容**(旧 exports 表、旧模型名单)。 - main 独有的 9 个提交按 patch-id **全部已在 release 中等效存在** (git cherry 逐条列出均为 -,即已合入)。 3. 故回流 = 把 release 的内容带到 main,等价于"只捡拾修复、不带版本号", 且不会丢任何 main 独有的修复。 本次带回 v2.0.12 的全部内容(37 个提交): - 源串行 + 按模型节流(照 model-gate) - 已失效模型存储 + 远端目录闸门 - 余额分档选号 - 模型能力元数据(models.dev,isFree 权威判定) - **凭据续期修复**(此前恒报 ok:true 却一个请求都不发) - 通用件 TTL 缓存 + 串行锁;account_test/account_adopt/account_delete 修正 - 内核级:出站授权持久化重启后不生效、__llmsproxy_plugin_dir ground truth 校验:git diff release/v2.0.x = 0 行;go build -tags luajit ./... 通过。 | 21 天前 | |
fix(plugin): /api/ui-inject 的 stages 漏掉 chain_step + 忽略临时构建目录 部署到线上时用隔离实例(独立端口 18099 + 独立 config/runtime/adapter 目录) 发真实请求验证,抓到的第三个 bug。 ## bug:discovery 文档漏掉新 stage handlePluginUI 的响应里 stages 是**字面写死的三个**。加 chain_step 时只改了 lua.AllStages,没改这里,于是插件作者读 GET /api/ui-inject 会看到 ["request_start","routed","request_end"],**合理地得出结论:没有 chain_step 这个 stage**。stage 本身是注册好的、也确实在触发,只是没被声明。 改成从 lua.AllStages 派生——AllStages 是唯一定义顺序的地方,让它保持唯一。 ★ 而我原来的测试断言 len(view.Stages) != 3,**断言本身是 bug 的保护伞**: 它把"三个"固化成了期望值,于是新增第四个 stage 时测试全绿、bug 上线。 现在断言改为「与 AllStages 等长且逐项相同」,并显式要求 chain_step 在其中。 新增 stage 而忘了声明,这类问题会立刻红。 ## 顺带:.gitignore 补上临时构建目录 .probe/ 和 .build-work/ 是我调试时当 GOTMPDIR 和临时二进制用的,之前每轮 手工删,这轮差点提交进去 9.5MB 的二进制。 ## 部署验证留档 隔离实例跑真实 chat(158 prompt / 13 completion / 128 cache_hit),计费插件 算出 0.000688,与手算 (158-128)*1e-5 + 128*1e-5*0.1 + 13*2e-5 **逐位吻合**。 ★ 第一次手算我按全价算成 0.00184,一度以为插件算错了——查审计记录才看到 cache_hit_tokens。**算钱不对时先查输入再怀疑实现**,而我忘的恰是刚修的折扣。 ## 验证 351 个测试全绿;变异(stages 改回硬编码三个)被 TestUIInjectServesPluginUI 抓住,3 条断言同时红。 | 1 个月前 | |
fix(gui): 桌面版真正加载渠道插件 + 外链一律交给默认浏览器 ## ① 桌面版此前**完全没有插件系统**(渠道页根本不存在) GUI 内嵌内核的 config.yaml 由 main.js 生成,唯独漏了 plugin_dir —— 而内核刻意不给它默认值(运维显式开启)。后果:桌面版 plugin_dir: "", /api/plugins 返回空列表,channels 插件(多平台账号登录、凭据保管、自动 领取)在桌面端**完全不可见**。实测复现:同一份配置去掉 plugin_dir 就是 plugins: [],加上就有 billing/channels/quota/usage 四个。 修法: - 新 profile 直接写 plugin_dir: <profile>/plugins; - **老 profile 回填**(用户已有 config.yaml,不写就只有老用户永远没有插件)—— 新增 ensurePluginDirConfigured(),只在缺少该键时追加,其余内容不动; - 插件树由内核 SeedBundled 播种(按文件补齐,用户改过的副本优先保留)。 ## ② 升级后渠道修复**下发不到桌面用户** 内核播种规则是「文件缺失才写」,所以桌面用户升级 GUI 后 channels.lua 仍是旧版 —— 我们发的渠道修复对桌面用户静默失效。桌面版必须自己处理: 按 app 版本打戳(.plugin-seed-version),版本变化时删掉 4 个内置插件文件 让新内核重新播种,旧副本留 .prev-<旧版本> 可人工恢复;用户自建插件不碰。 逻辑抽到 cmd/gui/plugin-sync.js(无 Electron 依赖),配 6 条 node 单测。 ## ③ 登录一律拉起默认浏览器(三条路径都覆盖) 桌面版的内容区是加载内核 WebUI 的 iframe,渠道登录链接是 target=_blank。 此前只处理了 setWindowOpenHandler,另两条路径仍会把 Electron 窗口带到 第三方页面。现在三条路径统一外抛: 1. window.open / target=_blank → setWindowOpenHandler 2. 顶层同标签跳转 → will-navigate 3. iframe 内导航 → will-frame-navigate **loopback 例外必须保留**:channels 的服务端浏览器(noVNC)就在 127.0.0.1:<port> 的 iframe 里,拦掉它会让 VNC 登录失效。 实测(打包后的 GUI + Xvfb + CDP 驱动真实窗口): [gui] open in default browser: https://example.com/mr-windowopen-probe [gui] open in default browser: https://example.com/mr-navigate-probe [gui] open in default browser: https://example.com/mr-frame-probe [gui] open in default browser: https://xiaohuanxiong.com/login?code=…(真实商汤登录链接) 顶层 URL 保持在内核页面(跳转被拦下),loopback iframe 正常加载。 ## ④ 顺手 - GUI 版本号 1.9.7 → 2.0.42(与内核同版本发版;package-lock 里还是 1.9.3, 一并同步); - make gui-test 跑桌面壳单测。 验证:打包后的 GUI 启动即 /api/plugins 列出 channels(ui.page=channels), 插件目录播种 4 个内置插件;外链四类实测均走默认浏览器。 (cherry picked from commit 411503aea5eb3b78360c1929a45cf97daa2ce833) | 16 天前 | |
docs: 契约 v2 文档收尾(Phase 7) 用户要求的收尾。三份文档与代码对齐。 ## docs/lua-adapters.md - 新增 §4「出向序列化钩子(契约 v2)」:serialize_response / serialize_open_preamble / serialize_chunk / serialize_close / serialize_done_marker。核心语义:**声明即完全取代**内核的 OpenAI 兜底, 不是叠加 —— 适配器输出里不会有内核补的 OpenAI 字段(R2/R5)。 - §8 标题与内容从 disable_thinking 改为中立的 thinking 预算: {enabled, budget_tokens},由适配器映射到目标协议。v1 的 disable_thinking/extra_body 是内核细节泄漏进适配器请求体的典型 (迫使 10 个适配器各写一行清理),已记明已被取代。 - §7 最小适配器示例同步改成映射中立 thinking 字段。 ## docs/plugins.md - §5.4 定时任务(PLUGIN.schedule):every/at/jitter/timeout 字段表 + **内核 ticker 驱动、浏览器关着也跑** + 任务与钩子同一个 VM(写 plugin.state 立即可见于请求)+ 卡死则停用该插件(不能中断 Lua 的 能力边界)。 - §5.5 自定义对外挂载点:plugin.http 声明同时可在 /api/plugins/http/、 /plugins/、/v1/plugins/ 三处访问;命名空间约束与内核路由冲突拒绝。 - §5.6 运行时安装/更新适配器(token 轮换)+ 三条安全规则。 - §6.1 更正:某个卡住的钩子/任务只卡**它自己**那个插件的钩子(跨插件 并行),原文写的是"卡住所有插件"。 - §9 对比表补周期任务/对外端点两行,钩子列表补 serialize_*。 ## README - disable_thinking 章节 → thinking 预算(中立字段 + 各适配器映射)。 - 适配器协议示例补 serialize_* 出向钩子。 - 特性列表与测试覆盖描述同步。 | 24 天前 | |
chore(adapters): 移除非官方调用方式的适配器,保留官方适配器与模板 移除 4 个靠伪造客户端身份/绕过 WAF 才能工作的适配器,内核不再发行任何 非官方调用方式: - opencodezen / opencode / opencodego:靠 x-opencode-* 客户端指纹冒充官方 客户端;zen 免费池明确要求「free tier can only be used in OpenCode」 - agentrouter:伪造 Origin: qwen.ai + Referer + X-Requested-With 绕过阿里云 WAF 保留 12 个官方适配器(openai/deepseek/anthropic/gemini/github/groq/mistral/ ollama/kimicode/qwen38/sensenova/trae),其中 sensenova/qwen38 实测零伪装特征。 本地保留两处:adapters/_local_archive/(gitignored 非递归子目录,不加载) 与线上 /etc/llmsproxy/adapters/(adapter_dir 权威副本,cp -f 不删文件)。 测试同步:删除仅服务于被移除适配器的 11 个测试函数与 usage_cost_test.go; 改写 2 处借 opencode 之名的 provider 测试为用 openai 的 HTML 错误页压缩能力 验证同一契约(适配器钩子拥有错误格式、原文不外泄)。 验证:go test -tags luajit ./... 全绿;二进制内 x-opencode-client / QwenCode/0.2.0 / qwen.ai 等指纹字符串计数归零。 | 25 天前 | |
feat(zcode): 身份块适配器 + 修复调度器泄漏 + codearts 目录 + 备份裁剪泛化 ## 1. ★ zcode 身份块适配器(channels-zcode)—— 准入的唯一开关 参考实测(三账号 17 次请求):zcode 的 /zcode-plan/anthropic 通道做**请求体 内容检查**,system 缺官方身份块结构时直接 HTTP 405 + {"code":3012}: 无 system → 3012 ✗ 仅 cliPrefix(42 字符) → 3012 ✗ cliPrefix + stable(约 2898 字符)→ 200 ✓ ⇒ 判据是「身份块结构是否存在」,与 HTTP 头、版本号、频率、多轮历史、tools 声明、运行时全部无关。我们此前用通用 anthropic 适配器 ⇒ **即使拿到 zcode 账号也必然被拒**(现在 0 账号,但这是能不能用起来的前提)。 实现:插件自装 channels-zcode —— 读内核 anthropic.lua 执行取基座,**只覆盖 transform_request** 注入身份块(协议转换始终与 anthropic 那一份一致,不漂移): block[1] cliPrefix(42) / block[2] stable(2852) / block[3] Environment 段 / block[4] 调用方 system(**必须追加在最后**:上游看的是前缀结构) 只给最后一块打 cache_control(Anthropic 上限 4 断点,逐块打会占满预算) 日期块插入首条 user 的 content **数组**最前(拼进字符串会改变结构) 文本与参考**逐字节一致**(已核对:42 + 2856 = 2898)。 ★ 基座读不到时**响亮失败**,不静默退化 —— 否则会产出"身份块看着对、工具/图像 全没转换"的请求,上游可能 200 但模型拿不到工具定义,症状是"模型变笨了", 与准入毫不相干、排查完全跑偏。 判据 4 条 + 变异 2/2:不注入 → 报红;截断到 100 字符 → 报红。 另加"协议转换不退化"判据(工具 input_schema / 图像块存活)。 ## 2. ★★ 同一缺陷家族第 3 个实例:Close() 不停调度器 修完 on_unload 后全量回归 internal/lua **600s 超时**,goroutine 快照显示 **69 个卡在 stateSaver.loop 与 PluginRuntime.loop**。 根因:Runtime.Stop() **只在单插件 Unload 里调**,Close() 不调 ⇒ 每个插件的 调度循环在"进程关闭/测试结束"路径上永不停止。与 on_unload 完全同一形状。 修法:Close() 新增 stopSchedulersAll(),顺序 = 停调度 → 落盘 → on_unload (调度器必须停在落盘之前,否则它写一个已收尾的 state)。 ★ 顺带:Stop() 不幂等(裸 close 会 panic: close of closed channel), 两处都调之后可达 —— 用 sync.Once 加固。给函数加第二个调用点前先查幂等性。 实测 internal/lua 从 600s 超时 → 62s 通过。 ## 3. codearts 模型目录:补两个**必须 AK/SK 签名**的端点 codearts 有 10 个账号但目录里只有硬编码 1 个模型。参考打两个端点: ① opengw.../gateway/config → result.models(benefit) ② snap-access.../v1/model/builtin → builtinModels(常规) 而 refreshRemoteModels **只支持 Bearer** ⇒ 这两端点从来没被打过。 已加 ep.sign="codearts" 分支 + 两个解析形态 + opengw host 白名单。 判据:两端点各自真实命中 + 每请求带 SDK-HMAC-SHA256 头 + **合并**(非取其一)。 ## 4. TRAE PKCE 候选来源补齐(3 → 5) 参考 authCodeCandidates 有 5 个来源,我们只有 3(漏了 authCodeInfo 为 JSON 的 authCode 字段、以及**纯字符串**形态)。★ 且不能写成 a or b or c 链 —— jsonField 取不到时返回**空串**,Lua 里空串是真值,链会在第一个空串处短路。 判据 5 个参数化用例 + 变异 2/2 咬住(正是 or 链取不到的两个形态)。 ## 5. ★ 部署脚本:备份裁剪只修了一半("两处只修一处"再现) prune_adapter_backups 有完整实现(含数量硬上限),但**只针对 adapters.bak**; sync_plugins 同样每次建 plugins.bak.* —— 修了 adapters 的堆积、plugins 继续涨。 实测线上 plugins.bak **36 个**、/etc/llmsproxy 192M。 泛化为 prune_backups <prefix> + prune_all_backups(两个前缀都裁),加空前缀守卫。 判据不止逐字检查,而是**扫描脚本里所有 .bak.$(date) 创建点**,要求每个前缀都在 裁剪范围内 —— 将来新增第三种备份目录会立刻报红,而不是等它涨到几百兆。 变异 2/2 咬住(只裁 adapters / 去掉守卫)。 ## 6. 上游客观缺口钉成可执行判据(TestUpstreamGapsAreDocumented) qoder 加密端点需 .wasm(**参考仓库里也不存在**)、TRAE PKCE 参考**明确不支持**、 zcode 滑块需真实浏览器交互。写成判据:任何人(包括未来的我)想"补"这些能力, 必须先让判据失败 —— 即先推翻事实再动手。防的正是我前两轮的反复误判。 清理:channels-buddy.lua(0 字节历史遗留,每次启动报 must return a table)。 | 20 天前 | |
feat: LuaJIT worker-pool VM, multimodal/disable-thinking passthrough, WebUI redesign, DeepSeek V4 | 2 个月前 | |
feat: LuaJIT worker-pool VM, multimodal/disable-thinking passthrough, WebUI redesign, DeepSeek V4 | 2 个月前 |
文件内容可能有违规信息