mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 17:10:43 -10:00
fix(error): surface the provider's reason on 400/422
A 400/422 collapsed to one generic localized line, discarding the body the provider actually returned (context-length exceeded, unpaired tool_calls, …). Append that reason for the request-shaped 4xx so the failure is diagnosable; other statuses keep their actionable fix text.
This commit is contained in:
1 parent
5711efc4ea
commit
f920da106a
2 files changed
+58
-3
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
@@ -17,10 +18,14 @@ func explainError(err error) error {
|
||||
}
|
||||
var apiErr *provider.APIError
|
||||
if errors.As(err, &apiErr) {
|
||||
if msg := i18n.M.ProviderStatusMessage(apiErr.Status); msg != "" {
|
||||
return errors.New(msg)
|
||||
msg := i18n.M.ProviderStatusMessage(apiErr.Status)
|
||||
if msg == "" {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
if reason := requestErrorReason(apiErr); reason != "" {
|
||||
return fmt.Errorf("%s\n%s", msg, reason)
|
||||
}
|
||||
return errors.New(msg)
|
||||
}
|
||||
var authErr *provider.AuthError
|
||||
if errors.As(err, &authErr) {
|
||||
@@ -35,3 +40,38 @@ func explainError(err error) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// requestErrorReason returns the provider's verbatim reason for request-shaped
|
||||
// 4xx (400/422) — the localized line names the category, the body names the
|
||||
// actual cause (context-length exceeded, unpaired tool_calls). Empty otherwise.
|
||||
func requestErrorReason(e *provider.APIError) string {
|
||||
if e.Status != 400 && e.Status != 422 {
|
||||
return ""
|
||||
}
|
||||
return providerBodyReason(e.Body)
|
||||
}
|
||||
|
||||
// providerBodyReason pulls the human reason from an OpenAI/Anthropic-shaped error
|
||||
// body ({"error":{"message":…}}), falling back to the trimmed raw body.
|
||||
func providerBodyReason(body string) string {
|
||||
if body == "" {
|
||||
return ""
|
||||
}
|
||||
var parsed struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if json.Unmarshal([]byte(body), &parsed) == nil && parsed.Error.Message != "" {
|
||||
return clampRunes(parsed.Error.Message, 800)
|
||||
}
|
||||
return clampRunes(body, 800)
|
||||
}
|
||||
|
||||
func clampRunes(s string, max int) string {
|
||||
r := []rune(s)
|
||||
if len(r) <= max {
|
||||
return s
|
||||
}
|
||||
return string(r[:max]) + "…"
|
||||
}
|
||||
@@ -31,6 +31,21 @@ func TestExplainError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
jsonBody := explainError(&provider.APIError{Provider: "deepseek", Status: 400, Body: `{"error":{"message":"This model's maximum context length is 65536 tokens.","type":"invalid_request_error"}}`})
|
||||
if !strings.Contains(jsonBody.Error(), i18n.M.ProviderErrBadRequest) || !strings.Contains(jsonBody.Error(), "maximum context length") {
|
||||
t.Errorf("400 should append the provider reason from a JSON body, got %q", jsonBody.Error())
|
||||
}
|
||||
|
||||
rawBody := explainError(&provider.APIError{Provider: "deepseek", Status: 422, Body: "some unparseable detail"})
|
||||
if !strings.Contains(rawBody.Error(), "some unparseable detail") {
|
||||
t.Errorf("422 should fall back to the raw body, got %q", rawBody.Error())
|
||||
}
|
||||
|
||||
noLeak := explainError(&provider.APIError{Provider: "deepseek", Status: 429, Body: `{"error":{"message":"slow down"}}`})
|
||||
if noLeak.Error() != i18n.M.ProviderErrRateLimited {
|
||||
t.Errorf("429 body must not leak into the message, got %q", noLeak.Error())
|
||||
}
|
||||
|
||||
plain := errors.New("some other failure")
|
||||
if explainError(plain) != plain {
|
||||
t.Error("unknown errors should pass through unchanged")
|
||||
|
||||
Reference in new issue
Block a user