feat: revue de code, doc CODE_REVIEW, forfaits 2026, traduction LLM, providers avec modèle
Made-with: Cursor
This commit is contained in:
670
services/providers/openai_provider.py
Normal file
670
services/providers/openai_provider.py
Normal file
@@ -0,0 +1,670 @@
|
||||
"""
|
||||
OpenAI Provider - Cloud LLM translation provider.
|
||||
|
||||
Extends TranslationProvider base class with robust error handling,
|
||||
retry logic, and health monitoring for OpenAI API.
|
||||
|
||||
Features:
|
||||
- Cloud LLM translation via OpenAI Chat Completions API
|
||||
- Custom system prompt support
|
||||
- Specific error codes for all OpenAI API errors
|
||||
- Retry logic with exponential backoff for transient errors
|
||||
- Timeout configuration (faster than local Ollama)
|
||||
- Health check with caching
|
||||
- Structlog-compatible logging (no document content in logs)
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
try:
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
_HAS_STRUCTLOG = True
|
||||
except ImportError:
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_HAS_STRUCTLOG = False
|
||||
|
||||
|
||||
def _log_info(event: str, **kwargs):
|
||||
"""Log info with structlog or standard logging compatibility."""
|
||||
if _HAS_STRUCTLOG:
|
||||
logger.info(event, **kwargs)
|
||||
else:
|
||||
msg = f"{event} " + " ".join(f"{k}={v}" for k, v in kwargs.items())
|
||||
logger.info(msg)
|
||||
|
||||
|
||||
def _log_warning(event: str, **kwargs):
|
||||
"""Log warning with structlog or standard logging compatibility."""
|
||||
if _HAS_STRUCTLOG:
|
||||
logger.warning(event, **kwargs)
|
||||
else:
|
||||
msg = f"{event} " + " ".join(f"{k}={v}" for k, v in kwargs.items())
|
||||
logger.warning(msg)
|
||||
|
||||
|
||||
def _log_error(event: str, **kwargs):
|
||||
"""Log error with structlog or standard logging compatibility."""
|
||||
if _HAS_STRUCTLOG:
|
||||
logger.error(event, **kwargs)
|
||||
else:
|
||||
msg = f"{event} " + " ".join(f"{k}={v}" for k, v in kwargs.items())
|
||||
logger.error(msg)
|
||||
|
||||
|
||||
import requests
|
||||
from requests.exceptions import Timeout, ConnectionError as RequestsConnectionError
|
||||
|
||||
from .base import TranslationProvider
|
||||
from .schemas import (
|
||||
ProviderHealthStatus,
|
||||
TranslationRequest,
|
||||
TranslationResponse,
|
||||
)
|
||||
|
||||
# Error codes
|
||||
OPENAI_RATE_LIMITED = "OPENAI_RATE_LIMITED"
|
||||
OPENAI_INVALID_KEY = "OPENAI_INVALID_KEY"
|
||||
OPENAI_QUOTA_EXCEEDED = "OPENAI_QUOTA_EXCEEDED"
|
||||
OPENAI_TIMEOUT = "OPENAI_TIMEOUT"
|
||||
OPENAI_SERVICE_ERROR = "OPENAI_SERVICE_ERROR"
|
||||
OPENAI_CONTEXT_TOO_LONG = "OPENAI_CONTEXT_TOO_LONG"
|
||||
|
||||
_RETRYABLE_ERRORS = {OPENAI_RATE_LIMITED, OPENAI_TIMEOUT, OPENAI_SERVICE_ERROR}
|
||||
|
||||
|
||||
class OpenAIProviderError(Exception):
|
||||
"""Exception raised for OpenAI API errors."""
|
||||
|
||||
def __init__(
|
||||
self, code: str, message: str, details: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.details = details or {}
|
||||
super().__init__(message)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert error to dictionary format."""
|
||||
result = {
|
||||
"error": self.code,
|
||||
"message": self.message,
|
||||
}
|
||||
if self.details:
|
||||
result["details"] = self.details
|
||||
return result
|
||||
|
||||
|
||||
DEFAULT_TRANSLATION_PROMPT = """You are a professional translator. Translate the following text from {source_lang} to {target_lang}.
|
||||
|
||||
Rules:
|
||||
- Translate ONLY the text, do not add explanations or notes
|
||||
- Preserve the original formatting, line breaks, and structure
|
||||
- Maintain the original tone and style
|
||||
- For technical terms, use the standard translation in the target language
|
||||
- If the text contains proper nouns or brand names, keep them unchanged unless there's a well-known translation"""
|
||||
|
||||
|
||||
def _build_system_prompt(
|
||||
source_lang: str, target_lang: str, custom_prompt: Optional[str] = None
|
||||
) -> str:
|
||||
"""Build system prompt for translation."""
|
||||
if custom_prompt:
|
||||
return custom_prompt
|
||||
return DEFAULT_TRANSLATION_PROMPT.format(
|
||||
source_lang=source_lang, target_lang=target_lang
|
||||
)
|
||||
|
||||
|
||||
def _get_language_name(code: str) -> str:
|
||||
"""Convert language code to full name for better LLM understanding."""
|
||||
language_names = {
|
||||
"en": "English",
|
||||
"fr": "French",
|
||||
"es": "Spanish",
|
||||
"de": "German",
|
||||
"it": "Italian",
|
||||
"pt": "Portuguese",
|
||||
"nl": "Dutch",
|
||||
"ru": "Russian",
|
||||
"zh": "Chinese",
|
||||
"ja": "Japanese",
|
||||
"ko": "Korean",
|
||||
"ar": "Arabic",
|
||||
"hi": "Hindi",
|
||||
"tr": "Turkish",
|
||||
"pl": "Polish",
|
||||
"vi": "Vietnamese",
|
||||
"th": "Thai",
|
||||
"id": "Indonesian",
|
||||
"ms": "Malay",
|
||||
"uk": "Ukrainian",
|
||||
"cs": "Czech",
|
||||
"sv": "Swedish",
|
||||
"da": "Danish",
|
||||
"fi": "Finnish",
|
||||
"no": "Norwegian",
|
||||
"el": "Greek",
|
||||
"he": "Hebrew",
|
||||
"ro": "Romanian",
|
||||
"hu": "Hungarian",
|
||||
"bg": "Bulgarian",
|
||||
"sk": "Slovak",
|
||||
"hr": "Croatian",
|
||||
"sl": "Slovenian",
|
||||
"lt": "Lithuanian",
|
||||
"lv": "Latvian",
|
||||
"et": "Estonian",
|
||||
}
|
||||
base_code = code.split("-")[0].lower()
|
||||
return language_names.get(base_code, code)
|
||||
|
||||
|
||||
class OpenAITranslationProvider(TranslationProvider):
|
||||
"""
|
||||
OpenAI LLM implementation for cloud translation.
|
||||
|
||||
Features:
|
||||
- Uses OpenAI Chat Completions API
|
||||
- Custom system prompt support for translation context
|
||||
- Thread-safe HTTP client
|
||||
- Robust error handling with specific error codes
|
||||
- Retry logic with exponential backoff
|
||||
- Configurable timeout (default 60s for cloud API)
|
||||
- Health check with result caching
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str,
|
||||
model: str = "gpt-4o-mini",
|
||||
timeout: int = 60,
|
||||
max_retries: int = 3,
|
||||
retry_delay: float = 1.0,
|
||||
base_url: str = "https://api.openai.com/v1",
|
||||
health_check_timeout: int = 5,
|
||||
):
|
||||
"""
|
||||
Initialize OpenAI provider.
|
||||
|
||||
Args:
|
||||
api_key: OpenAI API key
|
||||
model: Model name to use (default: gpt-4o-mini)
|
||||
timeout: Request timeout in seconds (default: 60)
|
||||
max_retries: Maximum retry attempts for transient errors (default: 3)
|
||||
retry_delay: Initial retry delay in seconds (default: 1.0)
|
||||
base_url: OpenAI API base URL (default: https://api.openai.com/v1)
|
||||
health_check_timeout: Timeout for health check requests in seconds (default: 5)
|
||||
"""
|
||||
if not api_key or not api_key.strip():
|
||||
raise ValueError("OpenAI API key cannot be empty")
|
||||
|
||||
self._api_key = api_key
|
||||
self._model = model
|
||||
self._base_url = base_url.rstrip("/")
|
||||
self._provider_name = "openai"
|
||||
self._timeout = timeout
|
||||
self._max_retries = max_retries
|
||||
self._retry_delay = retry_delay
|
||||
self._health_check_timeout = health_check_timeout
|
||||
self._health_cache: Dict[str, Any] = {}
|
||||
self._health_cache_ttl = 60
|
||||
self._health_cache_lock = threading.Lock()
|
||||
|
||||
def _make_api_request(self, text: str, system_prompt: str) -> tuple:
|
||||
"""
|
||||
Make API request to OpenAI.
|
||||
|
||||
Returns:
|
||||
Tuple of (translated_content, usage_dict). usage_dict may be empty.
|
||||
|
||||
Raises:
|
||||
OpenAIProviderError: For any API errors with specific codes
|
||||
"""
|
||||
if not text or not text.strip():
|
||||
return text, {}
|
||||
|
||||
# Check text length (rough estimate: 1 token ~= 4 chars)
|
||||
if len(text) > 16000: # ~4000 tokens
|
||||
raise OpenAIProviderError(
|
||||
code=OPENAI_CONTEXT_TOO_LONG,
|
||||
message="Texte trop long pour le modèle (max ~4000 tokens).",
|
||||
details={"text_length": len(text), "max_tokens": 4000},
|
||||
)
|
||||
|
||||
url = f"{self._base_url}/chat/completions"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self._api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload = {
|
||||
"model": self._model,
|
||||
"messages": [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": text},
|
||||
],
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 4096,
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
url,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=self._timeout,
|
||||
)
|
||||
|
||||
# Handle specific HTTP status codes
|
||||
if response.status_code == 401:
|
||||
raise OpenAIProviderError(
|
||||
code=OPENAI_INVALID_KEY,
|
||||
message="Clé API OpenAI invalide. Vérifiez votre configuration.",
|
||||
details={"status_code": 401},
|
||||
)
|
||||
|
||||
if response.status_code == 429:
|
||||
try:
|
||||
error_data = response.json().get("error", {}) or {}
|
||||
except Exception:
|
||||
error_data = {}
|
||||
error_code = error_data.get("code", "")
|
||||
|
||||
# Check for rate limit vs quota exceeded
|
||||
if error_code == "insufficient_quota":
|
||||
raise OpenAIProviderError(
|
||||
code=OPENAI_QUOTA_EXCEEDED,
|
||||
message="Quota OpenAI épuisé. Vérifiez votre facturation.",
|
||||
details={"status_code": 429, "error_code": error_code},
|
||||
)
|
||||
else:
|
||||
# Rate limit
|
||||
retry_after = response.headers.get("retry-after", "20")
|
||||
raise OpenAIProviderError(
|
||||
code=OPENAI_RATE_LIMITED,
|
||||
message=f"Limite de requêtes OpenAI atteinte. Réessayez dans {retry_after}s.",
|
||||
details={
|
||||
"status_code": 429,
|
||||
"retry_after_seconds": int(retry_after)
|
||||
if retry_after.isdigit()
|
||||
else 20,
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code == 400:
|
||||
try:
|
||||
error_data = response.json().get("error", {}) or {}
|
||||
except Exception:
|
||||
error_data = {}
|
||||
error_code = error_data.get("code", "")
|
||||
|
||||
if error_code == "context_length_exceeded":
|
||||
raise OpenAIProviderError(
|
||||
code=OPENAI_CONTEXT_TOO_LONG,
|
||||
message="Texte trop long pour le modèle (max ~4000 tokens).",
|
||||
details={"status_code": 400, "error_code": error_code},
|
||||
)
|
||||
|
||||
if response.status_code >= 500:
|
||||
raise OpenAIProviderError(
|
||||
code=OPENAI_SERVICE_ERROR,
|
||||
message="Service OpenAI temporairement indisponible.",
|
||||
details={"status_code": response.status_code},
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
error_text = response.text[:200] if response.text else "Unknown error"
|
||||
raise OpenAIProviderError(
|
||||
code=OPENAI_SERVICE_ERROR,
|
||||
message=f"Erreur OpenAI: {error_text}",
|
||||
details={"status_code": response.status_code},
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
choices = data.get("choices", [])
|
||||
|
||||
if not choices:
|
||||
raise OpenAIProviderError(
|
||||
code=OPENAI_SERVICE_ERROR,
|
||||
message="Erreur OpenAI: réponse vide",
|
||||
details={"response": str(data)[:200]},
|
||||
)
|
||||
|
||||
content = choices[0].get("message", {}).get("content", "")
|
||||
|
||||
if not content:
|
||||
raise OpenAIProviderError(
|
||||
code=OPENAI_SERVICE_ERROR,
|
||||
message="Erreur OpenAI: réponse vide",
|
||||
details={"response": str(data)[:200]},
|
||||
)
|
||||
|
||||
usage = data.get("usage", {})
|
||||
return content.strip(), usage
|
||||
|
||||
except Timeout:
|
||||
raise OpenAIProviderError(
|
||||
code=OPENAI_TIMEOUT,
|
||||
message="Délai d'attente OpenAI dépassé. Le service est lent.",
|
||||
details={"timeout_seconds": self._timeout},
|
||||
)
|
||||
except RequestsConnectionError:
|
||||
raise OpenAIProviderError(
|
||||
code=OPENAI_SERVICE_ERROR,
|
||||
message="Service OpenAI temporairement indisponible.",
|
||||
details={"error": "Connection failed"},
|
||||
)
|
||||
except OpenAIProviderError:
|
||||
raise
|
||||
except Exception as e:
|
||||
error_str = str(e).lower()
|
||||
if "connection" in error_str or "refused" in error_str:
|
||||
raise OpenAIProviderError(
|
||||
code=OPENAI_SERVICE_ERROR,
|
||||
message="Service OpenAI temporairement indisponible.",
|
||||
details={"original_error": str(e)[:100]},
|
||||
)
|
||||
raise OpenAIProviderError(
|
||||
code=OPENAI_SERVICE_ERROR,
|
||||
message=f"Erreur OpenAI: {str(e)[:100]}",
|
||||
details={"original_error": str(e)[:100]},
|
||||
)
|
||||
|
||||
def get_name(self) -> str:
|
||||
"""Return provider name."""
|
||||
return self._provider_name
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""
|
||||
Check if OpenAI API is available.
|
||||
|
||||
Uses cached result if available and not expired.
|
||||
"""
|
||||
current_time = time.time()
|
||||
|
||||
with self._health_cache_lock:
|
||||
if "is_available" in self._health_cache:
|
||||
cached = self._health_cache["is_available"]
|
||||
if current_time - cached["timestamp"] < self._health_cache_ttl:
|
||||
return cached["value"]
|
||||
|
||||
try:
|
||||
url = f"{self._base_url}/models"
|
||||
headers = {"Authorization": f"Bearer {self._api_key}"}
|
||||
response = requests.get(
|
||||
url, headers=headers, timeout=self._health_check_timeout
|
||||
)
|
||||
available = response.status_code == 200
|
||||
except Exception as e:
|
||||
_log_warning("openai_availability_check_failed", error=str(e)[:100])
|
||||
available = False
|
||||
|
||||
with self._health_cache_lock:
|
||||
self._health_cache["is_available"] = {
|
||||
"value": available,
|
||||
"timestamp": current_time,
|
||||
}
|
||||
|
||||
return available
|
||||
|
||||
def translate_text(self, request: TranslationRequest) -> TranslationResponse:
|
||||
"""
|
||||
Translate a single text string using OpenAI LLM.
|
||||
|
||||
Supports custom system prompt via request.metadata["custom_prompt"].
|
||||
|
||||
Args:
|
||||
request: TranslationRequest with text and language info
|
||||
|
||||
Returns:
|
||||
TranslationResponse with translated text
|
||||
"""
|
||||
text = request.text
|
||||
target_language = request.target_language
|
||||
source_language = request.source_language or "auto"
|
||||
|
||||
if not text or not text.strip():
|
||||
return TranslationResponse(
|
||||
translated_text=text,
|
||||
provider_name=self._provider_name,
|
||||
from_cache=False,
|
||||
)
|
||||
|
||||
source_lang_name = _get_language_name(source_language)
|
||||
target_lang_name = _get_language_name(target_language)
|
||||
|
||||
custom_prompt = None
|
||||
if request.metadata:
|
||||
custom_prompt = request.metadata.get("custom_prompt")
|
||||
|
||||
system_prompt = _build_system_prompt(
|
||||
source_lang_name, target_lang_name, custom_prompt
|
||||
)
|
||||
|
||||
last_error: Optional[OpenAIProviderError] = None
|
||||
retries = 0
|
||||
|
||||
while retries <= self._max_retries:
|
||||
try:
|
||||
start_time = time.time()
|
||||
result, usage = self._make_api_request(text, system_prompt)
|
||||
latency = time.time() - start_time
|
||||
|
||||
log_kw: Dict[str, Any] = {
|
||||
"chars": len(text),
|
||||
"source_lang": source_language,
|
||||
"target_lang": target_language,
|
||||
"model": self._model,
|
||||
"latency_ms": round(latency * 1000, 2),
|
||||
"retries": retries,
|
||||
}
|
||||
if usage and isinstance(usage.get("total_tokens"), (int, float)):
|
||||
log_kw["tokens_used"] = usage.get("total_tokens")
|
||||
_log_info("openai_translation_success", **log_kw)
|
||||
|
||||
return TranslationResponse(
|
||||
translated_text=result,
|
||||
provider_name=self._provider_name,
|
||||
from_cache=False,
|
||||
source_language=source_language,
|
||||
)
|
||||
|
||||
except OpenAIProviderError as e:
|
||||
last_error = e
|
||||
|
||||
if e.code not in _RETRYABLE_ERRORS:
|
||||
break
|
||||
|
||||
retries += 1
|
||||
if retries <= self._max_retries:
|
||||
delay = self._retry_delay * (2 ** (retries - 1))
|
||||
_log_info(
|
||||
"openai_translation_retry",
|
||||
attempt=retries,
|
||||
delay_s=round(delay, 2),
|
||||
error_code=e.code,
|
||||
text_length=len(text),
|
||||
source_lang=source_language,
|
||||
target_lang=target_language,
|
||||
)
|
||||
time.sleep(delay)
|
||||
|
||||
except Exception as e:
|
||||
last_error = OpenAIProviderError(
|
||||
code=OPENAI_SERVICE_ERROR,
|
||||
message=f"Erreur OpenAI: {str(e)[:100]}",
|
||||
details={"original_error": str(e)[:100]},
|
||||
)
|
||||
retries += 1
|
||||
if retries <= self._max_retries:
|
||||
delay = self._retry_delay * (2 ** (retries - 1))
|
||||
time.sleep(delay)
|
||||
|
||||
if last_error:
|
||||
_log_error(
|
||||
"openai_translation_failed",
|
||||
error_code=last_error.code,
|
||||
text_length=len(text),
|
||||
source_lang=source_language,
|
||||
target_lang=target_language,
|
||||
retries=retries,
|
||||
)
|
||||
return TranslationResponse(
|
||||
translated_text=text,
|
||||
provider_name=self._provider_name,
|
||||
from_cache=False,
|
||||
error=last_error.message,
|
||||
error_code=last_error.code,
|
||||
error_details=last_error.details,
|
||||
)
|
||||
|
||||
return TranslationResponse(
|
||||
translated_text=text,
|
||||
provider_name=self._provider_name,
|
||||
from_cache=False,
|
||||
error="Unknown error",
|
||||
error_code=OPENAI_SERVICE_ERROR,
|
||||
)
|
||||
|
||||
def translate_batch(
|
||||
self, requests: List[TranslationRequest]
|
||||
) -> List[TranslationResponse]:
|
||||
"""
|
||||
Translate multiple texts.
|
||||
|
||||
Args:
|
||||
requests: List of TranslationRequest objects
|
||||
|
||||
Returns:
|
||||
List of TranslationResponse objects
|
||||
"""
|
||||
if not requests:
|
||||
return []
|
||||
|
||||
return [self.translate_text(req) for req in requests]
|
||||
|
||||
def health_check(self) -> ProviderHealthStatus:
|
||||
"""
|
||||
Return health status details for the provider.
|
||||
|
||||
Includes cached result for efficiency.
|
||||
|
||||
Returns:
|
||||
ProviderHealthStatus with availability, latency, and model information
|
||||
"""
|
||||
current_time = time.time()
|
||||
|
||||
with self._health_cache_lock:
|
||||
if "health_check" in self._health_cache:
|
||||
cached = self._health_cache["health_check"]
|
||||
if current_time - cached["timestamp"] < self._health_cache_ttl:
|
||||
return cached["value"]
|
||||
|
||||
start_time = time.time()
|
||||
last_check_iso = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
try:
|
||||
url = f"{self._base_url}/models"
|
||||
headers = {"Authorization": f"Bearer {self._api_key}"}
|
||||
response = requests.get(
|
||||
url, headers=headers, timeout=self._health_check_timeout
|
||||
)
|
||||
latency_ms = (time.time() - start_time) * 1000
|
||||
available = response.status_code == 200
|
||||
|
||||
error_msg = None
|
||||
model_available = None
|
||||
if available:
|
||||
try:
|
||||
models_data = response.json().get("data", [])
|
||||
model_ids = [m.get("id", "") for m in models_data]
|
||||
model_available = self._model in model_ids or any(
|
||||
self._model in mid for mid in model_ids
|
||||
)
|
||||
except Exception:
|
||||
model_available = None
|
||||
else:
|
||||
if response.status_code == 401:
|
||||
error_msg = "Invalid API key"
|
||||
else:
|
||||
error_msg = f"OpenAI API returned {response.status_code}"
|
||||
|
||||
status = ProviderHealthStatus(
|
||||
name=self._provider_name,
|
||||
available=available,
|
||||
latency_ms=round(latency_ms, 2),
|
||||
error=error_msg,
|
||||
last_check=last_check_iso,
|
||||
model=self._model,
|
||||
model_available=model_available,
|
||||
)
|
||||
except Exception as e:
|
||||
latency_ms = (time.time() - start_time) * 1000
|
||||
status = ProviderHealthStatus(
|
||||
name=self._provider_name,
|
||||
available=False,
|
||||
latency_ms=round(latency_ms, 2),
|
||||
error=str(e)[:100],
|
||||
last_check=last_check_iso,
|
||||
model=self._model,
|
||||
model_available=False,
|
||||
)
|
||||
|
||||
with self._health_cache_lock:
|
||||
self._health_cache["health_check"] = {
|
||||
"value": status,
|
||||
"timestamp": current_time,
|
||||
}
|
||||
|
||||
return status
|
||||
|
||||
|
||||
def register_openai_provider():
|
||||
"""
|
||||
Register the OpenAI provider in the global registry.
|
||||
|
||||
This function should be called during module initialization
|
||||
to make the provider available through the registry.
|
||||
"""
|
||||
from .registry import registry
|
||||
|
||||
provider = get_openai_provider()
|
||||
registry.register("openai", provider)
|
||||
return provider
|
||||
|
||||
|
||||
_provider_instance: Optional[OpenAITranslationProvider] = None
|
||||
_provider_lock = threading.Lock()
|
||||
|
||||
|
||||
def get_openai_provider() -> OpenAITranslationProvider:
|
||||
"""Get or create the OpenAI provider instance (reads config from env)."""
|
||||
global _provider_instance
|
||||
if _provider_instance is None:
|
||||
with _provider_lock:
|
||||
if _provider_instance is None:
|
||||
from .config import ProvidersConfig
|
||||
|
||||
_provider_instance = OpenAITranslationProvider(
|
||||
api_key=ProvidersConfig.OPENAI_API_KEY,
|
||||
model=ProvidersConfig.OPENAI_MODEL,
|
||||
timeout=ProvidersConfig.OPENAI_TIMEOUT,
|
||||
max_retries=ProvidersConfig.OPENAI_MAX_RETRIES,
|
||||
retry_delay=ProvidersConfig.OPENAI_RETRY_DELAY,
|
||||
base_url=ProvidersConfig.OPENAI_BASE_URL,
|
||||
health_check_timeout=ProvidersConfig.OPENAI_HEALTH_CHECK_TIMEOUT,
|
||||
)
|
||||
return _provider_instance
|
||||
|
||||
|
||||
def reset_openai_provider() -> None:
|
||||
"""Reset the OpenAI provider singleton (useful when config changes)."""
|
||||
global _provider_instance
|
||||
with _provider_lock:
|
||||
_provider_instance = None
|
||||
Reference in New Issue
Block a user