| """ |
| Provider-agnostic error handling for LLM embedding services. |
| |
| Supports OpenAI, Google AI, Anthropic, Ollama, and future providers |
| with unified error handling and sanitization patterns. |
| """ |
|
|
| import re |
| from abc import ABC, abstractmethod |
|
|
|
|
| class ProviderErrorAdapter(ABC): |
| """Abstract base class for provider-specific error handling.""" |
|
|
| @abstractmethod |
| def get_provider_name(self) -> str: |
| pass |
|
|
| @abstractmethod |
| def sanitize_error_message(self, message: str) -> str: |
| pass |
|
|
|
|
| class OpenAIErrorAdapter(ProviderErrorAdapter): |
| def get_provider_name(self) -> str: |
| return "openai" |
|
|
| def sanitize_error_message(self, message: str) -> str: |
| if not isinstance(message, str) or not message.strip() or len(message) > 2000: |
| return "OpenAI API encountered an error. Please verify your API key and quota." |
|
|
| sanitized = message |
|
|
| |
| patterns = [ |
| (r"sk-[a-zA-Z0-9]{48}", "[REDACTED_KEY]"), |
| (r"https?://[^\s]*openai\.com[^\s]*", "[REDACTED_URL]"), |
| (r"org-[a-zA-Z0-9]{20,}", "[REDACTED_ORG]"), |
| (r"proj_[a-zA-Z0-9]{10,}", "[REDACTED_PROJECT]"), |
| (r"req_[a-zA-Z0-9]{10,}", "[REDACTED_REQUEST]"), |
| (r"Bearer\s+[a-zA-Z0-9._-]+", "Bearer [REDACTED_TOKEN]"), |
| ] |
|
|
| for pattern, replacement in patterns: |
| sanitized = re.sub(pattern, replacement, sanitized, flags=re.IGNORECASE) |
|
|
| |
| sensitive_words = ["internal", "server", "endpoint"] |
| if any(word in sanitized.lower() for word in sensitive_words): |
| return "OpenAI API encountered an error. Please verify your API key and quota." |
|
|
| return sanitized |
|
|
|
|
| class GoogleAIErrorAdapter(ProviderErrorAdapter): |
| def get_provider_name(self) -> str: |
| return "google" |
|
|
| def sanitize_error_message(self, message: str) -> str: |
| if not isinstance(message, str) or not message.strip() or len(message) > 2000: |
| return "Google AI API encountered an error. Please verify your API key." |
|
|
| sanitized = message |
|
|
| |
| patterns = [ |
| (r"AIza[a-zA-Z0-9_-]{35}", "[REDACTED_KEY]"), |
| (r"https?://[^\s]*googleapis\.com[^\s]*", "[REDACTED_URL]"), |
| (r"https?://[^\s]*googleusercontent\.com[^\s]*", "[REDACTED_URL]"), |
| (r"projects/[a-zA-Z0-9_-]+", "projects/[REDACTED_PROJECT]"), |
| (r"ya29\.[a-zA-Z0-9_-]+", "[REDACTED_TOKEN]"), |
| (r"Bearer\s+[a-zA-Z0-9._-]+", "Bearer [REDACTED_TOKEN]"), |
| ] |
|
|
| for pattern, replacement in patterns: |
| sanitized = re.sub(pattern, replacement, sanitized, flags=re.IGNORECASE) |
|
|
| |
| sensitive_words = ["internal", "server", "endpoint", "project"] |
| if any(word in sanitized.lower() for word in sensitive_words): |
| return "Google AI API encountered an error. Please verify your API key." |
|
|
| return sanitized |
|
|
|
|
| class AnthropicErrorAdapter(ProviderErrorAdapter): |
| def get_provider_name(self) -> str: |
| return "anthropic" |
|
|
| def sanitize_error_message(self, message: str) -> str: |
| if not isinstance(message, str) or not message.strip() or len(message) > 2000: |
| return "Anthropic API encountered an error. Please verify your API key." |
|
|
| sanitized = message |
|
|
| |
| patterns = [ |
| (r"sk-ant-[a-zA-Z0-9_-]{10,}", "[REDACTED_KEY]"), |
| (r"https?://[^\s]*anthropic\.com[^\s]*", "[REDACTED_URL]"), |
| (r"Bearer\s+[a-zA-Z0-9._-]+", "Bearer [REDACTED_TOKEN]"), |
| ] |
|
|
| for pattern, replacement in patterns: |
| sanitized = re.sub(pattern, replacement, sanitized, flags=re.IGNORECASE) |
|
|
| |
| sensitive_words = ["internal", "server", "endpoint"] |
| if any(word in sanitized.lower() for word in sensitive_words): |
| return "Anthropic API encountered an error. Please verify your API key." |
|
|
| return sanitized |
|
|
|
|
| class ProviderErrorFactory: |
| """Factory for provider-agnostic error handling.""" |
|
|
| _adapters = { |
| "openai": OpenAIErrorAdapter(), |
| "google": GoogleAIErrorAdapter(), |
| "anthropic": AnthropicErrorAdapter(), |
| } |
|
|
| @classmethod |
| def get_adapter(cls, provider: str) -> ProviderErrorAdapter: |
| return cls._adapters.get(provider.lower(), cls._adapters["openai"]) |
|
|
| @classmethod |
| def sanitize_provider_error(cls, message: str, provider: str) -> str: |
| adapter = cls.get_adapter(provider) |
| return adapter.sanitize_error_message(message) |
|
|
| @classmethod |
| def detect_provider_from_error(cls, error_str: str) -> str: |
| """Detect provider from error message with comprehensive pattern matching.""" |
| if not error_str: |
| return "openai" |
|
|
| error_lower = error_str.lower() |
|
|
| |
| if ( |
| "anthropic" in error_lower |
| or re.search(r"sk-ant-[a-zA-Z0-9_-]+", error_str, re.IGNORECASE) |
| or "claude" in error_lower |
| ): |
| return "anthropic" |
| elif ( |
| "google" in error_lower |
| or re.search(r"AIza[a-zA-Z0-9_-]+", error_str, re.IGNORECASE) |
| or "googleapis" in error_lower |
| or "vertex" in error_lower |
| ): |
| return "google" |
| elif ( |
| "openai" in error_lower |
| or re.search(r"sk-[a-zA-Z0-9]{48}", error_str, re.IGNORECASE) |
| or "gpt" in error_lower |
| ): |
| return "openai" |
| else: |
| return "openai" |
|
|