Files
easyai-ai-gateway/apps/api/internal/httpapi/public_errors.go
T
wangbo ebdb96e7d7 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。
2026-08-05 00:40:42 +08:00

177 lines
5.1 KiB
Go

package httpapi
import (
"net/http"
"strings"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/publicerror"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func publicGatewayTask(task store.GatewayTask) store.GatewayTask {
if task.Status == "failed" || task.Status == "cancelled" || task.ErrorCode != "" || task.ErrorMessage != "" {
value := publicerror.FromFields(task.ErrorCode, firstNonEmpty(task.ErrorMessage, task.Error), taskErrorHTTPStatus(task), false)
value = mergeStoredPublicError(task.PublicError, value)
value = publicerror.WithIDs(value, task.RequestID, task.ID)
publicerror.Observe(value)
task.PublicError = &value
task.ErrorCode = value.Code
task.ErrorMessage = value.Message
task.Error = value.Message
}
task.Attempts = append([]store.TaskAttempt(nil), task.Attempts...)
for index := range task.Attempts {
attempt := &task.Attempts[index]
if attempt.ErrorCode == "" && attempt.ErrorMessage == "" {
continue
}
status := attempt.StatusCode
if status <= 0 {
status = http.StatusBadGateway
}
value := publicerror.FromFields(attempt.ErrorCode, attempt.ErrorMessage, status, attempt.Retryable)
value = mergeStoredPublicError(attempt.PublicError, value)
value = publicerror.WithIDs(value, attempt.RequestID, task.ID)
attempt.PublicError = &value
attempt.ErrorCode = value.Code
attempt.ErrorMessage = value.Message
}
return task
}
func publicTaskError(task store.GatewayTask) publicerror.Error {
status := taskErrorHTTPStatus(task)
if status <= 0 {
status = http.StatusBadGateway
}
value := publicerror.FromFields(task.ErrorCode, firstNonEmpty(task.ErrorMessage, task.Error, task.Message), status, false)
value = mergeStoredPublicError(task.PublicError, value)
return publicerror.WithIDs(value, task.RequestID, task.ID)
}
func mergeStoredPublicError(stored *publicerror.Error, derived publicerror.Error) publicerror.Error {
if stored == nil || stored.Code == "" || stored.Source == "" {
return derived
}
value := *stored
if len(value.Details) == 0 && len(derived.Details) > 0 {
value.Details = derived.Details
}
if message := safeDerivedUpstreamMessage(derived); message != "" {
value.Message = message
}
return value
}
func safeDerivedUpstreamMessage(value publicerror.Error) string {
upstream, _ := value.Details["upstreamError"].(map[string]any)
message, _ := upstream["message"].(string)
return strings.TrimSpace(message)
}
func taskErrorHTTPStatus(task store.GatewayTask) int {
if task.PublicError != nil && task.PublicError.Source != "" && task.PublicError.HTTPStatus >= 400 {
return task.PublicError.HTTPStatus
}
for index := len(task.Attempts) - 1; index >= 0; index-- {
if status := task.Attempts[index].StatusCode; status >= 400 && status <= 599 {
return status
}
}
if status := publicErrorInt(task.Metrics["statusCode"]); status >= 400 && status <= 599 {
return status
}
return storedTaskErrorStatus(task.ErrorCode)
}
func publicErrorInt(value any) int {
switch typed := value.(type) {
case int:
return typed
case int32:
return int(typed)
case int64:
return int(typed)
case float32:
return int(typed)
case float64:
return int(typed)
default:
return 0
}
}
func publicTaskList(items []store.GatewayTask) []store.GatewayTask {
out := make([]store.GatewayTask, len(items))
for index, task := range items {
out[index] = publicGatewayTask(task)
}
return out
}
func publicErrorMap(value publicerror.Error) map[string]any {
out := map[string]any{
"code": value.Code, "message": value.Message, "category": value.Category, "source": value.Source,
"httpStatus": value.HTTPStatus, "retryable": value.Retryable, "action": value.Action,
"version": value.Version,
}
if value.RetryAfterSeconds > 0 {
out["retryAfterSeconds"] = value.RetryAfterSeconds
}
if requestID := strings.TrimSpace(value.RequestID); requestID != "" {
out["requestId"] = requestID
}
if taskID := strings.TrimSpace(value.TaskID); taskID != "" {
out["taskId"] = taskID
}
if len(value.Details) > 0 {
out["details"] = value.Details
}
return out
}
func publicErrorWithRetryAfter(value publicerror.Error, details map[string]any) publicerror.Error {
if value.RetryAfterSeconds > 0 || details == nil {
return value
}
switch typed := details["retryAfterSeconds"].(type) {
case int:
value.RetryAfterSeconds = typed
case int32:
value.RetryAfterSeconds = int(typed)
case int64:
value.RetryAfterSeconds = int(typed)
case float64:
value.RetryAfterSeconds = int(typed)
}
if value.RetryAfterSeconds < 0 {
value.RetryAfterSeconds = 0
}
return value
}
func safePublicErrorDetails(details map[string]any, value publicerror.Error, includePublicError bool) map[string]any {
out := map[string]any{}
for _, key := range []string{"param", "retryAfterSeconds", "recoveryAt", "rateLimit", "pricing"} {
if item, ok := details[key]; ok && item != nil {
out[key] = item
}
}
if value.Category == "request" {
for _, key := range []string{"reason", "diagnosticId"} {
if item, ok := details[key]; ok && item != nil {
out[key] = item
}
}
}
for key, item := range value.Details {
if item != nil {
out[key] = item
}
}
if includePublicError {
out["publicError"] = value
}
return out
}