thereisnohr / src /llm /errors.py
jbeiroa's picture
Initial clean deploy of demo app
74711df
"""Application-level error taxonomy for the LLM layer.
This module normalizes provider exceptions into a small stable error contract
that service-layer code can rely on without depending on provider-specific
classes.
"""
from functools import lru_cache
class LLMError(Exception):
"""Base exception for all LLM-layer failures."""
def __init__(self, message: str | None = None):
self.message = message or self.__doc__
super().__init__(self.message.strip())
class LLMConfigError(LLMError):
"""Model configuration is invalid or missing."""
class LLMProviderError(LLMError):
"""Raised for provider-side failures that are not more specific."""
class LLMTimeoutError(LLMProviderError):
"""Raised when an LLM request times out."""
class LLMRateLimitError(LLMProviderError):
"""Raised when the provider rejects requests due to rate limits."""
class LLMStructuredOutputError(LLMError):
"""The model's response could not be parsed to JSON."""
class LLMSchemaValidationError(LLMError):
"""Parsed JSON schema failed validation."""
class LLMRetryExhaustedError(LLMError):
"""All retries and fallbacks were exhausted."""
@lru_cache(maxsize=1)
def _litellm_exception_types() -> tuple[type[Exception], ...]:
"""Returns LiteLLM exception base types exposed by the SDK."""
try:
import litellm
except ImportError:
return ()
known = getattr(litellm, "LITELLM_EXCEPTION_TYPES", ())
return tuple(exc for exc in known if isinstance(exc, type) and issubclass(exc, Exception))
def coerce_provider_exception(error: BaseException) -> BaseException:
"""Normalizes provider exceptions into app-level error types.
Args:
error: Exception instance raised by provider/runtime code.
Returns:
BaseException: Mapped application exception when recognized, otherwise
the original exception.
"""
if not isinstance(error, Exception):
return error
try:
import litellm
except ImportError:
return error
timeout_type = getattr(litellm, "Timeout", None)
rate_limit_type = getattr(litellm, "RateLimitError", None)
if isinstance(timeout_type, type) and isinstance(error, timeout_type):
return LLMTimeoutError(str(error))
if isinstance(rate_limit_type, type) and isinstance(error, rate_limit_type):
return LLMRateLimitError(str(error))
if _litellm_exception_types() and isinstance(error, _litellm_exception_types()):
return LLMProviderError(str(error))
return error
def error_type_for_exception(error: BaseException) -> str:
"""Maps exceptions to normalized metadata error labels.
Args:
error: Exception to classify.
Returns:
str: Error-category label suitable for telemetry metadata.
"""
normalized = coerce_provider_exception(error)
if isinstance(normalized, LLMTimeoutError):
return "timeout"
if isinstance(normalized, LLMRateLimitError):
return "rate_limit"
if isinstance(normalized, LLMProviderError):
return "provider"
if isinstance(normalized, LLMStructuredOutputError):
return "structured_output"
if isinstance(normalized, LLMSchemaValidationError):
return "schema_validation"
if isinstance(normalized, LLMRetryExhaustedError):
return "retry_exhausted"
return normalized.__class__.__name__.lower()