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:
2025-12-06 10:31:36 +01:00
co-authored by Claude
parent 769c12b33b
commit 0ed6c5086c
5 changed files with 181 additions and 0 deletions
View File
View File
+86
View File
@@ -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()
+52
View File
@@ -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)
+43
View File
@@ -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}
)