diff --git a/internal/lua/adapters/sensenova.lua b/internal/lua/adapters/sensenova.lua index ed37d60..e78ca45 100644 --- a/internal/lua/adapters/sensenova.lua +++ b/internal/lua/adapters/sensenova.lua @@ -57,6 +57,9 @@ function adapter.transform_response(raw_body) unified.content = ch.message.content or "" if ch.message.reasoning_content then unified.reasoning_content = ch.message.reasoning_content + elseif ch.message.reasoning then + -- sensenova-6.8-flash-lite 用 reasoning 字段而不是 reasoning_content + unified.reasoning_content = ch.message.reasoning end end unified.finish_reason = ch.finish_reason or "" @@ -97,10 +100,11 @@ function adapter.transform_stream_chunk(raw_chunk) local delta = chunk.choices[1].delta or {} - -- Sensenova: delta has reasoning_content but no content + -- Sensenova: delta has reasoning_content (deepseek-v4-flash) or reasoning + -- (sensenova-6.8-flash-lite) but no content field local content = delta.content or "" - if content == "" and delta.reasoning_content then - content = delta.reasoning_content + if content == "" and (delta.reasoning_content or delta.reasoning) then + content = delta.reasoning_content or delta.reasoning end -- Sensenova: finish_reason is "" on every chunk, "stop" on last diff --git a/internal/lua/adapters/trae.lua b/internal/lua/adapters/trae.lua new file mode 100644 index 0000000..4a153ed --- /dev/null +++ b/internal/lua/adapters/trae.lua @@ -0,0 +1,175 @@ +-- trae 源适配器(trae-local-api 的 OpenAI 兼容代理) +-- trae-local-api 运行在 http://127.0.0.1:19900,接 Trae CN 账号的云资源。 +-- 协议:OpenAI /v1/chat/completions +-- 特性: +-- - 排队:上游 busy 时排队,可能长时间无响应 +-- - 非流式请求偶发返回 SSE 数据(upstream bug) +-- - 支持 thinking,由上游模型自动决定是否开启 + +local adapter = {} +adapter.name = "trae" +adapter.version = "1.0.0" +adapter.endpoint = "/v1/chat/completions" +adapter.headers = {} + +function adapter.transform_request(raw_body) + local ok, req = pcall(json.decode, raw_body) + if not ok then return raw_body end + req.disable_thinking = nil + req.extra_body = nil + if req.messages then + for _, msg in ipairs(req.messages) do + msg.reasoning_content = nil + end + end + return json.encode(req) +end + +function adapter.transform_response(raw_body) + -- trae-local-api 偶发在非流式请求中返回 SSE 格式数据, + -- 表现为多个 data: {...} 行或混合了 reasoning_chunk 等。 + -- 剥掉 data: 前缀,取最后一个完整的 JSON 块(通常是 + -- 最终的 stop chunk 或 usage chunk)。 + local use_json = raw_body + if raw_body:match("^data:") or raw_body:match("\ndata:") then + -- 取最后一个 data: 行的 JSON + local last = nil + for line in raw_body:gmatch("data: ([^\n\r]+)") do + local trimmed = line:match("^%s*(.-)%s*$") + if trimmed and trimmed ~= "[DONE]" then + local ok2, obj = pcall(json.decode, trimmed) + if ok2 and type(obj) == "table" then + last = obj + end + end + end + if last then + use_json = json.encode(last) + end + -- 如果解析失败,回退到原始 body + end + + local ok, resp = pcall(json.decode, use_json) + if not ok or resp == nil then return raw_body end + + local unified = { + content = "", + finish_reason = "", + token_usage = { prompt = 0, completion = 0, total = 0 } + } + + if type(resp.usage) == "table" then + unified.token_usage.prompt = resp.usage.prompt_tokens or 0 + unified.token_usage.completion = resp.usage.completion_tokens or 0 + unified.token_usage.total = resp.usage.total_tokens or 0 + end + + if type(resp.choices) == "table" and #resp.choices > 0 then + local ch = resp.choices[1] + if type(ch.message) == "table" then + unified.content = ch.message.content or "" + if ch.message.reasoning_content then + unified.reasoning_content = ch.message.reasoning_content + end + if type(ch.message.tool_calls) == "table" then + local tcs = {} + for _, tc in ipairs(ch.message.tool_calls) do + local args_ok, args = pcall(json.decode, tc["function"].arguments) + if not args_ok then args = {} end + table.insert(tcs, { + id = tc.id, + type = tc.type or "function", + name = tc["function"].name, + arguments = args + }) + end + unified.tool_calls = tcs + end + end + unified.finish_reason = ch.finish_reason or "" + end + + return json.encode(unified) +end + +function adapter.transform_stream_chunk(raw_chunk) + local ok, chunk = pcall(json.decode, raw_chunk) + if not ok then return "" end + + local uses = nil + if type(chunk.usage) == "table" then + uses = { + prompt = chunk.usage.prompt_tokens or chunk.usage.prompt or 0, + completion = chunk.usage.completion_tokens or chunk.usage.completion or 0, + total = chunk.usage.total_tokens or chunk.usage.total or 0, + } + if type(chunk.usage.prompt_tokens_details) == "table" and chunk.usage.prompt_tokens_details.cached_tokens ~= nil then + uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_tokens_details.cached_tokens } + elseif (chunk.usage.prompt_cache_hit_tokens or 0) > 0 then + uses.prompt_cache_hit_tokens = chunk.usage.prompt_cache_hit_tokens + uses.prompt_cache_miss_tokens = chunk.usage.prompt_cache_miss_tokens or 0 + uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_cache_hit_tokens } + end + end + + if not chunk.choices or #chunk.choices == 0 then + if uses ~= nil then + return json.encode({ usage = uses, done = false }) + end + return "" + end + local delta = chunk.choices[1].delta or {} + local fr = chunk.choices[1].finish_reason + + local finish = (type(fr) == "string" and fr ~= "") and fr or nil + + local unified = { + content = delta.content or "", + done = (finish ~= nil) + } + if finish then + unified.finish_reason = finish + end + if uses ~= nil then + unified.usage = uses + end + if delta.reasoning_content then + unified.reasoning_content = delta.reasoning_content + end + if delta.tool_calls then + unified.tool_calls = delta.tool_calls + end + return json.encode(unified) +end + +function adapter.transform_error(status, body) + local ok, resp = pcall(json.decode, body) + if not ok or type(resp) ~= "table" then + -- Non-JSON body: extract title from HTML or first line + local title = string.match(body or "", "(.-)") + if title and title ~= "" then return title end + local line = string.match(body or "", "^%s*([^\r\n]+)") + if line and line ~= "" and not string.match(line, "^<") then + return string.sub(line, 1, 200) + end + return nil + end + local e = resp.error + if type(e) == "table" and type(e.message) == "string" then + -- trae quota / rate limit annotations + if e.code then + return tostring(e.code) .. ": " .. e.message + end + return e.message + end + if type(e) == "string" then return e end + if type(resp.message) == "string" and resp.message ~= "" then + if resp.code ~= nil then + return tostring(resp.code) .. ": " .. resp.message + end + return resp.message + end + return nil +end + +return adapter