This commit is contained in:
XOF
2025-11-20 12:24:05 +08:00
commit f28bdc751f
164 changed files with 64248 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
// Filename: internal/error/api_error.go
package errors
import (
"errors"
"net/http"
"strings"
"github.com/go-sql-driver/mysql"
"github.com/jackc/pgx/v5/pgconn"
"gorm.io/gorm"
)
// APIError defines a standard error structure for API responses.
type APIError struct {
HTTPStatus int
Code string
Message string
}
// Error implements the error interface.
func (e *APIError) Error() string {
return e.Message
}
// Predefined API errors
var (
ErrBadRequest = &APIError{HTTPStatus: http.StatusBadRequest, Code: "BAD_REQUEST", Message: "Invalid request parameters"}
ErrInvalidJSON = &APIError{HTTPStatus: http.StatusBadRequest, Code: "INVALID_JSON", Message: "Invalid JSON format"}
ErrValidation = &APIError{HTTPStatus: http.StatusBadRequest, Code: "VALIDATION_FAILED", Message: "Input validation failed"}
ErrDuplicateResource = &APIError{HTTPStatus: http.StatusConflict, Code: "DUPLICATE_RESOURCE", Message: "Resource already exists"}
ErrResourceNotFound = &APIError{HTTPStatus: http.StatusNotFound, Code: "NOT_FOUND", Message: "Resource not found"}
ErrInternalServer = &APIError{HTTPStatus: http.StatusInternalServerError, Code: "INTERNAL_SERVER_ERROR", Message: "An unexpected error occurred"}
ErrDatabase = &APIError{HTTPStatus: http.StatusInternalServerError, Code: "DATABASE_ERROR", Message: "Database operation failed"}
ErrUnauthorized = &APIError{HTTPStatus: http.StatusUnauthorized, Code: "UNAUTHORIZED", Message: "Authentication failed"}
ErrForbidden = &APIError{HTTPStatus: http.StatusForbidden, Code: "FORBIDDEN", Message: "You do not have permission to access this resource"}
ErrTaskInProgress = &APIError{HTTPStatus: http.StatusConflict, Code: "TASK_IN_PROGRESS", Message: "A task is already in progress"}
ErrBadGateway = &APIError{HTTPStatus: http.StatusBadGateway, Code: "BAD_GATEWAY", Message: "Upstream service error"}
ErrNoActiveKeys = &APIError{HTTPStatus: http.StatusServiceUnavailable, Code: "NO_ACTIVE_KEYS", Message: "No active API keys available for this group"}
ErrMaxRetriesExceeded = &APIError{HTTPStatus: http.StatusBadGateway, Code: "MAX_RETRIES_EXCEEDED", Message: "Request failed after maximum retries"}
ErrNoKeysAvailable = &APIError{HTTPStatus: http.StatusServiceUnavailable, Code: "NO_KEYS_AVAILABLE", Message: "No API keys available to process the request"}
ErrStateConflict = &APIError{HTTPStatus: http.StatusConflict, Code: "STATE_CONFLICT", Message: "The operation cannot be completed due to the current state of the resource."}
ErrGroupNotFound = &APIError{HTTPStatus: http.StatusNotFound, Code: "GROUP_NOT_FOUND", Message: "The specified group was not found."}
ErrPermissionDenied = &APIError{HTTPStatus: http.StatusForbidden, Code: "PERMISSION_DENIED", Message: "Permission denied for this operation."}
ErrConfigurationError = &APIError{HTTPStatus: http.StatusInternalServerError, Code: "CONFIGURATION_ERROR", Message: "A configuration error prevents this request from being processed."}
ErrStateConflictMasterRevoked = &APIError{HTTPStatus: http.StatusConflict, Code: "STATE_CONFLICT_MASTER_REVOKED", Message: "Cannot perform this operation on a revoked key."}
ErrNotFound = &APIError{HTTPStatus: http.StatusNotFound, Code: "NOT_FOUND", Message: "Resource not found"}
ErrNoKeysMatchFilter = &APIError{HTTPStatus: http.StatusBadRequest, Code: "NO_KEYS_MATCH_FILTER", Message: "No keys were found that match the provided filter criteria."}
)
// NewAPIError creates a new APIError with a custom message.
func NewAPIError(base *APIError, message string) *APIError {
return &APIError{
HTTPStatus: base.HTTPStatus,
Code: base.Code,
Message: message,
}
}
// NewAPIErrorWithUpstream creates a new APIError specifically for wrapping raw upstream errors.
func NewAPIErrorWithUpstream(statusCode int, code string, upstreamMessage string) *APIError {
return &APIError{
HTTPStatus: statusCode,
Code: code,
Message: upstreamMessage,
}
}
// ParseDBError intelligently converts a GORM error into a standard APIError.
func ParseDBError(err error) *APIError {
if err == nil {
return nil
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrResourceNotFound
}
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
if pgErr.Code == "23505" { // unique_violation
return ErrDuplicateResource
}
}
var mysqlErr *mysql.MySQLError
if errors.As(err, &mysqlErr) {
if mysqlErr.Number == 1062 { // Duplicate entry
return ErrDuplicateResource
}
}
// Generic check for SQLite
if strings.Contains(strings.ToLower(err.Error()), "unique constraint failed") {
return ErrDuplicateResource
}
return ErrDatabase
}

19
internal/errors/errors.go Normal file
View File

@@ -0,0 +1,19 @@
// Filename: internal/errors/errors.go
package errors
import (
std_errors "errors" // 为标准库errors包指定别名
)
func Is(err, target error) bool {
return std_errors.Is(err, target)
}
func As(err error, target any) bool {
return std_errors.As(err, target)
}
func Unwrap(err error) error {
return std_errors.Unwrap(err)
}

View File

@@ -0,0 +1,111 @@
// Filename: internal/errors/upstream_errors.go
package errors
import (
"strings"
)
// TODO: [Future Evolution] This file establishes the new, granular error classification framework.
// The next step is to refactor the handleKeyUsageEvent method in APIKeyService to utilize these new
// classifiers and implement the corresponding actions:
//
// 1. On IsPermanentUpstreamError:
// - Set mapping status to models.StatusBanned.
// - Set the master APIKey's status to models.MasterStatusRevoked.
// - This is a "one-strike, you're out" policy for definitively invalid keys.
//
// 2. On IsTemporaryUpstreamError:
// - Increment mapping.ConsecutiveErrorCount.
// - Check against the blacklist threshold to potentially set status to models.StatusDisabled.
// - This is for recoverable errors that are the key's fault (e.g., quota limits).
//
// 3. On ALL other upstream errors (that are not Permanent or Temporary):
// - These are treated as "Truly Ignorable" from the key's perspective (e.g., 503 Service Unavailable).
// - Do NOT increment the error count. Only update LastUsedAt.
// - This prevents good keys from being punished for upstream service instability.
// --- 1. Permanent Errors ---
// Errors that indicate the API Key itself is permanently invalid.
// Action: Ban mapping, Revoke Master Key.
var permanentErrorSubstrings = []string{
"invalid api key",
"api key not valid",
"api key suspended",
"API Key not found",
"api key expired",
"permission denied", // Often indicates the key lacks permissions for the target model/service.
"permission_denied", // Catches the 'status' field in Google's JSON error, e.g., "status": "PERMISSION_DENIED".
"service_disabled", // Catches the 'reason' field for disabled APIs, e.g., "reason": "SERVICE_DISABLED".
"api has not been used",
}
// --- 2. Temporary Errors ---
// Errors that are attributable to the key's state but are recoverable over time.
// Action: Increment consecutive error count, potentially disable the key.
var temporaryErrorSubstrings = []string{
"quota",
"limit reached",
"insufficient",
"billing",
"exceeded",
"too many requests",
}
// --- 3. Unretryable Request Errors ---
// Errors indicating a problem with the user's request, not the key. Retrying with a new key is pointless.
// Action: Abort the retry loop immediately in ProxyHandler.
var unretryableRequestErrorSubstrings = []string{
"invalid content",
"invalid argument",
"malformed",
"unsupported",
"invalid model",
}
// --- 4. Ignorable Client/Network Errors ---
// Network-level errors, typically caused by the client disconnecting.
// Action: Ignore for logging and metrics purposes.
var clientNetworkErrorSubstrings = []string{
"context canceled",
"connection reset by peer",
"broken pipe",
"use of closed network connection",
"request canceled",
}
// IsPermanentUpstreamError checks if an upstream error indicates the key is permanently invalid.
func IsPermanentUpstreamError(msg string) bool {
return containsSubstring(msg, permanentErrorSubstrings)
}
// IsTemporaryUpstreamError checks if an upstream error is due to temporary, key-specific limits.
func IsTemporaryUpstreamError(msg string) bool {
return containsSubstring(msg, temporaryErrorSubstrings)
}
// IsUnretryableRequestError checks if an upstream error is due to a malformed user request.
func IsUnretryableRequestError(msg string) bool {
return containsSubstring(msg, unretryableRequestErrorSubstrings)
}
// IsClientNetworkError checks if an error is a common, ignorable client-side network issue.
func IsClientNetworkError(err error) bool {
if err == nil {
return false
}
return containsSubstring(err.Error(), clientNetworkErrorSubstrings)
}
// containsSubstring is a helper function to avoid code repetition.
func containsSubstring(s string, substrings []string) bool {
if s == "" {
return false
}
lowerS := strings.ToLower(s)
for _, sub := range substrings {
if strings.Contains(lowerS, sub) {
return true
}
}
return false
}

View File

@@ -0,0 +1,79 @@
package errors
import (
"encoding/json"
"strings"
)
const (
// maxErrorBodyLength defines the maximum length of an error message to be stored or returned.
maxErrorBodyLength = 2048
)
// standardErrorResponse matches formats like: {"error": {"message": "..."}}
type standardErrorResponse struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
}
// vendorErrorResponse matches formats like: {"error_msg": "..."}
type vendorErrorResponse struct {
ErrorMsg string `json:"error_msg"`
}
// simpleErrorResponse matches formats like: {"error": "..."}
type simpleErrorResponse struct {
Error string `json:"error"`
}
// rootMessageErrorResponse matches formats like: {"message": "..."}
type rootMessageErrorResponse struct {
Message string `json:"message"`
}
// ParseUpstreamError attempts to parse a structured error message from an upstream response body
func ParseUpstreamError(body []byte) string {
// 1. Attempt to parse the standard OpenAI/Gemini format.
var stdErr standardErrorResponse
if err := json.Unmarshal(body, &stdErr); err == nil {
if msg := strings.TrimSpace(stdErr.Error.Message); msg != "" {
return truncateString(msg, maxErrorBodyLength)
}
}
// 2. Attempt to parse vendor-specific format (e.g., Baidu).
var vendorErr vendorErrorResponse
if err := json.Unmarshal(body, &vendorErr); err == nil {
if msg := strings.TrimSpace(vendorErr.ErrorMsg); msg != "" {
return truncateString(msg, maxErrorBodyLength)
}
}
// 3. Attempt to parse simple error format.
var simpleErr simpleErrorResponse
if err := json.Unmarshal(body, &simpleErr); err == nil {
if msg := strings.TrimSpace(simpleErr.Error); msg != "" {
return truncateString(msg, maxErrorBodyLength)
}
}
// 4. Attempt to parse root-level message format.
var rootMsgErr rootMessageErrorResponse
if err := json.Unmarshal(body, &rootMsgErr); err == nil {
if msg := strings.TrimSpace(rootMsgErr.Message); msg != "" {
return truncateString(msg, maxErrorBodyLength)
}
}
// 5. Graceful Degradation: If all parsing fails, return the raw (but safe) body.
return truncateString(string(body), maxErrorBodyLength)
}
// truncateString ensures a string does not exceed a maximum length.
func truncateString(s string, maxLength int) string {
if len(s) > maxLength {
return s[:maxLength]
}
return s
}