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", `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) }