"""Normalized error categories for the inference gateway. Maps provider-specific failures into a protocol-agnostic taxonomy so that retry logic, alerting, and metrics work uniformly across Ollama, OpenAI-compatible, and specialist endpoints. Requirements: 2.1, 2.9 """ from __future__ import annotations from enum import Enum class InferenceErrorCategory(str, Enum): """Normalized error categories for inference failures.""" # Network / transport TIMEOUT = "timeout" CONNECTION_REFUSED = "connection_refused" CONNECTION_ERROR = "connection_error" # Authentication / authorization AUTH_FAILED = "auth_failed" FORBIDDEN = "forbidden" # Rate limiting RATE_LIMITED = "rate_limited" # Server errors SERVER_ERROR = "server_error" SERVICE_UNAVAILABLE = "service_unavailable" # Client errors BAD_REQUEST = "bad_request" MODEL_NOT_FOUND = "model_not_found" INVALID_REQUEST = "invalid_request" # Response problems INVALID_RESPONSE = "invalid_response" EMPTY_RESPONSE = "empty_response" SCHEMA_VIOLATION = "schema_violation" # Capability / policy CAPABILITY_UNAVAILABLE = "capability_unavailable" POLICY_VIOLATION = "policy_violation" # Ollama-specific STALL_DETECTED = "stall_detected" # Unknown UNKNOWN = "unknown" @property def retryable(self) -> bool: """Whether this error category should generally be retried.""" return self in _RETRYABLE_CATEGORIES _RETRYABLE_CATEGORIES = frozenset({ InferenceErrorCategory.TIMEOUT, InferenceErrorCategory.CONNECTION_ERROR, InferenceErrorCategory.CONNECTION_REFUSED, InferenceErrorCategory.SERVER_ERROR, InferenceErrorCategory.SERVICE_UNAVAILABLE, InferenceErrorCategory.RATE_LIMITED, InferenceErrorCategory.STALL_DETECTED, InferenceErrorCategory.EMPTY_RESPONSE, }) class InferenceError(Exception): """Typed inference error with category and optional provider detail.""" def __init__( self, category: InferenceErrorCategory, message: str = "", *, provider_detail: str | None = None, status_code: int | None = None, ) -> None: self.category = category self.provider_detail = provider_detail self.status_code = status_code super().__init__(message or category.value) @property def retryable(self) -> bool: return self.category.retryable