Add OpenRouter provider with DeepSeek support - best value for translation (.14/M tokens)

This commit is contained in:
2025-11-30 22:10:34 +01:00
parent b65e683d32
commit 3346817a8a
7 changed files with 601 additions and 8 deletions

View File

@@ -483,6 +483,143 @@ ADDITIONAL CONTEXT AND INSTRUCTIONS:
return []
class OpenRouterTranslationProvider(TranslationProvider):
"""
OpenRouter API translation - Access to many cheap & high-quality models
Recommended models for translation (by cost/quality):
- deepseek/deepseek-chat: $0.14/M tokens - Excellent quality, very cheap
- mistralai/mistral-7b-instruct: $0.06/M tokens - Fast and cheap
- meta-llama/llama-3.1-8b-instruct: $0.06/M tokens - Good quality
- google/gemma-2-9b-it: $0.08/M tokens - Good for European languages
"""
def __init__(self, api_key: str, model: str = "deepseek/deepseek-chat", system_prompt: str = ""):
self.api_key = api_key
self.model = model
self.custom_system_prompt = system_prompt
self.base_url = "https://openrouter.ai/api/v1"
self.provider_name = "openrouter"
self._session = None
def _get_session(self):
"""Get or create a requests session for connection pooling"""
if self._session is None:
import requests
self._session = requests.Session()
self._session.headers.update({
"Authorization": f"Bearer {self.api_key}",
"HTTP-Referer": "https://translate-app.local",
"X-Title": "Document Translator",
"Content-Type": "application/json"
})
return self._session
def translate(self, text: str, target_language: str, source_language: str = 'auto') -> str:
if not text or not text.strip():
return text
# Skip very short text or numbers only
if len(text.strip()) < 2 or text.strip().isdigit():
return text
# Check cache first
cached = _translation_cache.get(text, target_language, source_language, self.provider_name)
if cached is not None:
return cached
try:
session = self._get_session()
# Optimized prompt for translation
system_prompt = f"""Translate to {target_language}. Output ONLY the translation, nothing else. Preserve formatting."""
if self.custom_system_prompt:
system_prompt = f"{system_prompt}\n\nContext: {self.custom_system_prompt}"
response = session.post(
f"{self.base_url}/chat/completions",
json={
"model": self.model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": text}
],
"temperature": 0.2,
"max_tokens": 1000
},
timeout=30
)
response.raise_for_status()
result = response.json()
translated = result.get("choices", [{}])[0].get("message", {}).get("content", "").strip()
if translated:
# Cache the result
_translation_cache.set(text, target_language, source_language, self.provider_name, translated)
return translated
return text
except Exception as e:
print(f"OpenRouter translation error: {e}")
return text
def translate_batch(self, texts: List[str], target_language: str, source_language: str = 'auto') -> List[str]:
"""
Batch translate using OpenRouter with parallel requests.
Uses caching to avoid redundant translations.
"""
if not texts:
return []
results = [''] * len(texts)
texts_to_translate = []
indices_to_translate = []
# Check cache first
for i, text in enumerate(texts):
if not text or not text.strip():
results[i] = text if text else ''
else:
cached = _translation_cache.get(text, target_language, source_language, self.provider_name)
if cached is not None:
results[i] = cached
else:
texts_to_translate.append(text)
indices_to_translate.append(i)
if not texts_to_translate:
return results
# Translate in parallel batches
import concurrent.futures
def translate_one(text: str) -> str:
return self.translate(text, target_language, source_language)
# Use thread pool for parallel requests
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
translated = list(executor.map(translate_one, texts_to_translate))
# Map back results
for idx, trans in zip(indices_to_translate, translated):
results[idx] = trans
return results
@staticmethod
def list_recommended_models() -> List[dict]:
"""List recommended models for translation with pricing"""
return [
{"id": "deepseek/deepseek-chat", "name": "DeepSeek Chat", "price": "$0.14/M tokens", "quality": "Excellent", "speed": "Fast"},
{"id": "mistralai/mistral-7b-instruct", "name": "Mistral 7B", "price": "$0.06/M tokens", "quality": "Good", "speed": "Very Fast"},
{"id": "meta-llama/llama-3.1-8b-instruct", "name": "Llama 3.1 8B", "price": "$0.06/M tokens", "quality": "Good", "speed": "Fast"},
{"id": "google/gemma-2-9b-it", "name": "Gemma 2 9B", "price": "$0.08/M tokens", "quality": "Good", "speed": "Fast"},
{"id": "anthropic/claude-3-haiku", "name": "Claude 3 Haiku", "price": "$0.25/M tokens", "quality": "Excellent", "speed": "Fast"},
{"id": "openai/gpt-4o-mini", "name": "GPT-4o Mini", "price": "$0.15/M tokens", "quality": "Excellent", "speed": "Fast"},
]
class WebLLMTranslationProvider(TranslationProvider):
"""WebLLM browser-based translation (client-side processing)"""