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:
@ -68,6 +68,16 @@ func TestDeadModelGoneJudgement(t *testing.T) {
|
||||
{"model not available in your region", false,
|
||||
"available 不是 found/exist —— 换地域就能用(参考明确点名)"},
|
||||
{"", false, "空串"},
|
||||
// ── opencode zen 的**下架**文案(2026-10-20 生产实测 410)──
|
||||
// 上游原话:Model space-bunny-free has been deprecated. Use
|
||||
// minimax-m3.1-flash-free instead. 此前不识别 ⇒ 该模型一直留在源里,
|
||||
// 用户选中必 410("渠道不可用"的观感来源之一)。
|
||||
{"Model space-bunny-free has been deprecated. Use minimax-m3.1-flash-free instead.",
|
||||
true, "opencode zen 410 下架文案"},
|
||||
{"model has been deprecated", true, "同族最短形态(无模型名)"},
|
||||
// 阴性:参数/字段被弃用 ≠ 模型下架(主语不是 Model)
|
||||
{"parameter temperature has been deprecated", false, "参数弃用不是模型下架"},
|
||||
{"the field max_tokens has been deprecated", false, "字段弃用不是模型下架"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
out, err := ps.Call("channels", "__is_model_gone_probe",
|
||||
|
||||
111
internal/core/channels_trae_models_test.go
Normal file
111
internal/core/channels_trae_models_test.go
Normal file
@ -0,0 +1,111 @@
|
||||
package core
|
||||
|
||||
import "testing"
|
||||
|
||||
// TRAE 的模型选择器**不许**出现 `custom_model_*`。
|
||||
//
|
||||
// 真实缺陷(2026-10-20 用户报「trae 的模型不正确 / 渠道不可用」):
|
||||
// 这些 id 是 TRAE 的「自定义模型槽位」,上游目录里它们的权威判据是
|
||||
// `is_custom_model: true`(插件 trae_batch 解析器早已按这条准入剔除)。
|
||||
// 但**兜底表**把它们当普通模型列了出来 —— 而远端目录在部分账号下恒空
|
||||
// (企业租户账号实测:22 个 function 全回 error_code 2001 "Config detail
|
||||
// for function: X not found"),于是用户只能看到兜底表,选中这些槽位后
|
||||
// 上游回**空 content**(实测 custom_model_kimi / custom_model_claude /
|
||||
// custom_model_1M 均空),表现就是"模型坏了、渠道不可用"。
|
||||
//
|
||||
// 判据分两层:
|
||||
// - model_list(UI 选择器)不含它们;
|
||||
// - 交给内核的上游源里也不含它们(effectiveModels 走 models_unavailable)。
|
||||
func TestTraeCustomModelSlotsAreNotOffered(t *testing.T) {
|
||||
c := kernelTestCore(t)
|
||||
ps := c.Plugins()
|
||||
src, err := readBundledChannels(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ps.LoadSource("channels", src); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = ps.SetPluginKernelPermission("channels",
|
||||
[]string{"add_source", "remove_source", "list_sources", "status"}, false)
|
||||
|
||||
out, err := ps.Call("channels", "model_list", map[string]interface{}{"platform": "trae"})
|
||||
if err != nil {
|
||||
t.Fatalf("model_list: %v", err)
|
||||
}
|
||||
j, _ := out.(map[string]interface{})["json"].(map[string]interface{})
|
||||
models, _ := j["models"].([]interface{})
|
||||
if len(models) == 0 {
|
||||
t.Fatal("trae 应至少有一个可选模型(兜底表)")
|
||||
}
|
||||
for _, raw := range models {
|
||||
m, _ := raw.(map[string]interface{})
|
||||
id, _ := m["id"].(string)
|
||||
if len(id) > 13 && id[:13] == "custom_model_" {
|
||||
t.Fatalf("model_list 不应包含自定义模型槽位 %q(选中后上游回空内容)", id)
|
||||
}
|
||||
}
|
||||
// 可调用模型必须还在(不能把整张表干掉换绿)
|
||||
found := false
|
||||
for _, raw := range models {
|
||||
m, _ := raw.(map[string]interface{})
|
||||
if m["id"] == "glm-5.2" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("可调用的 glm-5.2 不应被一起藏掉;实得 %v", models)
|
||||
}
|
||||
}
|
||||
|
||||
// 上游源的模型清单同样不含占位槽位(客户端按裸名就能调,藏 UI 不够)。
|
||||
func TestTraeSourceOmitsCustomModelSlots(t *testing.T) {
|
||||
c := kernelTestCore(t)
|
||||
ps := c.Plugins()
|
||||
src, err := readBundledChannels(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ps.LoadSource("channels", src); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = ps.SetPluginKernelPermission("channels",
|
||||
[]string{"add_source", "remove_source", "list_sources", "status"}, false)
|
||||
if _, err := ps.Call("channels", "account_add", map[string]interface{}{
|
||||
"platform": "trae", "account_id": "trae-a",
|
||||
"credential": `{"access_token":"jwt","uid":"1","device_id":"d","machine_id":"m"}`,
|
||||
}); err != nil {
|
||||
t.Fatalf("account_add: %v", err)
|
||||
}
|
||||
ls, err := ps.Call("channels", "__list_sources_probe", map[string]interface{}{})
|
||||
if err != nil {
|
||||
t.Fatalf("probe: %v", err)
|
||||
}
|
||||
j, _ := ls.(map[string]interface{})["json"].(map[string]interface{})
|
||||
arr, _ := j["sources"].([]interface{})
|
||||
haveTrae := false
|
||||
for _, raw := range arr {
|
||||
m, _ := raw.(map[string]interface{})
|
||||
if m["name"] == "channels-trae" {
|
||||
haveTrae = true
|
||||
}
|
||||
}
|
||||
if !haveTrae {
|
||||
t.Fatalf("trae 账号加好后应产出 channels-trae 源;实得 %v", arr)
|
||||
}
|
||||
// 源里的模型清单从内核真实源读(不是插件的 model_list)
|
||||
srcs := c.Sources()
|
||||
for _, s := range srcs {
|
||||
if s.Name != "channels-trae" {
|
||||
continue
|
||||
}
|
||||
for _, m := range s.Models {
|
||||
if len(m.ID) > 13 && m.ID[:13] == "custom_model_" {
|
||||
t.Fatalf("上游源不应包含自定义模型槽位 %q", m.ID)
|
||||
}
|
||||
}
|
||||
if len(s.Models) == 0 {
|
||||
t.Fatal("channels-trae 源应带模型清单")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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