All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m40s
- Chaque modele d'un palier devient une route {canal, modele} : openrouter,
deepseek direct, zhipu (z.ai, nouveau canal), minimax, openai, xai ;
repli automatique sur la route suivante si la cle manque, compatibilite
ascendante avec les anciens reglages a chaines
- Page Fournisseurs refondue en sections ; interrupteurs et chaine de
secours retablis ; tests de connexion avec delai d'attente
- Emails de relance generes par IA (canal au choix, prompt construit depuis
le plan marketing, HTML sane, RTL arabe/persan, jamais d'envoi automatique)
- Codes promo : validation locale avant Stripe, Coupon+PromotionCode avec
double plafond, comptage par session (webhook+sync=1 fois), ecriture
atomique, relier-a-Stripe, suppression, validation publique au checkout
- Page Tarifs : champ code promo (?promo=) avec verification traduite
- Tests : 1357 verts (facturation par palier testee dans le worker,
point de contact providers/available, promos, generation)
313 lines
12 KiB
Python
313 lines
12 KiB
Python
"""
|
|
Provider Configuration - Environment-based settings for translation providers.
|
|
|
|
Loads API keys, URLs, and enable/disable flags from environment variables.
|
|
"""
|
|
|
|
import os
|
|
from typing import Dict, 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")
|
|
)
|
|
|
|
|
|
# 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")
|
|
)
|
|
|
|
|
|
# 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 (public OpenAI-compatible API - https://api.minimax.io)
|
|
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-M3")
|
|
MINIMAX_BASE_URL: str = os.getenv("MINIMAX_BASE_URL", "https://api.minimax.io/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"))
|
|
|
|
# z.ai / Zhipu (modèles GLM — API directe compatible OpenAI)
|
|
# Clé : https://z.ai/manage-apikey/apikey-list — base par défaut https://api.z.ai/api/paas/v4
|
|
ZHIPU_ENABLED: bool = os.getenv("ZHIPU_ENABLED", "false").lower() == "true"
|
|
ZHIPU_API_KEY: str = os.getenv("ZHIPU_API_KEY", "")
|
|
ZHIPU_MODEL: str = os.getenv("ZHIPU_MODEL", "glm-5.3")
|
|
ZHIPU_BASE_URL: str = os.getenv("ZHIPU_BASE_URL", "https://api.z.ai/api/paas/v4")
|
|
ZHIPU_TIMEOUT: int = int(os.getenv("ZHIPU_TIMEOUT", "60"))
|
|
ZHIPU_MAX_RETRIES: int = int(os.getenv("ZHIPU_MAX_RETRIES", "3"))
|
|
ZHIPU_RETRY_DELAY: float = float(os.getenv("ZHIPU_RETRY_DELAY", "1.0"))
|
|
|
|
# Fallback chain configuration
|
|
#
|
|
# IMPORTANT: the registry-based fallback (translate_with_fallback) only
|
|
# ever sees providers that _auto_register_providers() registers, i.e.
|
|
# google, openai, deepseek and minimax. The OpenAI-compatible
|
|
# shims (openrouter, openrouter_premium, zai) and google_cloud are wired
|
|
# directly in routes/translate_routes.py and are intentionally NOT part of
|
|
# the registry fallback chain — listing them here would make
|
|
# translate_with_fallback silently skip them with a "provider not
|
|
# registered" log on every call. Override via env if you know what you
|
|
# are doing.
|
|
FALLBACK_CHAIN: List[str] = [
|
|
name.strip()
|
|
for name in os.getenv(
|
|
"PROVIDER_FALLBACK_CHAIN", "google,openai,deepseek,minimax"
|
|
).split(",")
|
|
if name.strip()
|
|
]
|
|
|
|
# Mode-specific fallback chains
|
|
# Classic mode: Google Translate -> Google Cloud
|
|
FALLBACK_CHAIN_CLASSIC: List[str] = [
|
|
name.strip()
|
|
for name in os.getenv("FALLBACK_CHAIN_CLASSIC", "google").split(",")
|
|
if name.strip()
|
|
]
|
|
|
|
# LLM mode: cloud providers in order of cost/quality (registry-registered only)
|
|
FALLBACK_CHAIN_LLM: List[str] = [
|
|
name.strip()
|
|
for name in os.getenv("FALLBACK_CHAIN_LLM", "openai,deepseek,minimax").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", "openai")
|
|
|
|
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,
|
|
),
|
|
"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,
|
|
),
|
|
"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,
|
|
),
|
|
"zhipu": ProviderSettings(
|
|
enabled=cls.ZHIPU_ENABLED,
|
|
api_key=cls.ZHIPU_API_KEY if cls.ZHIPU_API_KEY else None,
|
|
base_url=cls.ZHIPU_BASE_URL,
|
|
model=cls.ZHIPU_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 = {"openai", "openrouter", "google_cloud", "deepseek", "minimax", "zhipu"}
|
|
|
|
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()
|
|
|
|
|
|
# Canaux LLM (tous compatibles OpenAI) utilisables dans les routes de palier
|
|
# « ai_tiers » et par la génération d'emails marketing. Pour chaque canal :
|
|
# base_url par défaut, variable d'environnement de la clé API, variable du
|
|
# modèle et modèle par défaut. Les réglages admin (provider_settings.json)
|
|
# priment sur les variables d'environnement.
|
|
LLM_CHANNELS: Dict[str, Dict[str, str]] = {
|
|
"openrouter": {
|
|
"base_url": "https://openrouter.ai/api/v1",
|
|
"key_env": "OPENROUTER_API_KEY",
|
|
"model_env": "OPENROUTER_MODEL",
|
|
"default_model": "deepseek/deepseek-v4-flash",
|
|
},
|
|
"openrouter_premium": {
|
|
"base_url": "https://openrouter.ai/api/v1",
|
|
"key_env": "OPENROUTER_API_KEY",
|
|
"model_env": "OPENROUTER_PREMIUM_MODEL",
|
|
"default_model": "anthropic/claude-sonnet-5",
|
|
},
|
|
"deepseek": {
|
|
"base_url": "https://api.deepseek.com/v1",
|
|
"key_env": "DEEPSEEK_API_KEY",
|
|
"model_env": "DEEPSEEK_MODEL",
|
|
"url_env": "DEEPSEEK_BASE_URL",
|
|
"default_model": "deepseek-chat",
|
|
},
|
|
"zhipu": {
|
|
"base_url": "https://api.z.ai/api/paas/v4",
|
|
"key_env": "ZHIPU_API_KEY",
|
|
"model_env": "ZHIPU_MODEL",
|
|
"url_env": "ZHIPU_BASE_URL",
|
|
"default_model": "glm-5.3",
|
|
},
|
|
"minimax": {
|
|
"base_url": "https://api.minimax.io/v1",
|
|
"key_env": "MINIMAX_API_KEY",
|
|
"model_env": "MINIMAX_MODEL",
|
|
"url_env": "MINIMAX_BASE_URL",
|
|
"default_model": "MiniMax-M3",
|
|
},
|
|
"openai": {
|
|
"base_url": "https://api.openai.com/v1",
|
|
"key_env": "OPENAI_API_KEY",
|
|
"model_env": "OPENAI_MODEL",
|
|
"url_env": "OPENAI_BASE_URL",
|
|
"default_model": "gpt-4o-mini",
|
|
},
|
|
"zai": {
|
|
"base_url": "https://api.x.ai/v1",
|
|
"key_env": "ZAI_API_KEY",
|
|
"model_env": "ZAI_MODEL",
|
|
"url_env": "ZAI_BASE_URL",
|
|
"default_model": "grok-2-1212",
|
|
},
|
|
}
|