forked from cardosofelipe/fast-next-template
- Add type annotations for mypy compliance - Use UTC-aware datetimes consistently (datetime.now(UTC)) - Add type: ignore comments for LiteLLM incomplete stubs - Fix import ordering and formatting - Update pyproject.toml mypy configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
477 lines
13 KiB
Python
477 lines
13 KiB
Python
"""
|
|
Custom exceptions for LLM Gateway MCP Server.
|
|
|
|
Provides structured error handling with error codes for consistent responses.
|
|
"""
|
|
|
|
from enum import Enum
|
|
from typing import Any
|
|
|
|
|
|
class ErrorCode(str, Enum):
|
|
"""Error codes for LLM Gateway errors."""
|
|
|
|
# General errors
|
|
UNKNOWN_ERROR = "LLM_UNKNOWN_ERROR"
|
|
INVALID_REQUEST = "LLM_INVALID_REQUEST"
|
|
CONFIGURATION_ERROR = "LLM_CONFIGURATION_ERROR"
|
|
|
|
# Provider errors
|
|
PROVIDER_ERROR = "LLM_PROVIDER_ERROR"
|
|
PROVIDER_TIMEOUT = "LLM_PROVIDER_TIMEOUT"
|
|
PROVIDER_RATE_LIMIT = "LLM_PROVIDER_RATE_LIMIT"
|
|
PROVIDER_UNAVAILABLE = "LLM_PROVIDER_UNAVAILABLE"
|
|
ALL_PROVIDERS_FAILED = "LLM_ALL_PROVIDERS_FAILED"
|
|
|
|
# Model errors
|
|
INVALID_MODEL = "LLM_INVALID_MODEL"
|
|
INVALID_MODEL_GROUP = "LLM_INVALID_MODEL_GROUP"
|
|
MODEL_NOT_AVAILABLE = "LLM_MODEL_NOT_AVAILABLE"
|
|
|
|
# Circuit breaker errors
|
|
CIRCUIT_OPEN = "LLM_CIRCUIT_OPEN"
|
|
CIRCUIT_HALF_OPEN_EXHAUSTED = "LLM_CIRCUIT_HALF_OPEN_EXHAUSTED"
|
|
|
|
# Cost errors
|
|
COST_LIMIT_EXCEEDED = "LLM_COST_LIMIT_EXCEEDED"
|
|
BUDGET_EXHAUSTED = "LLM_BUDGET_EXHAUSTED"
|
|
|
|
# Rate limiting errors
|
|
RATE_LIMIT_EXCEEDED = "LLM_RATE_LIMIT_EXCEEDED"
|
|
|
|
# Streaming errors
|
|
STREAM_ERROR = "LLM_STREAM_ERROR"
|
|
STREAM_INTERRUPTED = "LLM_STREAM_INTERRUPTED"
|
|
|
|
# Token errors
|
|
TOKEN_LIMIT_EXCEEDED = "LLM_TOKEN_LIMIT_EXCEEDED"
|
|
CONTEXT_TOO_LONG = "LLM_CONTEXT_TOO_LONG"
|
|
|
|
|
|
class LLMGatewayError(Exception):
|
|
"""Base exception for LLM Gateway errors."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
code: ErrorCode = ErrorCode.UNKNOWN_ERROR,
|
|
details: dict[str, Any] | None = None,
|
|
cause: Exception | None = None,
|
|
) -> None:
|
|
"""
|
|
Initialize LLM Gateway error.
|
|
|
|
Args:
|
|
message: Human-readable error message
|
|
code: Error code for programmatic handling
|
|
details: Additional error details
|
|
cause: Original exception that caused this error
|
|
"""
|
|
super().__init__(message)
|
|
self.message = message
|
|
self.code = code
|
|
self.details = details or {}
|
|
self.cause = cause
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
"""Convert error to dictionary for JSON response."""
|
|
result: dict[str, Any] = {
|
|
"error": self.code.value,
|
|
"message": self.message,
|
|
}
|
|
if self.details:
|
|
result["details"] = self.details
|
|
return result
|
|
|
|
def __str__(self) -> str:
|
|
"""String representation."""
|
|
return f"[{self.code.value}] {self.message}"
|
|
|
|
def __repr__(self) -> str:
|
|
"""Detailed representation."""
|
|
return (
|
|
f"{self.__class__.__name__}("
|
|
f"message={self.message!r}, "
|
|
f"code={self.code.value!r}, "
|
|
f"details={self.details!r})"
|
|
)
|
|
|
|
|
|
class ProviderError(LLMGatewayError):
|
|
"""Error from an LLM provider."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
provider: str,
|
|
model: str | None = None,
|
|
status_code: int | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
cause: Exception | None = None,
|
|
) -> None:
|
|
"""
|
|
Initialize provider error.
|
|
|
|
Args:
|
|
message: Error message
|
|
provider: Provider that failed
|
|
model: Model that was being used
|
|
status_code: HTTP status code if applicable
|
|
details: Additional details
|
|
cause: Original exception
|
|
"""
|
|
error_details = details or {}
|
|
error_details["provider"] = provider
|
|
if model:
|
|
error_details["model"] = model
|
|
if status_code:
|
|
error_details["status_code"] = status_code
|
|
|
|
super().__init__(
|
|
message=message,
|
|
code=ErrorCode.PROVIDER_ERROR,
|
|
details=error_details,
|
|
cause=cause,
|
|
)
|
|
self.provider = provider
|
|
self.model = model
|
|
self.status_code = status_code
|
|
|
|
|
|
class RateLimitError(LLMGatewayError):
|
|
"""Rate limit exceeded error."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
provider: str | None = None,
|
|
retry_after: int | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
) -> None:
|
|
"""
|
|
Initialize rate limit error.
|
|
|
|
Args:
|
|
message: Error message
|
|
provider: Provider that rate limited (None for internal limit)
|
|
retry_after: Seconds until retry is allowed
|
|
details: Additional details
|
|
"""
|
|
error_details = details or {}
|
|
if provider:
|
|
error_details["provider"] = provider
|
|
if retry_after:
|
|
error_details["retry_after_seconds"] = retry_after
|
|
|
|
code = (
|
|
ErrorCode.PROVIDER_RATE_LIMIT if provider else ErrorCode.RATE_LIMIT_EXCEEDED
|
|
)
|
|
|
|
super().__init__(
|
|
message=message,
|
|
code=code,
|
|
details=error_details,
|
|
)
|
|
self.provider = provider
|
|
self.retry_after = retry_after
|
|
|
|
|
|
class CircuitOpenError(LLMGatewayError):
|
|
"""Circuit breaker is open, provider temporarily unavailable."""
|
|
|
|
def __init__(
|
|
self,
|
|
provider: str,
|
|
recovery_time: int | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
) -> None:
|
|
"""
|
|
Initialize circuit open error.
|
|
|
|
Args:
|
|
provider: Provider with open circuit
|
|
recovery_time: Seconds until circuit may recover
|
|
details: Additional details
|
|
"""
|
|
error_details = details or {}
|
|
error_details["provider"] = provider
|
|
if recovery_time:
|
|
error_details["recovery_time_seconds"] = recovery_time
|
|
|
|
super().__init__(
|
|
message=f"Circuit breaker open for provider {provider}",
|
|
code=ErrorCode.CIRCUIT_OPEN,
|
|
details=error_details,
|
|
)
|
|
self.provider = provider
|
|
self.recovery_time = recovery_time
|
|
|
|
|
|
class CostLimitExceededError(LLMGatewayError):
|
|
"""Cost limit exceeded for project or agent."""
|
|
|
|
def __init__(
|
|
self,
|
|
entity_type: str,
|
|
entity_id: str,
|
|
current_cost: float,
|
|
limit: float,
|
|
details: dict[str, Any] | None = None,
|
|
) -> None:
|
|
"""
|
|
Initialize cost limit error.
|
|
|
|
Args:
|
|
entity_type: 'project' or 'agent'
|
|
entity_id: ID of the entity
|
|
current_cost: Current accumulated cost
|
|
limit: Cost limit that was exceeded
|
|
details: Additional details
|
|
"""
|
|
error_details = details or {}
|
|
error_details["entity_type"] = entity_type
|
|
error_details["entity_id"] = entity_id
|
|
error_details["current_cost_usd"] = current_cost
|
|
error_details["limit_usd"] = limit
|
|
|
|
super().__init__(
|
|
message=(
|
|
f"Cost limit exceeded for {entity_type} {entity_id}: "
|
|
f"${current_cost:.2f} >= ${limit:.2f}"
|
|
),
|
|
code=ErrorCode.COST_LIMIT_EXCEEDED,
|
|
details=error_details,
|
|
)
|
|
self.entity_type = entity_type
|
|
self.entity_id = entity_id
|
|
self.current_cost = current_cost
|
|
self.limit = limit
|
|
|
|
|
|
class InvalidModelGroupError(LLMGatewayError):
|
|
"""Invalid or unknown model group."""
|
|
|
|
def __init__(
|
|
self,
|
|
model_group: str,
|
|
available_groups: list[str] | None = None,
|
|
) -> None:
|
|
"""
|
|
Initialize invalid model group error.
|
|
|
|
Args:
|
|
model_group: The invalid group name
|
|
available_groups: List of valid group names
|
|
"""
|
|
details: dict[str, Any] = {"requested_group": model_group}
|
|
if available_groups:
|
|
details["available_groups"] = available_groups
|
|
|
|
super().__init__(
|
|
message=f"Invalid model group: {model_group}",
|
|
code=ErrorCode.INVALID_MODEL_GROUP,
|
|
details=details,
|
|
)
|
|
self.model_group = model_group
|
|
self.available_groups = available_groups
|
|
|
|
|
|
class InvalidModelError(LLMGatewayError):
|
|
"""Invalid or unknown model."""
|
|
|
|
def __init__(
|
|
self,
|
|
model: str,
|
|
reason: str | None = None,
|
|
) -> None:
|
|
"""
|
|
Initialize invalid model error.
|
|
|
|
Args:
|
|
model: The invalid model name
|
|
reason: Reason why it's invalid
|
|
"""
|
|
details: dict[str, Any] = {"requested_model": model}
|
|
if reason:
|
|
details["reason"] = reason
|
|
|
|
super().__init__(
|
|
message=f"Invalid model: {model}" + (f" ({reason})" if reason else ""),
|
|
code=ErrorCode.INVALID_MODEL,
|
|
details=details,
|
|
)
|
|
self.model = model
|
|
|
|
|
|
class ModelNotAvailableError(LLMGatewayError):
|
|
"""Model not available (provider not configured)."""
|
|
|
|
def __init__(
|
|
self,
|
|
model: str,
|
|
provider: str,
|
|
) -> None:
|
|
"""
|
|
Initialize model not available error.
|
|
|
|
Args:
|
|
model: The unavailable model
|
|
provider: The provider that's not configured
|
|
"""
|
|
super().__init__(
|
|
message=f"Model {model} not available: {provider} provider not configured",
|
|
code=ErrorCode.MODEL_NOT_AVAILABLE,
|
|
details={"model": model, "provider": provider},
|
|
)
|
|
self.model = model
|
|
self.provider = provider
|
|
|
|
|
|
class AllProvidersFailedError(LLMGatewayError):
|
|
"""All providers in the failover chain failed."""
|
|
|
|
def __init__(
|
|
self,
|
|
model_group: str,
|
|
attempted_models: list[str],
|
|
errors: list[dict[str, Any]],
|
|
) -> None:
|
|
"""
|
|
Initialize all providers failed error.
|
|
|
|
Args:
|
|
model_group: The model group that was requested
|
|
attempted_models: Models that were attempted
|
|
errors: Errors from each attempt
|
|
"""
|
|
super().__init__(
|
|
message=f"All providers failed for model group {model_group}",
|
|
code=ErrorCode.ALL_PROVIDERS_FAILED,
|
|
details={
|
|
"model_group": model_group,
|
|
"attempted_models": attempted_models,
|
|
"errors": errors,
|
|
},
|
|
)
|
|
self.model_group = model_group
|
|
self.attempted_models = attempted_models
|
|
self.errors = errors
|
|
|
|
|
|
class StreamError(LLMGatewayError):
|
|
"""Error during streaming response."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
chunks_received: int = 0,
|
|
cause: Exception | None = None,
|
|
) -> None:
|
|
"""
|
|
Initialize stream error.
|
|
|
|
Args:
|
|
message: Error message
|
|
chunks_received: Number of chunks received before error
|
|
cause: Original exception
|
|
"""
|
|
super().__init__(
|
|
message=message,
|
|
code=ErrorCode.STREAM_ERROR,
|
|
details={"chunks_received": chunks_received},
|
|
cause=cause,
|
|
)
|
|
self.chunks_received = chunks_received
|
|
|
|
|
|
class TokenLimitExceededError(LLMGatewayError):
|
|
"""Request exceeds model's token limit."""
|
|
|
|
def __init__(
|
|
self,
|
|
model: str,
|
|
token_count: int,
|
|
limit: int,
|
|
) -> None:
|
|
"""
|
|
Initialize token limit error.
|
|
|
|
Args:
|
|
model: Model name
|
|
token_count: Requested token count
|
|
limit: Model's token limit
|
|
"""
|
|
super().__init__(
|
|
message=f"Token count {token_count} exceeds {model} limit of {limit}",
|
|
code=ErrorCode.TOKEN_LIMIT_EXCEEDED,
|
|
details={
|
|
"model": model,
|
|
"requested_tokens": token_count,
|
|
"limit": limit,
|
|
},
|
|
)
|
|
self.model = model
|
|
self.token_count = token_count
|
|
self.limit = limit
|
|
|
|
|
|
class ContextTooLongError(LLMGatewayError):
|
|
"""Input context exceeds model's context window."""
|
|
|
|
def __init__(
|
|
self,
|
|
model: str,
|
|
context_length: int,
|
|
max_context: int,
|
|
) -> None:
|
|
"""
|
|
Initialize context too long error.
|
|
|
|
Args:
|
|
model: Model name
|
|
context_length: Input context length
|
|
max_context: Model's max context window
|
|
"""
|
|
super().__init__(
|
|
message=(
|
|
f"Context length {context_length} exceeds {model} "
|
|
f"context window of {max_context}"
|
|
),
|
|
code=ErrorCode.CONTEXT_TOO_LONG,
|
|
details={
|
|
"model": model,
|
|
"context_length": context_length,
|
|
"max_context": max_context,
|
|
},
|
|
)
|
|
self.model = model
|
|
self.context_length = context_length
|
|
self.max_context = max_context
|
|
|
|
|
|
class ConfigurationError(LLMGatewayError):
|
|
"""Configuration error."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
config_key: str | None = None,
|
|
) -> None:
|
|
"""
|
|
Initialize configuration error.
|
|
|
|
Args:
|
|
message: Error message
|
|
config_key: Configuration key that's problematic
|
|
"""
|
|
details: dict[str, Any] = {}
|
|
if config_key:
|
|
details["config_key"] = config_key
|
|
|
|
super().__init__(
|
|
message=message,
|
|
code=ErrorCode.CONFIGURATION_ERROR,
|
|
details=details,
|
|
)
|
|
self.config_key = config_key
|