mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
feat(channels): TRAE 用插件自装的 SOLO 适配器(channels-trae)—— 载荷真转换
TRAE **不是 OpenAI 兼容**。参考 trae.ts:30 点名:「载荷必须转换:OpenAI 的
{model,messages,tools,tool_choice,stream} 需要映射为 SOLO 格式(function、
config_name 等字段),不能透传」。上一轮我把端点改对了(/api/agent/v3/
llm_utils_chat)但**载荷仍原样直发** —— 上游会回流内 4001,而 HTTP 状态码
仍是 200,属静默失败。
本次补齐 transformToSOLOBody 的 7 条规则(逐条对齐参考):
1. messages.content 字符串 → [{type:"text",text:…}];数组透传
2. stream 强制 true(非流式由服务端聚合)
3. model → config_name + model **双字段**(支持 __dev 后缀消除)
4. function 通道名,缺省 solo_work_lite
5. tool_choice 归一化:none → 删 tool_choice **与** tools;
auto/required 保留;{type:function,function:{name}} → 字符串 name
6. assistant 的 tool_calls: function → function_call,并剔除无 name 的条目
(孤儿 tool_call 会让整条会话被拒)
7. tools[].function.parameters: object → JSON **字符串**(SOLO 要求)
★ 命名为什么是 channels-trae 而不是 trae:内置 trae.lua 服务的是**运维自建
的 trae-local-api 反代**(base_url 127.0.0.1:19900,生产 config.yaml 在用),
与本插件的直连通道是两回事。重名会被内核冲突仲裁挂起(正确的护栏)。
判据里专门断言"内置 trae 未被顶掉"。
★ 踩到的坑:`function` 是 Lua 关键字,写 `body.function = x` 是**语法错误**
('<name>' expected near 'function'),必须 `body["function"]`。它只在 load()
时才暴露 —— 插件脚本本身能编译通过,适配器源码要运行期才报错。
判据(internal/lua/channels_trae_solo_test.go):走**真实路径**
(装插件 → 调已安装的适配器),而不是在插件 VM 里 load 一份副本 ——
副本拿不到内核注入的共享前导,也发现不了"装了但没生效"。逐条断言 7 条规则
+ 三处反向断言(extra_body 必剥离、无 name 的 tool_call 必剔除、
tool_choice=none 时 tools 与 tool_choice 都必须消失)。
回归:internal/{lua,core,provider,gateway} 全绿。
This commit is contained in:
@ -52,7 +52,7 @@ plugin.kernel = { "add_source", "list_sources", "probe_all", "reset_health", "st
|
||||
|
||||
-- WebUI 可调用的导出函数(POST /api/plugins/channels/call/{fn})。
|
||||
-- set_endpoints 让运维把渠道端点指向自建代理/镜像;测试用它打假上游。
|
||||
plugin.exports = { "set_endpoints", "account_add", "account_list", "account_update", "account_delete", "account_test", "account_refresh", "account_reorder", "account_reset", "account_retest", "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_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" }
|
||||
plugin.exports = { "set_endpoints", "account_add", "account_list", "account_update", "account_delete", "account_test", "account_refresh", "account_reorder", "account_reset", "account_retest", "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_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" }
|
||||
|
||||
|
||||
|
||||
@ -125,8 +125,10 @@ local PLATFORMS = {
|
||||
hosts = { "www.trae.cn", "api.trae.com.cn", "api.trae.cn", "trae-api-cn.mchost.guru" },
|
||||
-- trae 非 OpenAI 兼容:端点是 SOLO 的 /api/agent/v3/llm_utils_chat,
|
||||
-- 载荷须转 config_name/function 格式(见 trae-adapter.ts:374)。
|
||||
base_url = "https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat",
|
||||
adapter = "openai",
|
||||
base_url = "https://trae-api-cn.mchost.guru",
|
||||
-- TRAE 非 OpenAI 兼容,用**本插件自装**的 SOLO 适配器
|
||||
-- (见 TRAE_SOLO_ADAPTER_SRC)。端点由适配器声明 /api/agent/v3/llm_utils_chat。
|
||||
adapter = "channels-trae",
|
||||
-- 照参考 TRAE_FALLBACK_MODELS 全量(32 项)。其中 4 项带 isHidden
|
||||
-- (内部子 agent)—— 不进用户可选列表,单列在 models_hidden。
|
||||
models = {
|
||||
@ -6674,6 +6676,189 @@ end
|
||||
return adapter
|
||||
]==]
|
||||
|
||||
-- ============================================================================
|
||||
-- TRAE 专用适配器(插件自装:channels-trae)
|
||||
-- ============================================================================
|
||||
-- TRAE **不是 OpenAI 兼容**。参考 trae.ts:30 注释点名:
|
||||
-- 「载荷必须转换:OpenAI 的 {model,messages,tools,tool_choice,stream} 需要
|
||||
-- 映射为 SOLO 格式(function、config_name 等字段),不能透传」
|
||||
--
|
||||
-- 为什么名字不叫 "trae":内置适配器 `trae.lua` 服务的是**运维自建的
|
||||
-- trae-local-api 反代**(base_url 127.0.0.1:19900,生产 config.yaml 在用),
|
||||
-- 与本插件的直连通道是两回事。重名会被内核的冲突仲裁挂起(正确的护栏),
|
||||
-- 所以本插件用 `channels-trae`。
|
||||
--
|
||||
-- SOLO 转换规则(逐条对齐参考 trae.ts:transformToSOLOBody):
|
||||
-- 1. messages.content 字符串 → [{type:"text",text:...}];已是数组则透传
|
||||
-- 2. stream 强制 true(非流式由服务端聚合)
|
||||
-- 3. model → config_name + model **双字段**
|
||||
-- 4. function: 通道名,缺省 "solo_work_lite"
|
||||
-- 5. tool_choice 归一化("none" 删 tools;auto/required 保留;
|
||||
-- {type:"function",function:{name}} → 字符串 name)
|
||||
-- 6. assistant 消息的 tool_calls: function → function_call(SOLO 字段名),
|
||||
-- 并剔除没有 name 的条目(孤儿会让整条会话被拒)
|
||||
-- 7. tools[].function.parameters: object → JSON **字符串**(SOLO 要求)
|
||||
local TRAE_SOLO_ADAPTER_SRC = [==[
|
||||
local adapter = {}
|
||||
adapter.name = "channels-trae"
|
||||
adapter.version = "1.0.0"
|
||||
adapter.endpoint = "/api/agent/v3/llm_utils_chat"
|
||||
|
||||
local TRAE_DEFAULT_MODEL = "glm-5.2"
|
||||
local TRAE_FUNCTION = "solo_work_lite"
|
||||
local TRAE_MAX_MODE_TYPE = 1
|
||||
local TRAE_MAX_PROMPT_TOKENS = 936000
|
||||
local TRAE_MAX_OUTPUT_TOKENS = 64000
|
||||
|
||||
-- normalizeToolChoice:照参考 normalizeToolChoice(Go payload.go:111-154)
|
||||
local function normalize_tool_choice(body)
|
||||
local tc = body.tool_choice
|
||||
if tc == nil then return end
|
||||
local function suppress()
|
||||
body.tools = nil
|
||||
body.functions = nil
|
||||
end
|
||||
if type(tc) == "string" then
|
||||
if string.lower(tc):gsub("%s+", "") == "none" then
|
||||
body.tool_choice = nil
|
||||
suppress()
|
||||
end
|
||||
return
|
||||
end
|
||||
if type(tc) == "table" then
|
||||
local typ = type(tc.type) == "string" and string.lower(tc.type):gsub("%s+", "") or ""
|
||||
if typ == "none" then
|
||||
body.tool_choice = nil
|
||||
suppress()
|
||||
elseif typ == "auto" or typ == "required" then
|
||||
body.tool_choice = typ
|
||||
elseif typ == "function" then
|
||||
local name = ""
|
||||
local fn = tc["function"]
|
||||
if type(fn) == "table" and type(fn.name) == "string" then name = fn.name end
|
||||
if name == "" and type(tc.name) == "string" then name = tc.name end
|
||||
if #(name:gsub("%s+", "")) > 0 then body.tool_choice = name
|
||||
else body.tool_choice = "auto" end
|
||||
else
|
||||
body.tool_choice = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
body.tool_choice = nil
|
||||
end
|
||||
|
||||
-- normalizeTools:parameters 必须是**字符串**(SOLO 要求),OpenAI 标准是对象。
|
||||
local function normalize_tools(body)
|
||||
local raw = body.tools
|
||||
if type(raw) ~= "table" or #raw == 0 then return end
|
||||
local out = {}
|
||||
for _, item in ipairs(raw) do
|
||||
if type(item) == "table" then
|
||||
local fn = item["function"]
|
||||
if type(fn) == "table" then
|
||||
if type(fn.parameters) == "table" then
|
||||
fn.parameters = json.encode(fn.parameters)
|
||||
end
|
||||
out[#out + 1] = item
|
||||
end
|
||||
end
|
||||
end
|
||||
if #out > 0 then body.tools = out end
|
||||
end
|
||||
|
||||
-- transformSOLOMessage:单条消息(content 字符串→parts;tool_calls 字段改名)
|
||||
local function transform_solo_message(msg)
|
||||
if type(msg) ~= "table" then return msg end
|
||||
local result = {}
|
||||
for k, v in pairs(msg) do result[k] = v end
|
||||
|
||||
if result.role == "assistant" and type(result.tool_calls) == "table" then
|
||||
local kept = {}
|
||||
for _, tc in ipairs(result.tool_calls) do
|
||||
if type(tc) == "table" then
|
||||
-- function → function_call(SOLO 字段名)
|
||||
if type(tc["function"]) == "table" then
|
||||
tc.function_call = tc["function"]
|
||||
tc["function"] = nil
|
||||
end
|
||||
local fc = tc.function_call
|
||||
local name = (type(fc) == "table" and type(fc.name) == "string") and fc.name or ""
|
||||
-- 剔除没有 name 的条目(孤儿 tool_call 会让整条会话被拒)
|
||||
if #(name:gsub("%s+", "")) > 0 then kept[#kept + 1] = tc end
|
||||
end
|
||||
end
|
||||
if #kept > 0 then result.tool_calls = kept else result.tool_calls = nil end
|
||||
end
|
||||
|
||||
-- content:字符串 → [{type:"text",text:...}];数组/其它原样透传
|
||||
if type(result.content) == "string" then
|
||||
result.content = { { type = "text", text = result.content } }
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- transform_request:OpenAI 请求体 → SOLO 格式
|
||||
function adapter.transform_request(raw_body)
|
||||
local body = __prelude.decode_body(raw_body)
|
||||
if body == nil then return raw_body end
|
||||
__prelude.strip_control(body)
|
||||
|
||||
body.stream = true
|
||||
-- ⚠️ `function` 是 Lua 关键字:必须用 body["function"],写 body.function
|
||||
-- 是语法错误('<name>' expected near 'function'),且只在 load() 时才暴露
|
||||
-- —— 模块级 local function 不会在编译期报出来。
|
||||
if body["function"] == nil then body["function"] = TRAE_FUNCTION end
|
||||
if type(body["function"]) == "string" and body["function"] == "" then
|
||||
body["function"] = TRAE_FUNCTION
|
||||
end
|
||||
|
||||
-- messages 转换
|
||||
if type(body.messages) == "table" then
|
||||
local msgs = {}
|
||||
for _, m in ipairs(body.messages) do msgs[#msgs + 1] = transform_solo_message(m) end
|
||||
body.messages = msgs
|
||||
end
|
||||
|
||||
-- model → config_name + model(双字段)。支持 __dev 后缀消除。
|
||||
local model = type(body.model) == "string" and body.model or ""
|
||||
local base = model
|
||||
local sep = string.find(model, "__", 1, true)
|
||||
if sep then base = string.sub(model, 1, sep - 1) end
|
||||
local config_name = (base ~= "" and base) or TRAE_DEFAULT_MODEL
|
||||
body.config_name = config_name
|
||||
body.model = config_name
|
||||
|
||||
normalize_tool_choice(body)
|
||||
normalize_tools(body)
|
||||
return json.encode(body)
|
||||
end
|
||||
|
||||
function adapter.transform_response(raw_body)
|
||||
return __prelude.openai_response(raw_body)
|
||||
end
|
||||
|
||||
function adapter.transform_stream_chunk(raw_chunk)
|
||||
return __prelude.openai_stream_chunk(raw_chunk)
|
||||
end
|
||||
|
||||
function adapter.transform_error(status, body)
|
||||
return __prelude.openai_error(status, body)
|
||||
end
|
||||
|
||||
return adapter
|
||||
]==]
|
||||
|
||||
-- installTraeAdapter():装/更新本插件自己的 TRAE 直连适配器。
|
||||
local function installTraeAdapter()
|
||||
if not __llmsproxy_adapters then return false, "无适配器桥(旧内核?)" end
|
||||
local _, uerr = __llmsproxy_adapters.update("channels-trae", TRAE_SOLO_ADAPTER_SRC)
|
||||
if uerr then
|
||||
local _, ierr = __llmsproxy_adapters.install("channels-trae", TRAE_SOLO_ADAPTER_SRC)
|
||||
if ierr then return false, ierr end
|
||||
end
|
||||
return true, nil
|
||||
end
|
||||
|
||||
-- installCodeartsAdapter():装/更新本插件自己的 codearts 适配器。
|
||||
-- 幂等(update 失败时退化为 install);失败只记日志,不阻断插件加载 ——
|
||||
-- 适配器装不上时源注册会报"未知适配器",比让整个插件加载失败更好定位。
|
||||
@ -7239,6 +7424,9 @@ end
|
||||
do
|
||||
local ok, err = installCodeartsAdapter()
|
||||
plugin._codearts_adapter = { ok = ok, err = err }
|
||||
local tok2, terr2 = installTraeAdapter()
|
||||
plugin._trae_adapter = { ok = tok2, err = terr2 }
|
||||
if not tok2 then logEvent("TRAE 适配器装载失败: " .. tostring(terr2)) end
|
||||
end
|
||||
|
||||
return plugin
|
||||
|
||||
Reference in New Issue
Block a user