From aea1a2c48d07c590e7be44d14c2fe3a0682e7d9e Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 17 Oct 2026 00:37:32 +0800 Subject: [PATCH] =?UTF-8?q?fix(provider):=20=E4=B8=8A=E6=B8=B8=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=96=87=E6=A1=88=E5=88=AB=E5=86=8D=E4=B8=A2=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E8=A1=A5=E7=BD=91=E5=85=B3/=E8=85=BE?= =?UTF-8?q?=E8=AE=AF=E9=94=99=E8=AF=AF=E4=BD=93=E5=BD=A2=E7=8A=B6=20+=20?= =?UTF-8?q?=E4=BF=AE=E8=AF=AF=E5=AF=BC=E6=80=A7=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 发现路径:读生产日志(不是测试报红) ``` unhandled upstream error body (adapter "openai" lacks transform_error): status=404 body={"error_msg":"404 Route Not Found"} unhandled upstream error body (adapter "buddy" lacks transform_error): status=400 body={"code":11101,"msg":"Non-stream chat request is currently not supported",...} ``` ### ★ 缺陷 A:错误体形状漏了「网关」与「腾讯系」两类 `__prelude.openai_error()` 只认 `error.message` / `error`(裸串) / `{code,message}` / `detail` / HTML `` / 裸文本 六种,**没有** `error_msg`(华为/APISIX 网关) 与 `msg`(腾讯系用 msg 而非 message)⇒ 返回 nil ⇒ 客户端只看到 `api error 404: unknown error`,**而 body 里明明有可用文案**。 修法照参考(llm-adapter.ts:524 `errorDetail()` 显式认 `error_code`/`error_msg`; buddy-adapter.ts:576 认 `msg` + `displayMsg.{zh,en}`): - 新增 `{error_msg}` / `{msg}` / `{error_code,+error_msg}` 三种形状; - 新增 `displayMsg.{zh,en}` 本地化文案,**排在最前**(参考注释明说"本地化文案 最可读";其早期实现只读 `error.*`/`message` ⇒ 全部落空 ⇒ 把 291 字符原始 报文糊进错误提示,而服务端早已备好中文说明)。 覆盖 7 个用共享前导的适配器:buddy/deepseek/github/groq/kimicode/openai/sensenova。 ### ★ 缺陷 B:内核那句日志会把人带偏 措辞是 `adapter %q lacks transform_error`,但 **openai.lua:139 与 buddy.lua:166 都明明定义了该钩子** —— 真实原因只是它**返回了 nil**(认不出那个形状)。 我照着这句话去适配器里找"有没有这个函数",查了几轮才发现函数在、是返回值空。 修法:内核直接查事实(`p.vm.HasFn(adapter, "transform_error")`), 两种原因**分开措辞**,不再猜。 ## 判据 `internal/lua/adapter_prelude_error_shapes_test.go`: - 12 个形状子用例(4 个新增 + 6 个回归 + 2 个 displayMsg), **装载仓库里真实的 openai.lua**(含共享前导),不是自造桩 —— 第一版用空 VM 跑,12 个全 FAIL(`adapter openai not loaded`), 那等于什么也没验。 - 配对判据 `...StillReportsUnknownShapes`:真正认不出的形状仍须 `ok=false`, 防止"为了让上面通过就把『抽不出』和『没这个钩子』混为一谈"。 - 变异:删掉新增的 error_msg/msg 分支 ⇒ APISIX 用例报红。 ## 备注 `anthropic/gemini/mistral/ollama/opencodezen/qwen38/trae` 有自己写的 `transform_error`,不走共享前导,**未获得**本次新增形状(各自协议的错误体 本就不同)。此处只修共享前导的 7 个。 --- internal/lua/adapter_prelude.go | 33 +++++++ .../lua/adapter_prelude_error_shapes_test.go | 99 +++++++++++++++++++ internal/provider/provider.go | 14 ++- 3 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 internal/lua/adapter_prelude_error_shapes_test.go diff --git a/internal/lua/adapter_prelude.go b/internal/lua/adapter_prelude.go index c1a0c59..c2243b7 100644 --- a/internal/lua/adapter_prelude.go +++ b/internal/lua/adapter_prelude.go @@ -343,6 +343,39 @@ function __prelude.openai_error(status, body) return resp.detail end + -- 7/8/9. 网关与腾讯系形状(实测漏过,见函数头注释)。 + -- error_code 与 error_msg 同现时拼起来 —— 只看 msg 会丢掉可分类的码 + -- (参考 llm-adapter.ts:531 把两者都放进 parts 再 join)。 + local msg = nil + if type(resp.error_msg) == "string" and resp.error_msg ~= "" then + msg = resp.error_msg + elseif type(resp.msg) == "string" and resp.msg ~= "" then + msg = resp.msg + end + -- 腾讯系(buddy/workbuddy)用 msg + displayMsg.{zh,en},且**没有** message。 + -- 参考 buddy-adapter.ts:576 明确:本地化文案最可读,排在**最前**。 + local localized = nil + if type(resp.displayMsg) == "table" then + local dm = resp.displayMsg + if type(dm.zh) == "string" and dm.zh ~= "" then localized = dm.zh + elseif type(dm.en) == "string" and dm.en ~= "" then localized = dm.en end + end + if msg then + local code = nil + if type(resp.error_code) == "string" and resp.error_code ~= "" then + code = resp.error_code + elseif resp.code ~= nil and type(resp.code) ~= "string" then + code = tostring(resp.code) + end + local detail = code and (code .. ": " .. msg) or msg + if localized then return localized .. " " .. detail end + return detail + end + if localized then return localized end + if type(resp.error_code) == "string" and resp.error_code ~= "" then + return resp.error_code + end + return nil end diff --git a/internal/lua/adapter_prelude_error_shapes_test.go b/internal/lua/adapter_prelude_error_shapes_test.go new file mode 100644 index 0000000..2d006e1 --- /dev/null +++ b/internal/lua/adapter_prelude_error_shapes_test.go @@ -0,0 +1,99 @@ +package lua + +import ( + "testing" +) + +// ★ 上游错误体抽取必须覆盖**网关与腾讯系**形状 —— 这两类实测都漏过。 +// +// 发现路径(2026-10-16 读生产日志): +// +// unhandled upstream error body (adapter "openai" lacks transform_error): +// status=404 body={"error_msg":"404 Route Not Found"} +// unhandled upstream error body (adapter "buddy" lacks transform_error): +// status=400 body={"code":11101,"msg":"Non-stream chat request is not supported"} +// +// 两者都是 `{"error_msg":...}` / `{"msg":...}` —— 旧实现只认 +// error/message/detail,于是返回 nil ⇒ 客户端只看到 +// `api error 404: unknown error`,而 body 里明明有可用文案。 +// +// ⚠️ 那两个适配器**都定义了** transform_error(openai.lua:139、buddy.lua:166), +// 内核却报 "lacks" ⇒ 一句会把排查带偏的诊断(已另行修正内核文案)。 +func TestPreludeErrorExtractionCoversGatewayShapes(t *testing.T) { + vm := NewVM(t.TempDir()) + defer vm.Stop() + // 用**仓库里的真实** openai 适配器(含共享前导),不是自造桩: + // 判据必须走真实实现,否则"补了形状"这件事根本没被验。 + src, err := bundledAdapters.ReadFile("adapters/openai.lua") + if err != nil { + t.Fatal(err) + } + if err := vm.LoadAdapterSource("openai", string(src)); err != nil { + t.Fatalf("装载 openai 适配器: %v", err) + } + + cases := []struct { + name, body, want string + }{ + {"APISIX error_msg", + `{"error_msg":"404 Route Not Found"}`, "404 Route Not Found"}, + {"腾讯 msg(buddy 11101)", + `{"code":11101,"msg":"Non-stream chat request is currently not supported"}`, + "11101: Non-stream chat request is currently not supported"}, + {"error_code + error_msg", + `{"error_code":"APIG.0301","error_msg":"verify ak sk signature fail"}`, + "APIG.0301: verify ak sk signature fail"}, + {"腾讯 displayMsg 本地化(最可读,排最前)", + `{"code":11101,"msg":"Non-stream chat request is currently not supported",` + + `"displayMsg":{"en":"Invalid request. Please retry","zh":"请求内容不合法,请重试"}}`, + "请求内容不合法,请重试 11101: Non-stream chat request is currently not supported"}, + {"仅 displayMsg", + `{"displayMsg":{"zh":"只有本地化文案"}}`, "只有本地化文案"}, + {"仅 error_code", + `{"error_code":"InferHub.4291.200"}`, "InferHub.4291.200"}, + // 回归:原有的 6 种形状不得丢 + {"OpenAI 信封", `{"error":{"message":"boom"}}`, "boom"}, + {"裸字符串 error", `{"error":"boom"}`, "boom"}, + {"扁平 code/message", `{"message":"boom"}`, "boom"}, + {"FastAPI detail", `{"detail":"boom"}`, "boom"}, + {"HTML title", `<html><title>413 Too Large`, "413 Too Large"}, + {"裸文本", "plain failure line\nmore", "plain failure line"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got, ok, err := vm.TransformError("openai", 400, c.body) + if err != nil { + t.Fatalf("TransformError: %v", err) + } + if !ok { + t.Fatalf("★ 抽不出消息(返回 ok=false)⇒ 客户端只会看到 "+ + "'unknown error',而 body 里明明有:%s", c.body) + } + if got != c.want { + t.Fatalf("抽出的文案不对\n 实得: %q\n 期望: %q", got, c.want) + } + }) + } +} + +// 配对判据:真正**没有** transform_error 的适配器仍应返回 ok=false +// (不能为了让上面那条通过就把"抽不出"和"没这个钩子"混为一谈)。 +func TestPreludeErrorExtractionStillReportsUnknownShapes(t *testing.T) { + vm := NewVM(t.TempDir()) + defer vm.Stop() + src, err := bundledAdapters.ReadFile("adapters/openai.lua") + if err != nil { + t.Fatal(err) + } + if err := vm.LoadAdapterSource("openai", string(src)); err != nil { + t.Fatalf("装载 openai 适配器: %v", err) + } + // 一个确实认不出的 JSON(没有 message/error/detail/msg/error_msg) + got, ok, err := vm.TransformError("openai", 400, `{"weird":true}`) + if err != nil { + t.Fatalf("TransformError: %v", err) + } + if ok { + t.Fatalf("认不出的形状不该硬凑文案,实得 %q", got) + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index ac0fc1a..294d65a 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1459,8 +1459,18 @@ func (p *Provider) apiErrReason(status int, raw string) string { return fmt.Sprintf("api error %d: %s", status, types.OneLine(trimmed, 200)) } } - log.Printf("[provider] unhandled upstream error body (adapter %q lacks transform_error): status=%d body=%.300s", - p.adapter, status, types.OneLine(raw, 300)) + // ⚠️ 措辞必须**区分两种原因**(实测教训):适配器**有** transform_error 钩子 + // 但它返回 nil(不认识的错误体)是**常见**情况,而"真的没这个钩子"是另一回事。 + // 原先一律说 "lacks transform_error" —— 我照着这句话去适配器里找那个函数, + // 查了几轮才发现函数明明在、只是抽不出消息。 + // 这里直接问内核要事实,不再猜(判据也据此断言)。 + if p.vm.HasFn(p.adapter, "transform_error") { + log.Printf("[provider] unhandled upstream error body (adapter %q: transform_error returned no reason for this shape): status=%d body=%.300s", + p.adapter, status, types.OneLine(raw, 300)) + } else { + log.Printf("[provider] unhandled upstream error body (adapter %q defines no transform_error): status=%d body=%.300s", + p.adapter, status, types.OneLine(raw, 300)) + } return fmt.Sprintf("api error %d: unknown error", status) }