refactor: Introduce standardized API error

This commit refactors the application's error handling mechanism by introducing a new standardized error type, `types.NewAPIError`. It also renames common JSON utility functions for better clarity.

Previously, internal error handling was tightly coupled to the `dto.OpenAIError` format. This change decouples the internal logic from the external API representation.

Key changes:
- A new `types.NewAPIError` struct is introduced to serve as a canonical internal representation for all API errors.
- All relay adapters (OpenAI, Claude, Gemini, etc.) are updated to return `*types.NewAPIError`.
- Controllers now convert the internal `NewAPIError` to the client-facing `OpenAIError` format at the API boundary, ensuring backward compatibility.
- Channel auto-disable/enable logic is updated to use the new standardized error type.
- JSON utility functions are renamed to align with Go's standard library conventions (e.g., `UnmarshalJson` -> `Unmarshal`, `EncodeJson` -> `Marshal`).
This commit is contained in:
CaIon
2025-07-10 15:02:40 +08:00
parent 0e6b608f91
commit 98952198bb
77 changed files with 1071 additions and 930 deletions

View File

@@ -8,6 +8,7 @@ import (
"one-api/dto"
"one-api/model"
"one-api/setting/operation_setting"
"one-api/types"
"strings"
)
@@ -34,14 +35,17 @@ func EnableChannel(channelId int, channelName string) {
}
}
func ShouldDisableChannel(channelType int, err *dto.OpenAIErrorWithStatusCode) bool {
func ShouldDisableChannel(channelType int, err *types.NewAPIError) bool {
if !common.AutomaticDisableChannelEnabled {
return false
}
if err == nil {
return false
}
if err.LocalError {
if types.IsChannelError(err) {
return true
}
if types.IsLocalError(err) {
return false
}
if err.StatusCode == http.StatusUnauthorized {
@@ -53,7 +57,8 @@ func ShouldDisableChannel(channelType int, err *dto.OpenAIErrorWithStatusCode) b
return true
}
}
switch err.Error.Code {
oaiErr := err.ToOpenAIError()
switch oaiErr.Code {
case "invalid_api_key":
return true
case "account_deactivated":
@@ -63,7 +68,7 @@ func ShouldDisableChannel(channelType int, err *dto.OpenAIErrorWithStatusCode) b
case "pre_consume_token_quota_failed":
return true
}
switch err.Error.Type {
switch oaiErr.Type {
case "insufficient_quota":
return true
case "insufficient_user_quota":
@@ -77,23 +82,19 @@ func ShouldDisableChannel(channelType int, err *dto.OpenAIErrorWithStatusCode) b
return true
}
lowerMessage := strings.ToLower(err.Error.Message)
lowerMessage := strings.ToLower(err.Error())
search, _ := AcSearch(lowerMessage, operation_setting.AutomaticDisableKeywords, true)
if search {
return true
}
return false
return search
}
func ShouldEnableChannel(err error, openaiWithStatusErr *dto.OpenAIErrorWithStatusCode, status int) bool {
func ShouldEnableChannel(err error, newAPIError *types.NewAPIError, status int) bool {
if !common.AutomaticEnableChannelEnabled {
return false
}
if err != nil {
return false
}
if openaiWithStatusErr != nil {
if newAPIError != nil {
return false
}
if status != common.ChannelStatusAutoDisabled {

View File

@@ -163,7 +163,7 @@ func ClaudeToOpenAIRequest(claudeRequest dto.ClaudeRequest, info *relaycommon.Re
oaiToolMessage.SetStringContent(mediaMsg.GetStringContent())
} else {
mediaContents := mediaMsg.ParseMediaContent()
encodeJson, _ := common.EncodeJson(mediaContents)
encodeJson, _ := common.Marshal(mediaContents)
oaiToolMessage.SetStringContent(string(encodeJson))
}
openAIMessages = append(openAIMessages, oaiToolMessage)

View File

@@ -2,11 +2,13 @@ package service
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"one-api/common"
"one-api/dto"
"one-api/types"
"strconv"
"strings"
)
@@ -25,32 +27,32 @@ func MidjourneyErrorWithStatusCodeWrapper(code int, desc string, statusCode int)
}
}
// OpenAIErrorWrapper wraps an error into an OpenAIErrorWithStatusCode
func OpenAIErrorWrapper(err error, code string, statusCode int) *dto.OpenAIErrorWithStatusCode {
text := err.Error()
lowerText := strings.ToLower(text)
if !strings.HasPrefix(lowerText, "get file base64 from url") && !strings.HasPrefix(lowerText, "mime type is not supported") {
if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") {
common.SysLog(fmt.Sprintf("error: %s", text))
text = "请求上游地址失败"
}
}
openAIError := dto.OpenAIError{
Message: text,
Type: "new_api_error",
Code: code,
}
return &dto.OpenAIErrorWithStatusCode{
Error: openAIError,
StatusCode: statusCode,
}
}
func OpenAIErrorWrapperLocal(err error, code string, statusCode int) *dto.OpenAIErrorWithStatusCode {
openaiErr := OpenAIErrorWrapper(err, code, statusCode)
openaiErr.LocalError = true
return openaiErr
}
//// OpenAIErrorWrapper wraps an error into an OpenAIErrorWithStatusCode
//func OpenAIErrorWrapper(err error, code string, statusCode int) *dto.OpenAIErrorWithStatusCode {
// text := err.Error()
// lowerText := strings.ToLower(text)
// if !strings.HasPrefix(lowerText, "get file base64 from url") && !strings.HasPrefix(lowerText, "mime type is not supported") {
// if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") {
// common.SysLog(fmt.Sprintf("error: %s", text))
// text = "请求上游地址失败"
// }
// }
// openAIError := dto.OpenAIError{
// Message: text,
// Type: "new_api_error",
// Code: code,
// }
// return &dto.OpenAIErrorWithStatusCode{
// Error: openAIError,
// StatusCode: statusCode,
// }
//}
//
//func OpenAIErrorWrapperLocal(err error, code string, statusCode int) *dto.OpenAIErrorWithStatusCode {
// openaiErr := OpenAIErrorWrapper(err, code, statusCode)
// openaiErr.LocalError = true
// return openaiErr
//}
func ClaudeErrorWrapper(err error, code string, statusCode int) *dto.ClaudeErrorWithStatusCode {
text := err.Error()
@@ -77,43 +79,37 @@ func ClaudeErrorWrapperLocal(err error, code string, statusCode int) *dto.Claude
return claudeErr
}
func RelayErrorHandler(resp *http.Response, showBodyWhenFail bool) (errWithStatusCode *dto.OpenAIErrorWithStatusCode) {
errWithStatusCode = &dto.OpenAIErrorWithStatusCode{
func RelayErrorHandler(resp *http.Response, showBodyWhenFail bool) (newApiErr *types.NewAPIError) {
newApiErr = &types.NewAPIError{
StatusCode: resp.StatusCode,
Error: dto.OpenAIError{
Type: "upstream_error",
Code: "bad_response_status_code",
Param: strconv.Itoa(resp.StatusCode),
},
}
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return
}
common.CloseResponseBodyGracefully(resp)
var errResponse dto.GeneralErrorResponse
err = json.Unmarshal(responseBody, &errResponse)
err = common.Unmarshal(responseBody, &errResponse)
if err != nil {
if showBodyWhenFail {
errWithStatusCode.Error.Message = string(responseBody)
newApiErr.Err = fmt.Errorf("bad response status code %d, body: %s", resp.StatusCode, string(responseBody))
} else {
errWithStatusCode.Error.Message = fmt.Sprintf("bad response status code %d", resp.StatusCode)
newApiErr.Err = fmt.Errorf("bad response status code %d", resp.StatusCode)
}
return
}
if errResponse.Error.Message != "" {
// OpenAI format error, so we override the default one
errWithStatusCode.Error = errResponse.Error
// General format error (OpenAI, Anthropic, Gemini, etc.)
newApiErr = types.WithOpenAIError(errResponse.Error, resp.StatusCode)
} else {
errWithStatusCode.Error.Message = errResponse.ToMessage()
}
if errWithStatusCode.Error.Message == "" {
errWithStatusCode.Error.Message = fmt.Sprintf("bad response status code %d", resp.StatusCode)
newApiErr = types.NewErrorWithStatusCode(errors.New(errResponse.ToMessage()), types.ErrorCodeBadResponseStatusCode, resp.StatusCode)
}
return
}
func ResetStatusCode(openaiErr *dto.OpenAIErrorWithStatusCode, statusCodeMappingStr string) {
func ResetStatusCode(newApiErr *types.NewAPIError, statusCodeMappingStr string) {
if statusCodeMappingStr == "" || statusCodeMappingStr == "{}" {
return
}
@@ -122,13 +118,13 @@ func ResetStatusCode(openaiErr *dto.OpenAIErrorWithStatusCode, statusCodeMapping
if err != nil {
return
}
if openaiErr.StatusCode == http.StatusOK {
if newApiErr.StatusCode == http.StatusOK {
return
}
codeStr := strconv.Itoa(openaiErr.StatusCode)
codeStr := strconv.Itoa(newApiErr.StatusCode)
if _, ok := statusCodeMapping[codeStr]; ok {
intCode, _ := strconv.Atoi(statusCodeMapping[codeStr])
openaiErr.StatusCode = intCode
newApiErr.StatusCode = intCode
}
}