// 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 }