mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
fix(channels): trae 模型表去掉不可调用槽位 + opencode 免费模型按实测收敛
## ① trae:模型选择器里那几个 `custom_model_*` 是**槽位不是模型**
用户报「trae 的模型不正确 / 渠道不可用」。逐个实测(生产):
`custom_model_kimi` / `custom_model_claude` / `custom_model_1M` 返回 **空
content**(`custom_model_gpt-5` 也答非所问),而真实模型
(glm-5.2 → Z.ai GLM、kimi-k3 → Moonshot、minimax-m3 → MiniMax、
Doubao-Seed-2.1-Pro → 字节)全部正常 —— 所以渠道是通的,问题在**表**。
根因:`custom_model_*` 是 TRAE 的「自定义模型槽位」,上游目录里它们的权威
判据是 `is_custom_model: true`。插件自己的 `trae_batch` 解析器早已按这条
**准入**剔除,但**兜底表**把它们当普通模型列了出来;而远端目录在部分账号
下恒空(企业租户账号实测:22 个 function 全回
`error_code 2001 Config detail for function: X not found`),用户只能看到
兜底表 ⇒ 选中槽位后上游回空 ⇒ 「模型坏了」。
修法(**纯插件**,内核零改动):
- 新增 `models_unavailable` 一档:声明「本协议下不可调用」的条目,
**既不进 UI 选择器,也不进上游源**(与 `models_hidden` 区别:后者只是
不进 UI,内部子 agent 仍可显式调用);
- trae 的 11 个 `custom_model_*` 移入该档;把 4 个内部子 agent 留在
`models_hidden`(语义不变);
- 生产验证:trae 源/选择器从 28 → **17**,逐个实测 17 个全部返回非空内容。
## ② 远端目录「空」要能区分原因
`refresh_models` 此前只会报「远端返回空目录」,把两种完全不同的故障混为一谈:
网络/HTTP 故障 vs 服务端 200 但账号侧没有可下发配置。
现在后者如实报:「远端 200 但无模型配置(账号侧未下发 function config,
企业/租户账号需管理员开启,实测 error_code 2001)」——运维据此知道该找
租户管理员,而不是反复重试网络。
另加 `__trae_catalog_probe`(诊断探针):原样回传目录端点的 status/body 前
N 字节/解析条数,并在 2001 现场定位时立了大功(这类"上游 200 却空目录"没有
原始响应只能瞎猜)。
## ③ opencode / localzen:免费模型按**实测**收敛
用户报「localzen 也不能用了」。逐项实测(生产):
- `space-bunny-free` → **410** `Model space-bunny-free has been deprecated.
Use minimax-m3.1-flash-free instead.`(仍在本地源里 ⇒ 选中必失败)
- `jev-1.13-free` → 400 `Model does not support this protocol.`(非 OpenAI
chat 协议,本端点永远调不通)
- `beam-free` → 200 但 content 空、无 finish_reason
- 其余 7 个(mimo / nemotron-super-vl / nemotron-lightning / longcat /
deepseek-v4.1 / glm-5.3-flash / minimax-m3.1-flash)全部正常返回内容
修法:
- `DEAD_GONE_PATTERNS` 补 opencode 的下架文案
(`Model … has been deprecated`);配阴/阳性判据(参数/字段被弃用**不算**);
- opencode 平台把 jev-1.13-free / beam-free 列入 `models_unavailable`,
并从兜底表与 models_free 里移除;
- 生产 `localzen` 源(手写源,插件不管)按实测刷新为 7 个可用免费模型,
逐个验证返回内容。
## 纪律
渠道逻辑全部留在插件里(channels.lua / 插件自装适配器);内核这轮**没有**
新增任何厂商语义 —— 上一轮加进内核的只有通用的 `adapter.hidden` 机制。
(cherry picked from commit 401a7ead27)
This commit is contained in:
@ -140,7 +140,7 @@ plugin.kernel = {
|
||||
|
||||
-- WebUI 可调用的导出函数(POST /api/plugins/channels/call/{fn})。
|
||||
-- set_endpoints 让运维把渠道端点指向自建代理/镜像;测试用它打假上游。
|
||||
plugin.exports = { "set_endpoints", "account_add", "account_list", "account_update", "account_clear_credentialless", "account_delete", "account_test", "account_refresh", "account_reorder", "account_reset", "account_retest", "account_adopt", "model_list", "model_refresh", "model_set_disabled", "model_set_disabled_many", "model_set_all_disabled", "model_clear_dead", "provider_status", "provider_set_enabled", "provider_get_order", "provider_set_order", "credits_claim_all", "credits_status", "credits_balances", "credits_pool_reason", "credits_permanent_lock", "backup_export", "backup_import", "backup_status", "auto_checkin", "gateway_get_enabled", "gateway_set_enabled", "usage_token_ledger", "usage_badge", "usage_badge_preference", "onboarding_status", "onboarding_claim", "__trae_derive_probe", "__codearts_sign_probe", "__qoder_machine_probe", "__qoder_refresh_probe", "__codearts_dpop_probe", "__codearts_cred_probe", "__codearts_adapter_sign_probe", "__sync_source_probe", "__trae_solo_probe", "__adapter_install_probe", "__store_dir_probe", "__set_loopback_probe", "reap_loopbacks", "reap_orphan_sources", "__add_source_for_test", "__forget_account_probe", "__set_pool_cred_probe", "__cred_headers_probe", "__b64_probe", "__codearts_jwk_pem_probe", "__platform_def_probe", "__zcode_diag_probe", "__trae_entry_probe", "__list_sources_probe", "__clear_priorities_probe", "__resync_sources_probe", "__utf8_clip_probe", "__event_log_probe", "__is_model_gone_probe", "__rank_accounts_probe", "__chosen_account_probe", "__inject_refresh_platform_probe", "__clear_capabilities_probe", "__qoder_claim_outcome_probe", "__is_free_probe", "__claim_covers_probe", "__schedule_probe", "__parse_capability_probe", "__inject_capability_probe", "__ttl_reset_probe", "__ttl_probe", "__ttl_invalidate_probe", "__serial_probe", "__serial_release_probe", "__set_legacy_cred_probe", "__read_cred_probe", "__clear_all_creds_probe", "credits_tier_probe", "__set_lock_permanent_probe", "__set_expiring_window_probe", "__expiring_window_probe", "__record_dead_probe", "__catalog_gate_probe", "__catalog_gate_fail_probe", "credits_capability", "claims_capable" }
|
||||
plugin.exports = { "set_endpoints", "account_add", "account_list", "account_update", "account_clear_credentialless", "account_delete", "account_test", "account_refresh", "account_reorder", "account_reset", "account_retest", "account_adopt", "model_list", "model_refresh", "model_set_disabled", "model_set_disabled_many", "model_set_all_disabled", "model_clear_dead", "provider_status", "provider_set_enabled", "provider_get_order", "provider_set_order", "credits_claim_all", "credits_status", "credits_balances", "credits_pool_reason", "credits_permanent_lock", "backup_export", "backup_import", "backup_status", "auto_checkin", "gateway_get_enabled", "gateway_set_enabled", "usage_token_ledger", "usage_badge", "usage_badge_preference", "onboarding_status", "onboarding_claim", "__trae_derive_probe", "__codearts_sign_probe", "__qoder_machine_probe", "__qoder_refresh_probe", "__codearts_dpop_probe", "__codearts_cred_probe", "__codearts_adapter_sign_probe", "__sync_source_probe", "__trae_solo_probe", "__adapter_install_probe", "__store_dir_probe", "__set_loopback_probe", "reap_loopbacks", "reap_orphan_sources", "__add_source_for_test", "__forget_account_probe", "__set_pool_cred_probe", "__cred_headers_probe", "__b64_probe", "__codearts_jwk_pem_probe", "__platform_def_probe", "__zcode_diag_probe", "__trae_entry_probe", "__trae_catalog_probe", "__list_sources_probe", "__clear_priorities_probe", "__resync_sources_probe", "__utf8_clip_probe", "__event_log_probe", "__is_model_gone_probe", "__rank_accounts_probe", "__chosen_account_probe", "__inject_refresh_platform_probe", "__clear_capabilities_probe", "__qoder_claim_outcome_probe", "__is_free_probe", "__claim_covers_probe", "__schedule_probe", "__parse_capability_probe", "__inject_capability_probe", "__ttl_reset_probe", "__ttl_probe", "__ttl_invalidate_probe", "__serial_probe", "__serial_release_probe", "__set_legacy_cred_probe", "__read_cred_probe", "__clear_all_creds_probe", "credits_tier_probe", "__set_lock_permanent_probe", "__set_expiring_window_probe", "__expiring_window_probe", "__record_dead_probe", "__catalog_gate_probe", "__catalog_gate_fail_probe", "credits_capability", "claims_capable" }
|
||||
|
||||
|
||||
|
||||
@ -291,14 +291,25 @@ local PLATFORMS = {
|
||||
"Doubao-Seed-2.1-Turbo", "Doubao-Seed-2.0-Code", "glm-5.2", "glm-5-turbo",
|
||||
"glm-5", "DeepSeek-V4-Pro", "DeepSeek-V4-Flash", "kimi-k3", "kimi-k2.7-code",
|
||||
"kimi-k2.6", "minimax-m3", "qwen-3.7-plus", "sagitta", "aquila",
|
||||
},
|
||||
-- ★ custom_model_* 是 TRAE 的「自定义模型槽位」,**不是可调用的模型**:
|
||||
-- 上游目录里它们的判据是 `is_custom_model: true`(本插件的 trae_batch
|
||||
-- 解析器早就按这条**准入**剔除),但兜底表此前把它们当普通模型列出来 ——
|
||||
-- 于是远端目录取不到时(企业租户账号实测:22 个 function 全回
|
||||
-- `2001 Config detail ... not found` ⇒ 目录为空),用户在模型选择器里
|
||||
-- 能看到这些条目,选中后上游回空内容(实测 custom_model_kimi /
|
||||
-- custom_model_claude / custom_model_1M 均返回空 content)——
|
||||
-- 表现就是「模型不正确 / 渠道不可用」。
|
||||
-- 它们与内部子 agent 同性质:保留在目录里(排查可见),但不进可选列表。
|
||||
models_hidden = {
|
||||
"browser_use_subagent", "explore_sub_agent_v13", "explore_sub_agent_v2", "summary",
|
||||
},
|
||||
models_unavailable = {
|
||||
"custom_model_gemini", "custom_model_placeholder", "custom_model_1M_text",
|
||||
"custom_model_1M", "custom_model_kimi", "custom_model_claude",
|
||||
"custom_model_gpt-5", "custom_model_no-fc", "custom_model_deepseek_chat",
|
||||
"custom_model_deepseek_reasoner", "custom_model_deepseek_v4",
|
||||
},
|
||||
models_hidden = {
|
||||
"browser_use_subagent", "explore_sub_agent_v13", "explore_sub_agent_v2", "summary",
|
||||
},
|
||||
-- 每日签到:POST /trae/api/v2/ug/checkin_credits/claim body {} 到 UG 主机。
|
||||
checkin = {
|
||||
url = "https://api.trae.cn/trae/api/v2/ug/checkin_credits/claim",
|
||||
@ -964,19 +975,27 @@ local PLATFORMS = {
|
||||
-- * 新增 beam-free / nemotron-3.5-super-vl-free / jev-1.13-free /
|
||||
-- glm-5.3-flash-free / minimax-m3.1-flash-free
|
||||
models = {
|
||||
"big-pickle", "beam-free", "longcat-2.5-preview-free",
|
||||
"big-pickle", "longcat-2.5-preview-free",
|
||||
"mimo-v2.6-flash-free", "nemotron-3.5-super-vl-free",
|
||||
"nemotron-3.5-lightning-free", "jev-1.13-free", "glm-5.3-flash-free",
|
||||
"nemotron-3.5-lightning-free", "glm-5.3-flash-free",
|
||||
"minimax-m3.1-flash-free", "deepseek-v4.1-flash-free",
|
||||
"deepseek-v4-flash", "deepseek-v4.1-flash",
|
||||
"glm-5.2", "kimi-k2.5", "minimax-m2.7", "minimax-m3", "qwen3.8-max",
|
||||
},
|
||||
models_free = {
|
||||
"big-pickle", "beam-free", "longcat-2.5-preview-free",
|
||||
"big-pickle", "longcat-2.5-preview-free",
|
||||
"mimo-v2.6-flash-free", "nemotron-3.5-super-vl-free",
|
||||
"nemotron-3.5-lightning-free", "jev-1.13-free",
|
||||
"nemotron-3.5-lightning-free",
|
||||
"glm-5.3-flash-free", "minimax-m3.1-flash-free", "deepseek-v4.1-flash-free",
|
||||
},
|
||||
-- ★ 目录里有、但**本协议下不可调用**的免费模型(2026-10-20 逐个实测):
|
||||
-- * jev-1.13-free → 400 "Model does not support this protocol."
|
||||
-- (它不是 OpenAI chat 协议,用 /v1/chat/completions 必失败)
|
||||
-- * beam-free → 200 但 content 为空、无 finish_reason(上游未真正产出)
|
||||
-- 二者若留在可选列表,用户选中后只会看到"空回复/报错" ⇒ 判为不可用,
|
||||
-- 既不进 UI 列表,也不进上游源(models_unavailable 与 hidden 的区别见
|
||||
-- effectiveModels 的注释)。
|
||||
models_unavailable = { "jev-1.13-free", "beam-free" },
|
||||
},
|
||||
|
||||
-- CodeArts(华为)—— 凭据 + SDK-HMAC-SHA256 签到(照参考 codearts-credits.ts)。
|
||||
@ -3735,6 +3754,12 @@ local deadModelIds
|
||||
local function effectiveModels(provider)
|
||||
local plat = platformById(provider)
|
||||
if not plat then return {} end
|
||||
-- models_unavailable:**声明上就不可调用**的条目(不是"暂不显示")。
|
||||
-- 与 models_hidden 的区别:hidden 只是不进 UI 列表(内部子 agent 仍可被
|
||||
-- 显式调用),unavailable 连**上游源**都不进 —— 因为它们调用必然失败/回空,
|
||||
-- 进源只会让客户端选中后报错(trae 的 custom_model_* 槽位就是这类)。
|
||||
local unavailable = {}
|
||||
for _, m in ipairs(plat.models_unavailable or {}) do unavailable[m] = true end
|
||||
local remote = plugin.state.remoteModels and plugin.state.remoteModels[provider]
|
||||
local out, seen = {}, {}
|
||||
-- 远端在前(权威顺序)
|
||||
@ -3755,7 +3780,7 @@ local function effectiveModels(provider)
|
||||
local dead = deadModelIds(provider)
|
||||
local kept = {}
|
||||
for _, id in ipairs(out) do
|
||||
if not dead[id] then kept[#kept+1] = id end
|
||||
if not dead[id] and not unavailable[id] then kept[#kept+1] = id end
|
||||
end
|
||||
return kept
|
||||
end
|
||||
@ -4161,6 +4186,9 @@ end
|
||||
local function refreshRemoteModels(provider)
|
||||
local plat = platformById(provider)
|
||||
if not plat then return nil, "未知平台" end
|
||||
-- 最近一次目录响应的**原始体**:解析失败时用来区分"网络/HTTP 故障"与
|
||||
-- "服务端 200 但账号侧没有可下发配置"(后者文案完全不同,见函数尾部)。
|
||||
local lastBody = nil
|
||||
local mr = plat.models_remote
|
||||
if not mr then return nil, "该平台无远端目录端点" end
|
||||
-- 取一个可用账号的凭据(无账号则无凭据,仅当 auth=none 时仍可拉)
|
||||
@ -4256,12 +4284,30 @@ local function refreshRemoteModels(provider)
|
||||
if err or not resp or resp.status ~= 200 then
|
||||
lastErr = (err or ("HTTP " .. tostring(resp and resp.status)))
|
||||
else
|
||||
lastBody = resp.body or ""
|
||||
for _, id in ipairs(parseRemoteModels(ep.parse or mr.parse, resp.body, provider)) do
|
||||
if not seen[id] then seen[id] = true merged[#merged+1] = id end
|
||||
end
|
||||
end
|
||||
end
|
||||
if #merged == 0 then return nil, lastErr or "远端返回空目录" end
|
||||
if #merged == 0 then
|
||||
-- ★ 把"空目录"再细分一层:远程 200 但一条配置都没有,通常不是网络问题,
|
||||
-- 而是**账号侧**没有可下发的 function 配置(企业租户账号实测:22 个
|
||||
-- function 全回 error_code 2001 "Config detail for function: X not found")。
|
||||
-- 这两类原因的处置完全不同(网络重试 vs 找租户管理员),日志不能都说
|
||||
-- "远端返回空目录"。判据只看响应体里出现 2001 或 function_configs。
|
||||
if not lastErr then
|
||||
local seenBody = false
|
||||
if type(lastBody) == "string" and lastBody:find("function_configs", 1, true) then
|
||||
seenBody = true
|
||||
end
|
||||
if seenBody then
|
||||
return nil, "远端 200 但无模型配置(账号侧未下发 function config,"
|
||||
.. "企业/租户账号需管理员开启,实测 error_code 2001)"
|
||||
end
|
||||
end
|
||||
return nil, lastErr or "远端返回空目录"
|
||||
end
|
||||
if not plugin.state.remoteModels then plugin.state.remoteModels = {} end
|
||||
plugin.state.remoteModels[provider] = {
|
||||
ids = merged,
|
||||
@ -7184,6 +7230,13 @@ local DEAD_GONE_PATTERNS = {
|
||||
"unknown%s+model",
|
||||
"no%s+such%s+model",
|
||||
"nonexistent%s+model",
|
||||
-- ★ opencode zen 的下架文案(2026-10-20 实测):
|
||||
-- 410 `Model space-bunny-free has been deprecated. Use minimax-m3.1-flash-free instead.`
|
||||
-- 此前不识别 ⇒ 该模型一直留在源里,用户选中必失败("渠道不可用"观感)。
|
||||
-- 带 "has been deprecated" 整句是为了不误伤「参数 X 已弃用」这类**非模型**文案:
|
||||
-- "Model ... has been deprecated" 的主语必须是 Model(%s+ 吃掉名字)。
|
||||
"model%s+%S+%s+has%s+been%s+deprecated",
|
||||
"model%s+has%s+been%s+deprecated",
|
||||
}
|
||||
|
||||
-- 反向模式:命中即**不是**下架(照参考 NOT_GONE_PATTERNS)。
|
||||
@ -8089,6 +8142,7 @@ function plugin.model_list(req)
|
||||
-- models_hidden:内部子 agent(照参考 isHidden 语义)不进用户可选列表。
|
||||
local hidden = {}
|
||||
for _, m in ipairs(plat.models_hidden or {}) do hidden[m] = true end
|
||||
for _, m in ipairs(plat.models_unavailable or {}) do hidden[m] = true end
|
||||
-- models_free:参考的 isFree 字段(官方语义:匿名/免费可用)。
|
||||
local free = {}
|
||||
for _, m in ipairs(plat.models_free or {}) do free[m] = true end
|
||||
@ -12986,6 +13040,66 @@ function plugin.__list_sources_probe()
|
||||
return { json = { sources = out } }
|
||||
end
|
||||
|
||||
-- plugin.__trae_catalog_probe:只给诊断用 —— 原样回传 TRAE 远端模型目录的
|
||||
-- HTTP 状态与前 1200 字节响应体。
|
||||
--
|
||||
-- 存在理由:`refresh_models` 只把「解析出几条」汇总成「远端返回空目录」,
|
||||
-- 看不到**上游到底回了什么**(空数组?错误信封?形状变了?)。模型目录决定
|
||||
-- 用户在渠道页能选哪些模型,它是"渠道不可用"类报障的第一现场,不能只有一句
|
||||
-- 二手结论。探测逻辑与 refreshRemoteModels 的 trae 分支**同源**(同一个
|
||||
-- traeCheckinHeaders + 同一份请求体),避免"探针通、真路径不通"。
|
||||
function plugin.__trae_catalog_probe(args)
|
||||
args = args or {}
|
||||
local plat = platformById("trae")
|
||||
if not plat or not plat.models_remote then
|
||||
return { json = { error = "trae 平台未声明 models_remote" } }
|
||||
end
|
||||
local ep = (plat.models_remote.endpoints or {})[1] or {}
|
||||
local cred
|
||||
for _, a in ipairs(poolList("trae")) do
|
||||
if a.enabled ~= false then
|
||||
local raw = __llmsproxy_secrets.get(a.credentialRef)
|
||||
local okj, cj = pcall(json.decode, raw or "")
|
||||
if okj and type(cj) == "table" and cj.access_token then cred = cj break end
|
||||
end
|
||||
end
|
||||
if not cred then return { json = { error = "无可用 TRAE 凭据" } } end
|
||||
if type(traeCheckinHeaders) ~= "function" then
|
||||
return { json = { error = "traeCheckinHeaders 未就绪" } }
|
||||
end
|
||||
local h, herr = traeCheckinHeaders(plat, cred, cred.uid or "")
|
||||
if herr then return { json = { error = "构造头部失败: " .. tostring(herr) } } end
|
||||
-- 诊断用覆盖:Accept 在参考里对目录请求是 application/json,而 checkin 的
|
||||
-- 头部构造给的是 text/event-stream —— 用它可以判定"是不是 Accept 导致空目录"。
|
||||
if type(args.accept) == "string" and args.accept ~= "" then
|
||||
h["Accept"] = args.accept
|
||||
end
|
||||
-- 诊断用:额外头部(企业/租户类账号的 IDE 端点通常要求租户身份,
|
||||
-- 而参考的 traeSOLOHeaders 不发。用它验证"缺租户头导致全 2001")。
|
||||
if type(args.headers) == "table" then
|
||||
for k, v in pairs(args.headers) do
|
||||
if type(k) == "string" and type(v) == "string" then h[k] = v end
|
||||
end
|
||||
end
|
||||
if type(args.ide_version) == "string" and args.ide_version ~= "" then
|
||||
h["X-Ide-Version"] = args.ide_version
|
||||
h["X-App-Version-Code"] = args.ide_version
|
||||
h["X-Ide-Version-Code"] = args.ide_version
|
||||
end
|
||||
local resp, err = __llmsproxy_http({
|
||||
url = ep.url, method = ep.method or "POST",
|
||||
headers = h, body = (type(args.body) == "string" and args.body ~= "" and args.body) or ep.body or "{}",
|
||||
})
|
||||
if err then return { json = { error = tostring(err) } } end
|
||||
local body = (resp and resp.body) or ""
|
||||
return { json = {
|
||||
status = resp and resp.status or 0,
|
||||
bytes = #body,
|
||||
head = body:sub(1, math.min(tonumber(args.clip) or 1200, #body)),
|
||||
parsed = #parseRemoteModels("trae_batch", body, "trae"),
|
||||
} }
|
||||
end
|
||||
|
||||
-- plugin.__trae_entry_probe:只给测试/诊断用 —— 报告某模型最终选中条目的关键字段。
|
||||
-- “合并规则选错了条目”(空档位覆盖有档位,issue IKI7WT/IKILR7)这类问题
|
||||
-- 没有它只能靠猜。
|
||||
|
||||
Reference in New Issue
Block a user