diff --git a/internal/core/channels_trae_catalog_test.go b/internal/core/channels_trae_catalog_test.go index 4644c6b..915668a 100644 --- a/internal/core/channels_trae_catalog_test.go +++ b/internal/core/channels_trae_catalog_test.go @@ -120,23 +120,23 @@ func newTraeCatalogHarness(t *testing.T, onBody func(string)) *traeCatalogHarnes // ① solo_agent:glm-5.2 有档位;另有三个必被硬性过滤的条目 `{"function":"solo_agent","config_info_list":[`+ `{"config_name":"glm-5.2","usage":"chat_completion","config_switch":true,`+ - `"reasoning_effort_config":{"support_thinking":true,"options":["high","low"]}},`+ + `"reasoning_effort_config":{"support_thinking":true,"options":["high","low"]}}`+ + `]},`+ + // ② solo_work_lite 排在**后面**:glm-5.2 在这里**没有**档位配置。 + // 另三个"必被过滤"的条目也放这里 —— 生产里适配器恒发这条通道, + // 硬性过滤的判据必须在这条通道上被验证到(放 solo_agent 会因为 + // admit_functions 策略先行剔除,测试就测不到过滤逻辑了)。 + `{"function":"solo_work_lite","config_info_list":[`+ + `{"config_name":"glm-5.2","usage":"chat_completion","config_switch":true},`+ `{"config_name":"summary-model","usage":"summary","config_switch":true},`+ `{"config_name":"disabled-model","usage":"chat_completion","config_switch":false},`+ `{"config_name":"hidden-model","usage":"chat_completion","config_switch":true,"is_invisible_to_user":true},`+ `{"config_name":"no-function-model","usage":"chat_completion","config_switch":true},`+ - // is_custom_model=true ⇒ 上游以流内 4001 拒绝("仅可见但不可调用") `{"config_name":"custom-flag-model","usage":"chat_completion","config_switch":true,`+ `"display_config":{"is_custom_model":true}},`+ - // 对照组:名字带 custom_model_ 前缀但**标志为 false** ⇒ 必须**保留** - // (参考点名:判据是标志的值,不是模型名) `{"config_name":"custom_model_prefix_but_ok","usage":"chat_completion","config_switch":true,`+ `"display_config":{"is_custom_model":false}}`+ `]},`+ - // ② solo_work_lite 排在**后面**:glm-5.2 在这里**没有**档位配置 - `{"function":"solo_work_lite","config_info_list":[`+ - `{"config_name":"glm-5.2","usage":"chat_completion","config_switch":true}`+ - `]},`+ // ③ 白名单外通道:整组必须被丢弃 `{"function":"inline_chat","config_info_list":[`+ `{"config_name":"only-in-bad-channel","usage":"chat_completion","config_switch":true}`+ diff --git a/internal/lua/plugins/channels.lua b/internal/lua/plugins/channels.lua index 263aab5..bcfe41f 100644 --- a/internal/lua/plugins/channels.lua +++ b/internal/lua/plugins/channels.lua @@ -3780,6 +3780,23 @@ local function effectiveModels(provider) -- 进源只会让客户端选中后报错(trae 的 custom_model_* 槽位就是这类)。 local unavailable = {} for _, m in ipairs(plat.models_unavailable or {}) do unavailable[m] = true end + -- ★ admit_functions:只放行**本适配器真的会发**的通道里的模型。 + -- + -- 白名单(parse 里的 rankOf)只说明"该通道在本端点下可调用",不等于 + -- "适配器会发它"。trae 适配器恒发 solo_work_lite,别的通道的模型经我们 + -- 这条路发出去要么回空内容、要么被上游悄悄回落到默认模型(实测 + -- kimi-k2.8-preview / deepseek-V3 空回,sagitta 自称 Claude、aquila 自称 + -- GPT)—— 那正是用户说的"模型不正确"。 + -- + -- 放在这里而不是解析器里,是因为解析器还要负责**元数据合并**:同一模型在 + -- solo_agent 里有思考档位、在 solo_work_lite 里没有,合并必须跨通道进行 + -- (否则模型显示"未提供推理等级")。策略只决定"哪些 id 对外露出"。 + local admit = nil + local mrAdmit = plat.models_remote and plat.models_remote.admit_functions + if type(mrAdmit) == "table" then + admit = {} + for _, f in ipairs(mrAdmit) do admit[f] = true end + end local remote = plugin.state.remoteModels and plugin.state.remoteModels[provider] local out, seen = {}, {} -- 远端在前(权威顺序) @@ -3804,9 +3821,24 @@ local function effectiveModels(provider) -- ⚠️ 只剔这里记录过的(真正 404 过的),**绝不**拿远端目录反推下架 —— -- buddy/qoder 那类平台的远端目录残缺,反推会删掉可用模型。 local dead = deadModelIds(provider) + local metaAll = plugin.state.modelMeta and plugin.state.modelMeta[provider] or {} local kept = {} for _, id in ipairs(out) do - if not dead[id] and not unavailable[id] then kept[#kept+1] = id end + local keep = (not dead[id]) and (not unavailable[id]) + if keep and admit then + local e = metaAll[id] + -- 有目录元数据 ⇒ 按它声明的通道判;没有元数据(兜底/手工源)⇒ 不拦, + -- 因为"目录没说"不等于"不可调用"。 + if type(e) == "table" and type(e.chans) == "table" then + keep = false + for _, c in ipairs(e.chans) do + if admit[c] then keep = true break end + end + elseif type(e) == "table" and type(e.chan) == "string" and e.chan ~= "" then + keep = admit[e.chan] == true + end + end + if keep then kept[#kept+1] = id end end return kept end @@ -4103,22 +4135,20 @@ local function parseRemoteModels(kind, body, provider) local groups = rec.function_configs or rec.functionConfigs if type(groups) == "table" then local byId, chosenRank, order = {}, {}, {} + -- chansOf[id]:该 id 在**哪些通道**里出现过(合并只留一条元数据,但 + -- "在哪些通道可发"是策略判定要用的信息 —— admit_functions 不能只看 + -- 合并后那条的 chan:同模型常在有档位的 solo_agent 与我们要发的 + -- solo_work_lite 同时出现,合并会选前者,导致误判"不可发")。 + local chansOf = {} for _, g in ipairs(groups) do if type(g) == "table" then local chan = rstr(g, "function", "Function") -- 准入:未声明 function 或不在白名单 → 整组丢弃 - -- ★ 追加一道「本适配器可发通道」闸门(声明式,见 models_remote.admit_functions): - -- 白名单只说明"这个通道在本适配器的推理端点下可调用",不代表 - -- **适配器会发它** —— trae 适配器恒发 solo_work_lite,别的通道的 - -- 模型即使可调用,经我们这条路发出去也会空回/回落默认模型。 - local admitted = nil - do - local pl = platformById(provider) - local mr2 = pl and pl.models_remote - local af = mr2 and mr2.admit_functions - if type(af) == "table" then admitted = {} for _, f in ipairs(af) do admitted[f] = true end end - end - if admitted and chan and not admitted[chan] then chan = nil end + -- + -- ⚠️ 「白名单」只回答"该通道在本适配器的推理端点下**可调用**"; + -- "适配器会不会发这条通道"是另一回事,由 effectiveModels 的 + -- models_remote.admit_functions 过滤(那是产品策略,不该写进解析器: + -- 解析器的职责是把目录如实解出来,含元数据合并)。 if chan and rankOf[chan] then local list = g.config_info_list or g.ConfigInfoList if type(list) == "table" then @@ -4134,6 +4164,8 @@ local function parseRemoteModels(kind, body, provider) -- is_custom_model: 仅可见但不可调用(上游流内 4001 拒绝) if e.custom == true then ok = false end if ok then + chansOf[e.id] = chansOf[e.id] or {} + chansOf[e.id][chan] = true local inc = byId[e.id] if inc == nil then byId[e.id] = e @@ -4169,7 +4201,14 @@ local function parseRemoteModels(kind, body, provider) -- 上层只能显示"未提供推理等级")。 if not plugin.state.modelMeta then plugin.state.modelMeta = {} end plugin.state.modelMeta[provider] = plugin.state.modelMeta[provider] or {} - plugin.state.modelMeta[provider][id] = byId[id] + local entry = byId[id] + if type(entry) == "table" and chansOf[id] then + local list = {} + for c in pairs(chansOf[id]) do list[#list+1] = c end + table.sort(list) + entry.chans = list + end + plugin.state.modelMeta[provider][id] = entry end end