fix(provider): 修正媒体请求转换与上游错误透传
按上游协议能力延迟处理媒体资源:OpenAI 兼容平台默认使用 multipart,显式配置后才发送 JSON URL;Gemini 官方协议使用 Files API,兼容协议使用内嵌 Base64,并同步覆盖相关媒体客户端。\n\n安全的上游 400/422 原始错误会作为下游 message 返回,同时保留结构化诊断信息和历史任务兼容。\n\n验证:API 全量无缓存测试、go vet、pnpm lint、pnpm test、pnpm build、pnpm openapi、git diff --check。
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package publicerror
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -9,17 +10,18 @@ import (
|
||||
const Version = "v1"
|
||||
|
||||
type Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Category string `json:"category"`
|
||||
Source string `json:"source"`
|
||||
HTTPStatus int `json:"httpStatus"`
|
||||
Retryable bool `json:"retryable"`
|
||||
Action string `json:"action"`
|
||||
RetryAfterSeconds int `json:"retryAfterSeconds,omitempty"`
|
||||
RequestID string `json:"requestId,omitempty"`
|
||||
TaskID string `json:"taskId,omitempty"`
|
||||
Version string `json:"version"`
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Category string `json:"category"`
|
||||
Source string `json:"source"`
|
||||
HTTPStatus int `json:"httpStatus"`
|
||||
Retryable bool `json:"retryable"`
|
||||
Action string `json:"action"`
|
||||
RetryAfterSeconds int `json:"retryAfterSeconds,omitempty"`
|
||||
RequestID string `json:"requestId,omitempty"`
|
||||
TaskID string `json:"taskId,omitempty"`
|
||||
Details map[string]any `json:"details,omitempty"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func FromFields(code string, message string, status int, retryable bool) Error {
|
||||
@@ -115,7 +117,7 @@ func mappedError(code string, message string, status int, retryable bool) (Error
|
||||
return newError("upstream_unavailable", "The upstream service is temporarily unavailable.", "upstream", http.StatusServiceUnavailable, true, "retry"), true
|
||||
}
|
||||
if strings.HasPrefix(code, "http_") || (code == "provider_failed" && status > 0 && status < 500) {
|
||||
return upstreamHTTPError(status), true
|
||||
return upstreamHTTPError(status, originalUpstreamErrorDetail(code, message, status)), true
|
||||
}
|
||||
if status >= 500 && code != "upstream_submission_unknown" && (strings.Contains(code, "upstream") || strings.Contains(code, "provider")) {
|
||||
return newError("upstream_unavailable", "The upstream service is temporarily unavailable.", "upstream", http.StatusServiceUnavailable, true, "retry"), true
|
||||
@@ -152,27 +154,66 @@ func newError(code string, message string, category string, status int, retryabl
|
||||
return Error{Code: code, Message: message, Category: category, Source: errorSource(code, category), HTTPStatus: status, Retryable: retryable, Action: action, Version: Version}
|
||||
}
|
||||
|
||||
func upstreamHTTPError(status int) Error {
|
||||
func upstreamHTTPError(status int, details map[string]any) Error {
|
||||
if status < 400 || status >= 500 {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
var value Error
|
||||
switch status {
|
||||
case http.StatusBadRequest:
|
||||
return newError("upstream_invalid_request", "The upstream service rejected the request parameters.", "upstream", status, false, "fix_request")
|
||||
value = newError("upstream_invalid_request", "The upstream service rejected the request parameters.", "upstream", status, false, "fix_request")
|
||||
case http.StatusNotFound:
|
||||
return newError("upstream_not_found", "The upstream service could not find the requested resource.", "upstream", status, false, "contact_support")
|
||||
value = newError("upstream_not_found", "The upstream service could not find the requested resource.", "upstream", status, false, "contact_support")
|
||||
case http.StatusMethodNotAllowed:
|
||||
return newError("upstream_method_not_allowed", "The upstream service did not accept the configured request method.", "upstream", status, false, "contact_support")
|
||||
value = newError("upstream_method_not_allowed", "The upstream service did not accept the configured request method.", "upstream", status, false, "contact_support")
|
||||
case http.StatusConflict:
|
||||
return newError("upstream_conflict", "The upstream service reported a request conflict.", "upstream", status, false, "fix_request")
|
||||
value = newError("upstream_conflict", "The upstream service reported a request conflict.", "upstream", status, false, "fix_request")
|
||||
case http.StatusRequestEntityTooLarge:
|
||||
return newError("upstream_payload_too_large", "The upstream service rejected the request because its payload was too large.", "upstream", status, false, "fix_request")
|
||||
value = newError("upstream_payload_too_large", "The upstream service rejected the request because its payload was too large.", "upstream", status, false, "fix_request")
|
||||
case http.StatusUnsupportedMediaType:
|
||||
return newError("upstream_unsupported_media_type", "The upstream service rejected the request media type.", "upstream", status, false, "fix_request")
|
||||
value = newError("upstream_unsupported_media_type", "The upstream service rejected the request media type.", "upstream", status, false, "fix_request")
|
||||
case http.StatusUnprocessableEntity:
|
||||
return newError("upstream_unprocessable_request", "The upstream service could not process the request parameters.", "upstream", status, false, "fix_request")
|
||||
value = newError("upstream_unprocessable_request", "The upstream service could not process the request parameters.", "upstream", status, false, "fix_request")
|
||||
default:
|
||||
return newError("upstream_request_rejected", "The upstream service rejected the request.", "upstream", status, false, "fix_request")
|
||||
value = newError("upstream_request_rejected", "The upstream service rejected the request.", "upstream", status, false, "fix_request")
|
||||
}
|
||||
if message := originalUpstreamMessage(details); message != "" {
|
||||
value.Message = message
|
||||
}
|
||||
value.Details = details
|
||||
return value
|
||||
}
|
||||
|
||||
func originalUpstreamMessage(details map[string]any) string {
|
||||
upstream, _ := details["upstreamError"].(map[string]any)
|
||||
message, _ := upstream["message"].(string)
|
||||
return strings.TrimSpace(message)
|
||||
}
|
||||
|
||||
func originalUpstreamErrorDetail(code string, message string, status int) map[string]any {
|
||||
if status != http.StatusBadRequest && status != http.StatusUnprocessableEntity {
|
||||
return nil
|
||||
}
|
||||
message = strings.TrimSpace(strings.ToValidUTF8(message, ""))
|
||||
if message == "" || sensitiveTransportMessage(message) || json.Valid([]byte(message)) {
|
||||
return nil
|
||||
}
|
||||
lower := strings.ToLower(message)
|
||||
for _, marker := range []string{"bearer ", "password=", "secret=", "api_key=", "apikey=", "access_token=", "refresh_token=", "sk-"} {
|
||||
if strings.Contains(lower, marker) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
runes := []rune(message)
|
||||
if len(runes) > 2048 {
|
||||
message = string(runes[:2048])
|
||||
}
|
||||
return map[string]any{
|
||||
"upstreamError": map[string]any{
|
||||
"code": strings.TrimSpace(code),
|
||||
"message": message,
|
||||
"statusCode": status,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user