feat(adapter): move per-source error condensing into transform_error hooks

Every upstream formats errors differently, which is adapter territory:
the protocol gains an optional transform_error(status, body) hook and all
built-in adapters implement their own envelope parsing (zen free-pool
labels, anthropic/gemini/ollama/mistral shapes, sensenova quota notes,
agentrouter WAF pages). The core keeps a single uniform fallback: when no
hook yields a reason clients get "api error <status>: unknown error" and
the raw body goes to server logs only.
This commit is contained in:
JianFeeeee
2026-08-24 19:17:36 +08:00
parent 6eac80bc6c
commit b2183df1e8
17 changed files with 305 additions and 69 deletions

View File

@ -108,4 +108,19 @@ function adapter.transform_stream_chunk(raw_chunk)
return json.encode(unified)
end
return adapter
-- 错误收敛:前置 WAF 会返回整页 HTML阿里云盾JSON 时为 new-api 风格
function adapter.transform_error(status, body)
local low = string.lower(body or "")
if string.sub(low, 1, 9) == "<!doctype" or string.find(low, "<html", 1, true) then
return "blocked by AgentRouter WAF"
end
local ok, resp = pcall(json.decode, body)
if not ok or type(resp) ~= "table" then return nil end
local e = resp.error
if type(e) == "table" and type(e.message) == "string" then
return e.message
end
return nil
end
return adapter

View File

@ -180,4 +180,15 @@ function adapter.transform_stream_chunk(raw_chunk)
return ""
end
-- 错误收敛Anthropic 信封 {type:"error", error:{type, message}}
function adapter.transform_error(status, body)
local ok, resp = pcall(json.decode, body)
if not ok or type(resp) ~= "table" then return nil end
if resp.type == "error" and type(resp.error) == "table"
and type(resp.error.message) == "string" then
return resp.error.message
end
return nil
end
return adapter

View File

@ -127,4 +127,15 @@ function adapter.transform_stream_chunk(raw_chunk)
return json.encode(unified)
end
-- 错误收敛DeepSeek 走标准 OpenAI 信封 {error:{message,...}}
function adapter.transform_error(status, body)
local ok, resp = pcall(json.decode, body)
if not ok or type(resp) ~= "table" then return nil end
local e = resp.error
if type(e) == "table" and type(e.message) == "string" then
return e.message
end
return nil
end
return adapter

View File

@ -152,4 +152,20 @@ function adapter.transform_stream_chunk(raw_chunk)
return json.encode(unified)
end
-- 错误收敛Gemini REST 信封 {error:{code, message, status}}
function adapter.transform_error(status, body)
local ok, resp = pcall(json.decode, body)
if not ok or type(resp) ~= "table" then return nil end
local e = resp.error
if type(e) == "table" then
if type(e.message) == "string" then
if type(e.status) == "string" then
return e.status .. ": " .. e.message
end
return e.message
end
end
return nil
end
return adapter

View File

@ -108,4 +108,15 @@ function adapter.transform_stream_chunk(raw_chunk)
return json.encode(unified)
end
-- 错误收敛GitHub Models 走标准 OpenAI 信封 {error:{message,...}}
function adapter.transform_error(status, body)
local ok, resp = pcall(json.decode, body)
if not ok or type(resp) ~= "table" then return nil end
local e = resp.error
if type(e) == "table" and type(e.message) == "string" then
return e.message
end
return nil
end
return adapter

View File

@ -107,4 +107,15 @@ function adapter.transform_stream_chunk(raw_chunk)
return json.encode(unified)
end
-- 错误收敛Groq 走标准 OpenAI 信封 {error:{message,...}}
function adapter.transform_error(status, body)
local ok, resp = pcall(json.decode, body)
if not ok or type(resp) ~= "table" then return nil end
local e = resp.error
if type(e) == "table" and type(e.message) == "string" then
return e.message
end
return nil
end
return adapter

View File

@ -140,4 +140,15 @@ function adapter.transform_stream_chunk(raw_chunk)
return json.encode(unified)
end
return adapter
-- 错误收敛kimi 网关为 OpenAI 风格 {error:{message,...}}
function adapter.transform_error(status, body)
local ok, resp = pcall(json.decode, body)
if not ok or type(resp) ~= "table" then return nil end
local e = resp.error
if type(e) == "table" and type(e.message) == "string" then
return e.message
end
return nil
end
return adapter

View File

@ -107,4 +107,14 @@ function adapter.transform_stream_chunk(raw_chunk)
return json.encode(unified)
end
-- 错误收敛Mistral 用顶层 {message="...", type="..."}(非嵌套 error
function adapter.transform_error(status, body)
local ok, resp = pcall(json.decode, body)
if not ok or type(resp) ~= "table" then return nil end
if type(resp.message) == "string" then
return resp.message
end
return nil
end
return adapter

View File

@ -131,4 +131,15 @@ function adapter.transform_stream_chunk(raw_chunk)
return json.encode(unified)
end
-- 错误收敛Ollama 常见 {error:"..."} 字符串(新版本也有对象形态)
function adapter.transform_error(status, body)
local ok, resp = pcall(json.decode, body)
if not ok or type(resp) ~= "table" then return nil end
if type(resp.error) == "string" then return resp.error end
if type(resp.error) == "table" and type(resp.error.message) == "string" then
return resp.error.message
end
return nil
end
return adapter

View File

@ -111,4 +111,16 @@ function adapter.transform_stream_chunk(raw_chunk)
return json.encode(unified)
end
-- 错误收敛:标准 OpenAI 信封 {error:{message,...}}
function adapter.transform_error(status, body)
local ok, resp = pcall(json.decode, body)
if not ok or type(resp) ~= "table" then return nil end
local e = resp.error
if type(e) == "table" and type(e.message) == "string" then
return e.message
end
if type(e) == "string" then return e end
return nil
end
return adapter

View File

@ -183,4 +183,17 @@ function adapter.transform_stream_chunk(raw_chunk)
return json.encode(unified)
end
-- 错误收敛可选钩子zen 错误信封固定为 {error={type,message}}
-- 免费池限流 FreeUsageLimitError 单独标注。返回 nil 走通用兜底。
function adapter.transform_error(status, body)
local ok, resp = pcall(json.decode, body)
if not ok or type(resp) ~= "table" then return nil end
local e = resp.error
if type(e) ~= "table" then return nil end
if e.type == "FreeUsageLimitError" then
return "zen free pool quota exhausted"
end
return e.message
end
return adapter

View File

@ -105,4 +105,19 @@ function adapter.transform_stream_chunk(raw_chunk)
return json.encode(unified)
end
-- 错误收敛sensenova 为 OpenAI 风格 {error:{message,...}}
-- 配额类错误单独点出便于客户端识别重置周期。
function adapter.transform_error(status, body)
local ok, resp = pcall(json.decode, body)
if not ok or type(resp) ~= "table" then return nil end
local e = resp.error
if type(e) ~= "table" then return nil end
if status == 429 and type(e.code) == "string"
and e.code == "insufficient_quota" then
return "workspace quota exhausted (resets periodically)"
end
if type(e.message) == "string" then return e.message end
return nil
end
return adapter

View File

@ -407,6 +407,46 @@ func (v *VM) BuildHeaders(name string, meta map[string]interface{}) (map[string]
return headers, nil
}
// TransformError executes the optional adapter.transform_error(status, body)
// hook: per-source condensing of an upstream error response into a short
// client-facing reason. ok=false means the adapter defines no hook (or it
// failed) — the caller falls back to the generic Go-side condenser.
func (v *VM) TransformError(name string, status int, body string) (reason string, ok bool, err error) {
p := v.pool(name)
if p == nil {
return "", false, fmt.Errorf("adapter %s not loaded", name)
}
w, err := p.acquire()
if err != nil {
return "", false, err
}
defer p.release(w)
L := w.L
L.SetTop(0)
defer L.SetTop(0)
L.GetGlobal(adapterGlobal)
if L.IsNil(-1) {
return "", false, nil
}
L.GetField(-1, "transform_error")
if !L.IsFunction(-1) {
return "", false, nil
}
L.SetTop(0)
L.GetGlobal(adapterGlobal)
L.GetField(-1, "transform_error")
L.PushInteger(int64(status))
L.PushString(body)
if callErr := L.Call(2, 1); callErr != nil {
return "", false, fmt.Errorf("transform_error: %w", callErr)
}
if L.Type(-1) != golua.LUA_TSTRING {
return "", false, nil
}
return L.ToString(-1), true, nil
}
func (v *VM) Endpoint(name string) string {
p := v.pool(name)
if p == nil {