""" DeepSeek Provider - Cloud LLM translation via DeepSeek API. DeepSeek uses an OpenAI-compatible Chat Completions API. """ import threading import time from datetime import datetime, timezone from typing import Any, Dict, List, Optional import requests from requests.exceptions import Timeout, ConnectionError as RequestsConnectionError from core.logging import get_logger from .base import TranslationProvider from .schemas import ProviderHealthStatus, TranslationRequest, TranslationResponse logger = get_logger(__name__) DEEPSEEK_RATE_LIMITED = "DEEPSEEK_RATE_LIMITED" DEEPSEEK_INVALID_KEY = "DEEPSEEK_INVALID_KEY" DEEPSEEK_TIMEOUT = "DEEPSEEK_TIMEOUT" DEEPSEEK_SERVICE_ERROR = "DEEPSEEK_SERVICE_ERROR" _RETRYABLE_ERRORS = {DEEPSEEK_RATE_LIMITED, DEEPSEEK_TIMEOUT, DEEPSEEK_SERVICE_ERROR} 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 _get_language_name(code: str) -> str: 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", "uk": "Ukrainian", "cs": "Czech", "sv": "Swedish", "ro": "Romanian", "hu": "Hungarian", "el": "Greek", "he": "Hebrew", } return language_names.get(code.split("-")[0].lower(), code) class DeepSeekProviderError(Exception): 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) class DeepSeekTranslationProvider(TranslationProvider): """ DeepSeek translation provider using OpenAI-compatible API. Supports DeepSeek Chat and DeepSeek Reasoner models. """ def __init__( self, api_key: str, model: str = "deepseek-chat", timeout: int = 60, max_retries: int = 3, retry_delay: float = 1.0, base_url: str = "https://api.deepseek.com/v1", ): if not api_key or not api_key.strip(): raise ValueError("DeepSeek API key cannot be empty") self._api_key = api_key self._model = model self._base_url = base_url.rstrip("/") self._provider_name = "deepseek" self._timeout = timeout self._max_retries = max_retries self._retry_delay = retry_delay 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: if not text or not text.strip(): return text, {} 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) if response.status_code == 401: raise DeepSeekProviderError(DEEPSEEK_INVALID_KEY, "Cle API DeepSeek invalide.") if response.status_code == 429: raise DeepSeekProviderError(DEEPSEEK_RATE_LIMITED, "Limite de requetes DeepSeek atteinte.") if response.status_code >= 500: raise DeepSeekProviderError(DEEPSEEK_SERVICE_ERROR, "Service DeepSeek temporairement indisponible.") if response.status_code != 200: raise DeepSeekProviderError(DEEPSEEK_SERVICE_ERROR, f"Erreur DeepSeek: {response.text[:200]}") data = response.json() choices = data.get("choices", []) if not choices: raise DeepSeekProviderError(DEEPSEEK_SERVICE_ERROR, "Reponse DeepSeek vide") content = choices[0].get("message", {}).get("content", "") if not content: raise DeepSeekProviderError(DEEPSEEK_SERVICE_ERROR, "Reponse DeepSeek vide") return content.strip(), data.get("usage", {}) except Timeout: raise DeepSeekProviderError(DEEPSEEK_TIMEOUT, "Delai d'attente DeepSeek depasse.") except RequestsConnectionError: raise DeepSeekProviderError(DEEPSEEK_SERVICE_ERROR, "Service DeepSeek indisponible.") except DeepSeekProviderError: raise except Exception as e: raise DeepSeekProviderError(DEEPSEEK_SERVICE_ERROR, f"Erreur DeepSeek: {str(e)[:100]}") def get_name(self) -> str: return self._provider_name def is_available(self) -> bool: try: headers = {"Authorization": f"Bearer {self._api_key}"} response = requests.get(f"{self._base_url}/models", headers=headers, timeout=5) return response.status_code == 200 except Exception: return False def translate_text(self, request: TranslationRequest) -> TranslationResponse: 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 = request.metadata.get("custom_prompt") if request.metadata else None system_prompt = custom_prompt or DEFAULT_TRANSLATION_PROMPT.format( source_lang=source_lang_name, target_lang=target_lang_name ) last_error = None for attempt in range(self._max_retries + 1): try: start_time = time.time() result, usage = self._make_api_request(text, system_prompt) latency = time.time() - start_time logger.info("deepseek_translation_success", chars=len(text), source_lang=source_language, target_lang=target_language, model=self._model, latency_ms=round(latency * 1000, 2), retries=attempt) return TranslationResponse( translated_text=result, provider_name=self._provider_name, from_cache=False, source_language=source_language) except DeepSeekProviderError as e: last_error = e if e.code not in _RETRYABLE_ERRORS or attempt >= self._max_retries: break delay = self._retry_delay * (2 ** attempt) time.sleep(delay) return TranslationResponse( translated_text=text, provider_name=self._provider_name, from_cache=False, error=last_error.message if last_error else "Unknown error", error_code=last_error.code if last_error else DEEPSEEK_SERVICE_ERROR) def translate_batch(self, requests: List[TranslationRequest]) -> List[TranslationResponse]: return [self.translate_text(req) for req in requests] def health_check(self) -> ProviderHealthStatus: start_time = time.time() try: headers = {"Authorization": f"Bearer {self._api_key}"} response = requests.get(f"{self._base_url}/models", headers=headers, timeout=5) latency_ms = (time.time() - start_time) * 1000 return ProviderHealthStatus( name=self._provider_name, available=response.status_code == 200, latency_ms=round(latency_ms, 2), last_check=datetime.now(timezone.utc).isoformat(), model=self._model) except Exception as e: return ProviderHealthStatus( name=self._provider_name, available=False, latency_ms=round((time.time() - start_time) * 1000, 2), error=str(e)[:100], last_check=datetime.now(timezone.utc).isoformat(), model=self._model) _provider_instance: Optional[DeepSeekTranslationProvider] = None _provider_lock = threading.Lock() def get_deepseek_provider() -> DeepSeekTranslationProvider: global _provider_instance if _provider_instance is None: with _provider_lock: if _provider_instance is None: from .config import ProvidersConfig _provider_instance = DeepSeekTranslationProvider( api_key=ProvidersConfig.DEEPSEEK_API_KEY, model=ProvidersConfig.DEEPSEEK_MODEL, timeout=ProvidersConfig.DEEPSEEK_TIMEOUT, max_retries=ProvidersConfig.DEEPSEEK_MAX_RETRIES, retry_delay=ProvidersConfig.DEEPSEEK_RETRY_DELAY, base_url=ProvidersConfig.DEEPSEEK_BASE_URL, ) return _provider_instance def register_deepseek_provider(): from .registry import registry provider = get_deepseek_provider() registry.register("deepseek", provider) return provider def reset_deepseek_provider(): global _provider_instance with _provider_lock: _provider_instance = None