""" Provider Configuration - Environment-based settings for translation providers. Loads API keys, URLs, and enable/disable flags from environment variables. """ import os from typing import List, Optional from pydantic import BaseModel def _ensure_dotenv_loaded() -> None: """Load .env file if not already loaded.""" from dotenv import load_dotenv load_dotenv() _ensure_dotenv_loaded() class ProviderSettings(BaseModel): """Settings for a single translation provider.""" enabled: bool = False api_key: Optional[str] = None base_url: Optional[str] = None model: Optional[str] = None class ProvidersConfig: """ Configuration for all translation providers. Loads settings from environment variables with sensible defaults. """ # Google Translate (no API key required via deep_translator — accès web non officiel) GOOGLE_ENABLED: bool = ( os.getenv("GOOGLE_TRANSLATE_ENABLED", "true").lower() == "true" ) GOOGLE_TRANSLATE_TIMEOUT: int = int(os.getenv("GOOGLE_TRANSLATE_TIMEOUT", "30")) GOOGLE_TRANSLATE_MAX_RETRIES: int = int( os.getenv("GOOGLE_TRANSLATE_MAX_RETRIES", "3") ) GOOGLE_TRANSLATE_RETRY_DELAY: float = float( os.getenv("GOOGLE_TRANSLATE_RETRY_DELAY", "1.0") ) # Google Cloud Translation API v2 (clé API officielle, facturable) # Obtenir la clé : https://console.cloud.google.com → APIs & Services → Credentials # Activer : Cloud Translation API (Basic v2) + Facturation sur le projet GOOGLE_CLOUD_ENABLED: bool = ( os.getenv("GOOGLE_CLOUD_ENABLED", "false").lower() == "true" ) GOOGLE_CLOUD_API_KEY: str = os.getenv("GOOGLE_CLOUD_API_KEY", "") GOOGLE_CLOUD_TIMEOUT: int = int(os.getenv("GOOGLE_CLOUD_TIMEOUT", "30")) GOOGLE_CLOUD_MAX_RETRIES: int = int(os.getenv("GOOGLE_CLOUD_MAX_RETRIES", "3")) GOOGLE_CLOUD_RETRY_DELAY: float = float( os.getenv("GOOGLE_CLOUD_RETRY_DELAY", "1.0") ) # DeepL DEEPL_ENABLED: bool = os.getenv("DEEPL_ENABLED", "false").lower() == "true" DEEPL_API_KEY: str = os.getenv("DEEPL_API_KEY", "") DEEPL_TIMEOUT: int = int(os.getenv("DEEPL_TIMEOUT", "30")) DEEPL_MAX_RETRIES: int = int(os.getenv("DEEPL_MAX_RETRIES", "3")) DEEPL_RETRY_DELAY: float = float(os.getenv("DEEPL_RETRY_DELAY", "1.0")) # OpenAI OPENAI_ENABLED: bool = os.getenv("OPENAI_ENABLED", "false").lower() == "true" OPENAI_API_KEY: str = os.getenv("OPENAI_API_KEY", "") OPENAI_MODEL: str = os.getenv("OPENAI_MODEL", "gpt-4o-mini") OPENAI_TIMEOUT: int = int(os.getenv("OPENAI_TIMEOUT", "60")) OPENAI_MAX_RETRIES: int = int(os.getenv("OPENAI_MAX_RETRIES", "3")) OPENAI_RETRY_DELAY: float = float(os.getenv("OPENAI_RETRY_DELAY", "1.0")) OPENAI_BASE_URL: str = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") OPENAI_HEALTH_CHECK_TIMEOUT: int = int( os.getenv("OPENAI_HEALTH_CHECK_TIMEOUT", "5") ) # Ollama (local LLM) - default model is config-only, no hardcode in provider _DEFAULT_OLLAMA_MODEL: str = "llama3" OLLAMA_ENABLED: bool = os.getenv("OLLAMA_ENABLED", "false").lower() == "true" OLLAMA_BASE_URL: str = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") OLLAMA_MODEL: str = os.getenv("OLLAMA_MODEL", _DEFAULT_OLLAMA_MODEL) OLLAMA_VISION_MODEL: str = os.getenv("OLLAMA_VISION_MODEL", "llava") OLLAMA_TIMEOUT: int = int(os.getenv("OLLAMA_TIMEOUT", "120")) OLLAMA_MAX_RETRIES: int = int(os.getenv("OLLAMA_MAX_RETRIES", "2")) OLLAMA_RETRY_DELAY: float = float(os.getenv("OLLAMA_RETRY_DELAY", "2.0")) # OpenRouter (multi-model API) OPENROUTER_ENABLED: bool = ( os.getenv("OPENROUTER_ENABLED", "false").lower() == "true" ) OPENROUTER_API_KEY: str = os.getenv("OPENROUTER_API_KEY", "") OPENROUTER_MODEL: str = os.getenv("OPENROUTER_MODEL", "deepseek/deepseek-chat") # DeepSeek (direct API) DEEPSEEK_ENABLED: bool = os.getenv("DEEPSEEK_ENABLED", "false").lower() == "true" DEEPSEEK_API_KEY: str = os.getenv("DEEPSEEK_API_KEY", "") DEEPSEEK_MODEL: str = os.getenv("DEEPSEEK_MODEL", "deepseek-chat") DEEPSEEK_BASE_URL: str = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1") DEEPSEEK_TIMEOUT: int = int(os.getenv("DEEPSEEK_TIMEOUT", "60")) DEEPSEEK_MAX_RETRIES: int = int(os.getenv("DEEPSEEK_MAX_RETRIES", "3")) DEEPSEEK_RETRY_DELAY: float = float(os.getenv("DEEPSEEK_RETRY_DELAY", "1.0")) # Minimax (direct API - m2.7, MiniMax-M1) MINIMAX_ENABLED: bool = os.getenv("MINIMAX_ENABLED", "false").lower() == "true" MINIMAX_API_KEY: str = os.getenv("MINIMAX_API_KEY", "") MINIMAX_MODEL: str = os.getenv("MINIMAX_MODEL", "MiniMax-M1") MINIMAX_BASE_URL: str = os.getenv("MINIMAX_BASE_URL", "https://api.minimax.chat/v1") MINIMAX_GROUP_ID: str = os.getenv("MINIMAX_GROUP_ID", "") MINIMAX_TIMEOUT: int = int(os.getenv("MINIMAX_TIMEOUT", "60")) MINIMAX_MAX_RETRIES: int = int(os.getenv("MINIMAX_MAX_RETRIES", "3")) MINIMAX_RETRY_DELAY: float = float(os.getenv("MINIMAX_RETRY_DELAY", "1.0")) # Fallback chain configuration # General fallback chain (backward compatibility) FALLBACK_CHAIN: List[str] = [ name.strip() for name in os.getenv( "PROVIDER_FALLBACK_CHAIN", "google,google_cloud,deepl,openrouter,openrouter_premium,openai,deepseek,zai" ).split(",") if name.strip() ] # Mode-specific fallback chains # Classic mode: Google Translate -> Google Cloud -> DeepL FALLBACK_CHAIN_CLASSIC: List[str] = [ name.strip() for name in os.getenv("FALLBACK_CHAIN_CLASSIC", "google,google_cloud,deepl").split(",") if name.strip() ] # LLM mode: cloud providers in order of cost/quality (no Ollama by default) FALLBACK_CHAIN_LLM: List[str] = [ name.strip() for name in os.getenv("FALLBACK_CHAIN_LLM", "openrouter,openrouter_premium,openai,deepseek,zai").split(",") if name.strip() ] @classmethod def get_fallback_chain(cls, mode: str = "auto") -> List[str]: """ Get the fallback chain for a specific mode. Args: mode: "classic" for Classic providers, "llm" for LLM providers, "auto" or any other value for general fallback chain Returns: List of provider names in fallback order """ mode = mode.lower() if mode == "classic": return cls.FALLBACK_CHAIN_CLASSIC elif mode == "llm": return cls.FALLBACK_CHAIN_LLM else: return cls.FALLBACK_CHAIN @classmethod def get_provider_settings(cls, provider_name: str) -> ProviderSettings: """ Get settings for a specific provider. Args: provider_name: Name of the provider (e.g., "google", "deepl") Returns: ProviderSettings for the requested provider """ settings_map = { "google": ProviderSettings( enabled=cls.GOOGLE_ENABLED, api_key=None, base_url=None, model=None ), "google_cloud": ProviderSettings( enabled=cls.GOOGLE_CLOUD_ENABLED, api_key=cls.GOOGLE_CLOUD_API_KEY if cls.GOOGLE_CLOUD_API_KEY else None, base_url=None, model=None, ), "deepl": ProviderSettings( enabled=cls.DEEPL_ENABLED, api_key=cls.DEEPL_API_KEY if cls.DEEPL_API_KEY else None, base_url=None, model=None, ), "openai": ProviderSettings( enabled=cls.OPENAI_ENABLED, api_key=cls.OPENAI_API_KEY if cls.OPENAI_API_KEY else None, base_url=cls.OPENAI_BASE_URL or None, model=cls.OPENAI_MODEL, ), "ollama": ProviderSettings( enabled=cls.OLLAMA_ENABLED, api_key=None, base_url=cls.OLLAMA_BASE_URL, model=cls.OLLAMA_MODEL, ), "openrouter": ProviderSettings( enabled=cls.OPENROUTER_ENABLED, api_key=cls.OPENROUTER_API_KEY if cls.OPENROUTER_API_KEY else None, base_url="https://openrouter.ai/api/v1", model=cls.OPENROUTER_MODEL, ), "deepseek": ProviderSettings( enabled=cls.DEEPSEEK_ENABLED, api_key=cls.DEEPSEEK_API_KEY if cls.DEEPSEEK_API_KEY else None, base_url=cls.DEEPSEEK_BASE_URL, model=cls.DEEPSEEK_MODEL, ), "minimax": ProviderSettings( enabled=cls.MINIMAX_ENABLED, api_key=cls.MINIMAX_API_KEY if cls.MINIMAX_API_KEY else None, base_url=cls.MINIMAX_BASE_URL, model=cls.MINIMAX_MODEL, ), } return settings_map.get(provider_name.lower(), ProviderSettings()) @classmethod def is_provider_configured(cls, provider_name: str) -> bool: """ Check if a provider is properly configured. Args: provider_name: Name of the provider Returns: True if the provider is enabled and has required configuration """ settings = cls.get_provider_settings(provider_name) if not settings.enabled: return False # Providers requiring API keys providers_requiring_key = {"deepl", "openai", "openrouter", "google_cloud", "deepseek", "minimax"} if provider_name.lower() in providers_requiring_key: return bool(settings.api_key) return True @classmethod def get_available_providers(cls) -> List[str]: """ Get list of configured and available providers. Returns: List of provider names that are ready to use """ return [name for name in cls.FALLBACK_CHAIN if cls.is_provider_configured(name)] providers_config = ProvidersConfig()