Compare commits

...

1 Commits

Author SHA1 Message Date
47fe759025 fix(channels): buddy 目录过滤不可调用模型(照参考 isChatModel 四类判据)
## 真实缺陷
`/v3/config` 的 `data.models` 混着**不是对话模型**的条目,我们原样全收 ⇒
生产源里 24 个模型,其中 `nes-gf` / `codewise-completions` / `codewise-rewrite` /
`codewise-jump` / `codewise-default-model-v2` / `hunyuan-image-alpha` / `auto`
全部调不通(`glm-5.1` 也 404)。用户选中报错,而错误文案指向**模型**
⇒ 极易被误判成"这个模型坏了"。

参考实现(buddy.ts `isChatModel` / `isAutoSelectAlias`)逐条给出四类判据,
且都有实测依据;本轮逐条移植:
  ① 自动选择别名 `auto` / `default`
     ⚠️ **只按这两个字面量**过滤,不做前缀匹配 —— 前缀会误伤
     `default-model` / `fast-model`(国际版抽象别名,但被 craft agent 引用,
     是官方推荐入口)。参考注释专门警告过这点。
  ② 补全/NES 专用:前缀 `nes-` / `completion-` / `codewise-`,或 `supportsExtra`
     (参考实测 codewise-default-model-v2 → `code 11102 service info not found`)
  ③ `maxOutputTokens > 0 且 ≤ 256`(补全用途;对话模型普遍 ≥24000)
  ④ 带 `text-to-image` 标签(如 hunyuan-image-alpha)

⇒ 故 `models` 先建成 id→meta 映射(③④ 需要元数据),再逐条判定。
同时收 `data.agents[].models` 里**被 agent 引用**的清单并合并 —— 参考据此给
`agentReferenced` 打标(`hy4-preview-f` 只由本端点下发且被 agent 引用,
白名单式重建会把它丢掉)。

## 判据(1 条 + 变异 1/1)
`TestBuddyCatalogFilterDropsNonChatModels`:断言四类规则**都在**源码里
("有人精简掉一条"就会把不可调用模型放回选择器),并**反向**断言
`default` 没被写成前缀匹配(那会误伤 `default-model`)。
变异:删掉 `nes-` 规则 → 报红。

## 说明
这是上一提交(v2.0.30)同一轮的第二个发现:v2.0.30 修掉 URL 拼接后,
buddy 通道**才真正可达**,于是"可达之后目录里哪些真能调"这件事第一次暴露出来。
两个缺陷是串联的 —— 不先修 URL,这个目录问题无法被观察到。
2026-10-16 14:06:51 +08:00
2 changed files with 130 additions and 10 deletions

View File

@ -0,0 +1,57 @@
package core
import (
"strings"
"testing"
)
// ★ 远端模型目录混着「不可调用」的条目,必须过滤掉。
//
// 真实缺陷(生产实测):buddy 源声明了 24 个模型,其中
//
// nes-gf / codewise-completions / codewise-rewrite / codewise-jump /
// codewise-default-model-v2 / hunyuan-image-alpha / auto
//
// 全部调不通(`glm-5.1` 也是 404)。用户选中就报错,而错误文案指向**模型**,
// 极易被误判成"这个模型坏了"。
//
// 参考实现(buddy.ts 的 isChatModel / isAutoSelectAlias)记录了**四类**判据,
// 且都是实测依据:
//
// ① 自动选择别名 auto/default(**只按字面量**,前缀匹配会误伤 default-model)
// ② 补全/NES 专用前缀 nes- / completion- / codewise-,或 supportsExtra=true
// (参考实测 codewise-default-model-v2 → `code 11102 service info not found`)
// ③ maxOutputTokens >0 且 ≤256(补全用途;对话模型普遍 ≥24000)
// ④ 带 text-to-image 标签(如 hunyuan-image-alpha)
func TestBuddyCatalogFilterDropsNonChatModels(t *testing.T) {
src, err := readBundledChannels(t)
if err != nil {
t.Fatal(err)
}
s := string(src)
// 判据一:四类规则必须都在源码里(防止有人"精简"掉其中一条)
for _, rule := range []string{
`id == "auto" or id == "default"`, // ①
`id:sub(1, 4) == "nes-"`, // ②
`id:sub(1, 11) == "completion-"`, // ②
`id:sub(1, 9) == "codewise-"`, // ②
`m.supportsExtra == true`, // ②
`mo > 0 and mo <= 256`, // ③
`t == "text-to-image"`, // ④
} {
if !strings.Contains(s, rule) {
t.Errorf("buddy 目录过滤缺少规则:%s\n"+
"(这四类都是参考用真实请求验证过的「列出来就会让用户报错」的条目,"+
"删任何一条都会把不可调用模型放回选择器)", rule)
}
}
// 判据二:`default-model` / `fast-model` 必须**不在**被过滤的形态里。
// 参考注释明确警告:只过滤 auto/default 两个字面量,不做前缀匹配 ——
// 前缀会误伤这两个国际版别名(它们被 craft agent 引用,是官方推荐入口)。
if strings.Contains(s, `id:sub(1, 7) == "default"`) {
t.Error("buddy 过滤把 default 做成了**前缀**匹配 ⇒ 会误伤 default-model(参考点名警告)")
}
t.Log("✓ buddy 目录过滤四类规则齐备,且未误伤 default-model 类别名")
}

View File

@ -3216,17 +3216,80 @@ local function parseRemoteModels(kind, body, provider)
end
end
elseif kind == "buddy_config" then
-- 照参考 parseModelsFromConfig:结果在 data.models / data.agents 结构里。
-- 多形态兜底(顶层 data / 直接 models),因为该端点历史上变过形状。
local d = rec.data or rec
local arr = d.models or d.agents or rec.models
if type(arr) == "table" then
for _, m in ipairs(arr) do
local id = type(m) == "table" and (m.id or m.modelId or m.name)
if type(id) == "string" and id ~= "" then out[#out+1] = id end
elseif kind == "buddy_config" then
-- 照参考 parseModelsFromConfig:结果在 data.models / data.agents 结构里。
-- 多形态兜底(顶层 data / 直接 models),因为该端点历史上变过形状。
--
-- ★★ 必须过滤「不可调用」的条目 —— 远端 data.models 里混着三类**不是对话
-- 模型**的东西,列进模型选择器只会让用户选中后报错(实测生产:glm-5.1
-- 与 glm-5.2 之外,codewise-* / nes-gf / hunyuan-image-alpha 同样调不通)。
-- 判据与命名逐条照参考 buddy.ts 的 isChatModel / isAutoSelectAlias:
-- ① 自动选择类内部别名:`auto` / `default`
-- ⚠️ 只按**这两个字面量**过滤,不做前缀匹配 —— 前缀会误伤
-- `default-model` / `fast-model`(国际版抽象别名,但被 craft agent
-- 引用,是官方推荐入口)。
-- ② 补全/NES 专用:前缀 `nes-` / `completion-` / `codewise-`,
-- 或带 `supportsExtra` 标记。参考实测 codewise-default-model-v2
-- 返回 `code 11102 model service info not found`(后端未开放)。
-- ③ 输出上限过小(>0 且 ≤256):补全用途,对话模型普遍 ≥24000。
-- ④ 带 `text-to-image` 标签的生成式模型(如 hunyuan-image-alpha)。
-- ⚠️ ③④ 需要元数据,故先把 models 建成 id→meta 映射。
local d = rec.data or rec
local arr = d.models or d.agents or rec.models
local metaById = {}
if type(arr) == "table" then
for _, m in ipairs(arr) do
if type(m) == "table" and type(m.id) == "string" and m.id ~= "" then
metaById[m.id] = m
end
end
end
end
-- 被 agent 引用的模型(craft/ask/plan/cli)。参考据此给 `agentReferenced`
-- 打标:`hy4-preview-f` 只由本端点下发且被 agent 引用,白名单重建会丢掉它。
local agentRef = {}
if type(d.agents) == "table" then
for _, ag in ipairs(d.agents) do
if type(ag) == "table" and type(ag.models) == "table" then
for _, mid in ipairs(ag.models) do
if type(mid) == "string" then agentRef[mid] = true end
end
end
end
end
local function buddyIsChatModel(id)
if id == "auto" or id == "default" then return false end
if id:sub(1, 4) == "nes-" or id:sub(1, 11) == "completion-"
or id:sub(1, 9) == "codewise-" then return false end
local m = metaById[id]
if type(m) == "table" then
if m.supportsExtra == true then return false end
local mo = tonumber(m.maxOutputTokens)
if mo and mo > 0 and mo <= 256 then return false end
if type(m.tags) == "table" then
for _, t in ipairs(m.tags) do
if t == "text-to-image" then return false end
end
end
end
return true
end
local seen = {}
local function pushId(id)
if type(id) ~= "string" or id == "" then return end
if seen[id] or not buddyIsChatModel(id) then return end
seen[id] = true
out[#out + 1] = id
end
-- models 与 agents 两处都收(agents 里是被引用的清单,可能含远端
-- data.models 没有的新变体)。models 优先,保持顺序权威。
if type(arr) == "table" then
for _, m in ipairs(arr) do
local id = type(m) == "table" and (m.id or m.modelId or m.name) or nil
if type(id) == "string" then pushId(id) end
end
end
for id in pairs(agentRef) do pushId(id) end
elseif kind == "codearts_builtin" then
-- 照参考 fetchCodeArtsRemoteModels 的第 2 个端点:字段是 builtinModels