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

@ -55,6 +55,7 @@ state may be shared across workers (`log` to stdout is the only side effect).
| `version` | string | no | Version, shown in the WebUI |
| `endpoint` | string | no | Upstream path, default `/chat/completions`; overridable by `source.endpoint` / `source.image_endpoint` |
| `headers` | table | no | Static default request headers; used as fallback when no `build_headers` hook is defined |
| `transform_error(status, body)` | function | no | Error-response condensing: return a one-line reason; on nil / absence clients uniformly receive `unknown error` (raw body goes to server logs only) |
These fields are extracted statically at load time (compile-once); reading them never
occupies a pooled worker.
@ -147,6 +148,27 @@ end
## 4. Optional Hooks
### `transform_error(status, body) -> string | nil`
Condense this source's error response into a one-line reason. Every upstream
formats errors differently — that is adapter territory: all built-in adapters
implement their own envelope parsing (zen's `{error={type,message}}`,
Anthropic's `{type="error",error={...}}`, Gemini's
`{error={code,message,status}}`, Ollama's string `{error="..."}`, etc.).
When the hook is absent or returns nil the core does not guess: clients get
`api error <status>: unknown error` and the raw body is logged server-side only.
```lua
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.error and resp.error.type == "FreeUsageLimitError" then
return "zen free pool quota exhausted"
end
return resp.error and resp.error.message or nil
end
```
### `build_headers(meta) -> table<string,string>`
Dynamically generate / sign request headers (e.g. KimiCode's HMAC signature). If the

View File

@ -50,6 +50,7 @@ return adapter
| `version` | string | 否 | 版本号,用于 WebUI 展示 |
| `endpoint` | string | 否 | 上游请求路径,默认 `/chat/completions`;可被 `source.endpoint` / `source.image_endpoint` 覆盖 |
| `headers` | table | 否 | 静态默认请求头;若未定义 `build_headers` 钩子则作为请求头回退 |
| `transform_error(status, body)` | function | 否 | 错误响应收敛:返回一行短原因;返回 nil / 未定义时客户端统一收到 `unknown error`(原始响应体只进服务端日志) |
这些字段在加载时静态提取compile-once之后读它们不会占用池内 worker。
@ -138,6 +139,26 @@ end
## 4. 可选钩子
### `transform_error(status, body) -> string | nil`
把该源特有的错误响应收敛成一行短原因。每个上游的错误格式不同——这是适配器层
的职责:内置各适配器均实现了自己的信封解析(如 zen 的
`{error={type,message}}`、Anthropic 的 `{type="error",error={...}}`、Gemini 的
`{error={code,message,status}}`、Ollama 的字符串 `{error="..."}` 等)。
未定义本钩子或返回 nil 时,核心不猜测格式,客户端统一收到
`api error <status>: unknown error`,原始响应体仅记录在服务端日志。
```lua
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.error and resp.error.type == "FreeUsageLimitError" then
return "zen free pool quota exhausted"
end
return resp.error and resp.error.message or nil
end
```
### `build_headers(meta) -> table<string,string>`
动态生成/签名请求头(如 KimiCode 的 HMAC 签名。若脚本未定义此函数Go 层回退使用静态