Add core configuration and base models
Implement global application configuration and custom Pydantic models following FastAPI best practices. Core Configuration (src/core/config.py): - BaseSettings with environment variable support - Split configuration by domain (following best practices) - Ollama connection settings (host, model, timeouts) - API configuration (host, port, prefix) - CORS settings - 20-second streaming timeout per turn - Cached configuration with @lru_cache Custom Base Models (src/core/models.py): - CustomBaseModel for consistent serialization - ISO datetime formatting - Alias population support - Enum value serialization - Validation on assignment - serializable_dict() for logging/debugging Exception Handling (src/core/exceptions.py): - Base AppException with status codes - OllamaConnectionError (503) - OllamaTimeoutError (504) - ModelNotFoundError (404) - ValidationError (422) - OpenAI-compatible error structure Benefits: - Consistent configuration across domains - Type-safe settings with validation - Easy environment override via .env - Predictable error responses - Better debugging with serializable models 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
"""
|
||||||
|
Global application configuration.
|
||||||
|
Following best practice of splitting config across domains.
|
||||||
|
"""
|
||||||
|
from enum import Enum
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from pydantic import Field, HttpUrl
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Environment(str, Enum):
|
||||||
|
"""Application environment."""
|
||||||
|
DEVELOPMENT = "development"
|
||||||
|
PRODUCTION = "production"
|
||||||
|
TESTING = "testing"
|
||||||
|
|
||||||
|
|
||||||
|
class Config(BaseSettings):
|
||||||
|
"""
|
||||||
|
Global application configuration.
|
||||||
|
|
||||||
|
Loads from environment variables and .env file.
|
||||||
|
Domain-specific configs should be in their respective modules.
|
||||||
|
"""
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
case_sensitive=True,
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Application
|
||||||
|
APP_NAME: str = "OpenAI-Compatible API"
|
||||||
|
APP_VERSION: str = "0.1.0"
|
||||||
|
ENVIRONMENT: Environment = Environment.DEVELOPMENT
|
||||||
|
DEBUG: bool = Field(default=False, description="Debug mode")
|
||||||
|
|
||||||
|
# API Configuration
|
||||||
|
API_HOST: str = Field(default="0.0.0.0", description="API host")
|
||||||
|
API_PORT: int = Field(default=8000, description="API port")
|
||||||
|
API_PREFIX: str = Field(default="/v1", description="API route prefix")
|
||||||
|
|
||||||
|
# Ollama Configuration
|
||||||
|
OLLAMA_HOST: HttpUrl = Field(
|
||||||
|
default="http://localhost:11434",
|
||||||
|
description="Ollama server URL"
|
||||||
|
)
|
||||||
|
OLLAMA_DEFAULT_MODEL: str = Field(
|
||||||
|
default="mistral-nemo:latest",
|
||||||
|
description="Default Ollama model"
|
||||||
|
)
|
||||||
|
OLLAMA_TIMEOUT: int = Field(
|
||||||
|
default=120,
|
||||||
|
description="Ollama request timeout in seconds"
|
||||||
|
)
|
||||||
|
STREAM_TIMEOUT: int = Field(
|
||||||
|
default=20,
|
||||||
|
description="Timeout for each streaming turn in seconds"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL: str = Field(default="INFO", description="Logging level")
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ORIGINS: list[str] = Field(
|
||||||
|
default=["*"],
|
||||||
|
description="Allowed CORS origins"
|
||||||
|
)
|
||||||
|
CORS_ALLOW_CREDENTIALS: bool = True
|
||||||
|
CORS_ALLOW_METHODS: list[str] = ["*"]
|
||||||
|
CORS_ALLOW_HEADERS: list[str] = ["*"]
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def get_config() -> Config:
|
||||||
|
"""
|
||||||
|
Get cached configuration instance.
|
||||||
|
|
||||||
|
Uses lru_cache to ensure config is loaded once and reused.
|
||||||
|
"""
|
||||||
|
return Config()
|
||||||
|
|
||||||
|
|
||||||
|
# Global config instance
|
||||||
|
config = get_config()
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""
|
||||||
|
Global exception definitions.
|
||||||
|
Domain-specific exceptions should be in their respective modules.
|
||||||
|
"""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class AppException(Exception):
|
||||||
|
"""Base exception for all application errors."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = "An error occurred",
|
||||||
|
status_code: int = 500,
|
||||||
|
details: dict[str, Any] | None = None,
|
||||||
|
):
|
||||||
|
self.message = message
|
||||||
|
self.status_code = status_code
|
||||||
|
self.details = details or {}
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
|
||||||
|
class OllamaConnectionError(AppException):
|
||||||
|
"""Raised when cannot connect to Ollama service."""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "Cannot connect to Ollama service"):
|
||||||
|
super().__init__(message=message, status_code=503)
|
||||||
|
|
||||||
|
|
||||||
|
class OllamaTimeoutError(AppException):
|
||||||
|
"""Raised when Ollama request times out."""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "Ollama request timed out"):
|
||||||
|
super().__init__(message=message, status_code=504)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelNotFoundError(AppException):
|
||||||
|
"""Raised when requested model is not available."""
|
||||||
|
|
||||||
|
def __init__(self, model_name: str):
|
||||||
|
super().__init__(
|
||||||
|
message=f"Model '{model_name}' not found",
|
||||||
|
status_code=404,
|
||||||
|
details={"model": model_name}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationError(AppException):
|
||||||
|
"""Raised for validation errors."""
|
||||||
|
|
||||||
|
def __init__(self, message: str, details: dict[str, Any] | None = None):
|
||||||
|
super().__init__(message=message, status_code=422, details=details)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"""
|
||||||
|
Custom Pydantic base models for consistent serialization.
|
||||||
|
Following best practice of having a global base model.
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
def datetime_to_iso_str(dt: datetime) -> str:
|
||||||
|
"""Convert datetime to ISO format string."""
|
||||||
|
return dt.isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
class CustomBaseModel(BaseModel):
|
||||||
|
"""
|
||||||
|
Custom base model with consistent configuration.
|
||||||
|
|
||||||
|
All domain models should inherit from this for:
|
||||||
|
- Consistent JSON serialization
|
||||||
|
- Timezone-aware datetime handling
|
||||||
|
- Alias population support
|
||||||
|
"""
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_encoders={datetime: datetime_to_iso_str},
|
||||||
|
populate_by_name=True,
|
||||||
|
use_enum_values=True,
|
||||||
|
validate_assignment=True,
|
||||||
|
arbitrary_types_allowed=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def serializable_dict(self, **kwargs: Any) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Return dict with only JSON-serializable fields.
|
||||||
|
|
||||||
|
Useful for logging and debugging.
|
||||||
|
"""
|
||||||
|
return jsonable_encoder(
|
||||||
|
self.model_dump(**kwargs),
|
||||||
|
custom_encoder={datetime: datetime_to_iso_str}
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user