mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-21 01:17:59 +00:00
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:
@ -10,6 +10,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
@ -434,7 +435,7 @@ func (p *Provider) probeModels(ctx context.Context) (bool, string) {
|
||||
case resp.StatusCode == 404 || resp.StatusCode == 405:
|
||||
return false, ""
|
||||
default:
|
||||
return false, shortAPIError(resp.StatusCode, string(raw))
|
||||
return false, p.apiErrReason(resp.StatusCode, string(raw))
|
||||
}
|
||||
}
|
||||
|
||||
@ -471,7 +472,7 @@ func (p *Provider) probeChat(ctx context.Context) (bool, string) {
|
||||
} else if status == 200 {
|
||||
ok = true
|
||||
} else {
|
||||
msg = shortAPIError(status, raw)
|
||||
msg = p.apiErrReason(status, raw)
|
||||
}
|
||||
} else {
|
||||
msg = err.Error()
|
||||
@ -717,7 +718,7 @@ func (p *Provider) Chat(ctx context.Context, req *types.ChatRequest) (*types.Uni
|
||||
}
|
||||
if status != 200 {
|
||||
p.ReportStatus(model, status)
|
||||
return nil, fmt.Errorf("%s", shortAPIError(status, raw))
|
||||
return nil, fmt.Errorf("%s", p.apiErrReason(status, raw))
|
||||
}
|
||||
unified, err := p.vm.Transform(p.adapter, "transform_response", raw)
|
||||
if err != nil {
|
||||
@ -793,7 +794,7 @@ func (p *Provider) ChatStream(ctx context.Context, req *types.ChatRequest) (<-ch
|
||||
sel.resp.Body.Close()
|
||||
p.ReportStatus(model, sel.resp.StatusCode)
|
||||
p.Release()
|
||||
return nil, fmt.Errorf("%s", shortAPIError(sel.resp.StatusCode, string(raw)))
|
||||
return nil, fmt.Errorf("%s", p.apiErrReason(sel.resp.StatusCode, string(raw)))
|
||||
}
|
||||
go func() {
|
||||
defer p.Release()
|
||||
@ -928,48 +929,6 @@ func errorOnlyChunk(ck types.UnifiedChunk) bool {
|
||||
ck.ReasoningContent == "" && ck.Usage == nil
|
||||
}
|
||||
|
||||
// shortAPIError condenses an upstream error response into its human reason:
|
||||
// JSON envelopes contribute their error/message field, HTML pages (WAF
|
||||
// blocklists) collapse to a marker, anything else is capped as-is. Raw
|
||||
// bodies must not leak through errors to clients.
|
||||
func shortAPIError(status int, body string) string {
|
||||
b := strings.TrimSpace(body)
|
||||
low := strings.ToLower(b)
|
||||
if strings.HasPrefix(low, "<!doctype") || strings.Contains(low, "<html") {
|
||||
return fmt.Sprintf("api error %d: upstream returned an HTML error page", status)
|
||||
}
|
||||
var env struct {
|
||||
Error json.RawMessage `json:"error"`
|
||||
Message string `json:"message"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(b), &env); err == nil {
|
||||
msg := ""
|
||||
switch {
|
||||
case len(env.Error) > 0:
|
||||
var es string
|
||||
if json.Unmarshal(env.Error, &es) == nil {
|
||||
msg = es
|
||||
} else {
|
||||
var obj struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if json.Unmarshal(env.Error, &obj) == nil {
|
||||
msg = obj.Message
|
||||
}
|
||||
}
|
||||
case env.Message != "":
|
||||
msg = env.Message
|
||||
case env.Msg != "":
|
||||
msg = env.Msg
|
||||
}
|
||||
if msg != "" {
|
||||
return fmt.Sprintf("api error %d: %s", status, oneLineStr(msg, 160))
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("api error %d: %s", status, oneLineStr(b, 160))
|
||||
}
|
||||
|
||||
func oneLineStr(s string, n int) string {
|
||||
s = strings.Join(strings.Fields(s), " ")
|
||||
if len(s) > n {
|
||||
@ -977,6 +936,20 @@ func oneLineStr(s string, n int) string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
// apiErrReason builds the client-facing reason for a non-200 upstream
|
||||
// response: the adapter's optional transform_error hook wins (per-source
|
||||
// protocol knowledge lives in Lua), otherwise clients get a uniform
|
||||
// "unknown error" while the raw body stays in the server log for debugging.
|
||||
func (p *Provider) apiErrReason(status int, raw string) string {
|
||||
if reason, ok, err := p.vm.TransformError(p.adapter, status, raw); err == nil && ok {
|
||||
if trimmed := strings.TrimSpace(reason); trimmed != "" {
|
||||
return fmt.Sprintf("api error %d: %s", status, oneLineStr(trimmed, 200))
|
||||
}
|
||||
}
|
||||
log.Printf("[provider] unhandled upstream error body (adapter %q lacks transform_error): status=%d body=%.300s",
|
||||
p.adapter, status, oneLineStr(raw, 300))
|
||||
return fmt.Sprintf("api error %d: unknown error", status)
|
||||
}
|
||||
|
||||
// Image generates images via /v1/images/generations. Same scheduling-state
|
||||
// accounting as Chat: fail fast on busy, record per (source, model).
|
||||
@ -1015,7 +988,7 @@ func (p *Provider) Image(ctx context.Context, req *types.ImageGenRequest) (*type
|
||||
}
|
||||
if status != 200 {
|
||||
p.ReportStatus(model, status)
|
||||
return nil, fmt.Errorf("image api error %d: %s", status, truncate(raw, 500))
|
||||
return nil, fmt.Errorf("%s", p.apiErrReason(status, raw))
|
||||
}
|
||||
var out types.UnifiedResponse
|
||||
// try adapter transform_response; if missing, parse standard openai image format
|
||||
|
||||
Reference in New Issue
Block a user