feat: update to June 2026 models (Claude Sonnet 4.6, Gemini 3.5 Flash), add glossary button, and implement cost factor quota & vision fallback
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m4s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m4s
This commit is contained in:
@@ -913,10 +913,10 @@ async def get_settings(admin_id: str = Depends(require_admin)):
|
||||
return d
|
||||
|
||||
payload = settings.model_dump()
|
||||
# Essentielle : DeepSeek V3.2 — meilleur rapport qualité/prix (mars 2026)
|
||||
payload["openrouter"] = _merge_env(settings.openrouter, key_env="OPENROUTER_API_KEY", model_env="OPENROUTER_MODEL", default_model="deepseek/deepseek-v3.2")
|
||||
# Premium : Claude 3.5 Haiku — précision maximale sur documents complexes
|
||||
payload["openrouter_premium"] = _merge_env(settings.openrouter_premium, key_env="OPENROUTER_API_KEY", model_env="OPENROUTER_PREMIUM_MODEL", default_model="anthropic/claude-3.5-haiku")
|
||||
# Essentielle : Gemini 3.5 Flash / DeepSeek Chat — meilleur rapport qualité/prix (juin 2026)
|
||||
payload["openrouter"] = _merge_env(settings.openrouter, key_env="OPENROUTER_API_KEY", model_env="OPENROUTER_MODEL", default_model="google/gemini-3.5-flash")
|
||||
# Premium : Claude Sonnet 4.6 — précision maximale sur documents complexes
|
||||
payload["openrouter_premium"] = _merge_env(settings.openrouter_premium, key_env="OPENROUTER_API_KEY", model_env="OPENROUTER_PREMIUM_MODEL", default_model="anthropic/claude-sonnet-4.6")
|
||||
payload["openai"] = _merge_env(settings.openai, key_env="OPENAI_API_KEY", model_env="OPENAI_MODEL", default_model="gpt-4o-mini")
|
||||
payload["deepl"] = _merge_env(settings.deepl, key_env="DEEPL_API_KEY")
|
||||
payload["deepseek"] = _merge_env(settings.deepseek, key_env="DEEPSEEK_API_KEY", model_env="DEEPSEEK_MODEL", default_model="deepseek-chat")
|
||||
|
||||
@@ -105,41 +105,58 @@ async def get_available_providers():
|
||||
"tier": "pro",
|
||||
})
|
||||
|
||||
# AI Essentielle (OpenRouter — cheap model)
|
||||
# AI Essentielle (OpenRouter — cheap model / Eco)
|
||||
if _is_enabled("openrouter", key_var="OPENROUTER_API_KEY"):
|
||||
or_cfg = getattr(settings, "openrouter", None)
|
||||
model = _resolve_model(
|
||||
or_cfg.model if or_cfg else None,
|
||||
"OPENROUTER_MODEL",
|
||||
"deepseek/deepseek-v3.2",
|
||||
"google/gemini-3.5-flash",
|
||||
)
|
||||
available.append({
|
||||
"id": "openrouter",
|
||||
"label": "Traduction IA Essentielle",
|
||||
"description": "IA rapide et économique — idéale pour documents techniques",
|
||||
"label": "Traduction IA Éco",
|
||||
"description": "IA rapide, économique et complète — supporte les images",
|
||||
"mode": "llm",
|
||||
"tier": "pro",
|
||||
"model": model,
|
||||
})
|
||||
|
||||
# AI Standard (DeepSeek via OpenRouter or direct DeepSeek API)
|
||||
if _is_enabled("openrouter", key_var="OPENROUTER_API_KEY") or _is_enabled("deepseek", key_var="DEEPSEEK_API_KEY"):
|
||||
ds_cfg = getattr(settings, "deepseek", None)
|
||||
model = _resolve_model(
|
||||
ds_cfg.model if ds_cfg else None,
|
||||
"DEEPSEEK_MODEL",
|
||||
"deepseek/deepseek-chat",
|
||||
)
|
||||
available.append({
|
||||
"id": "deepseek",
|
||||
"label": "Traduction IA Standard",
|
||||
"description": "IA ultra-précise pour le texte (ne traduit pas les images)",
|
||||
"mode": "llm",
|
||||
"tier": "pro",
|
||||
"model": model,
|
||||
})
|
||||
|
||||
# AI Premium (OpenRouter — premium model)
|
||||
if _is_enabled("openrouter_premium", key_var="OPENROUTER_API_KEY"):
|
||||
if _is_enabled("openrouter_premium", key_var="OPENROUTER_API_KEY") or _is_enabled("openrouter", key_var="OPENROUTER_API_KEY"):
|
||||
orp_cfg = getattr(settings, "openrouter_premium", None)
|
||||
model = _resolve_model(
|
||||
orp_cfg.model if orp_cfg else None,
|
||||
"OPENROUTER_PREMIUM_MODEL",
|
||||
"anthropic/claude-3.5-haiku",
|
||||
"anthropic/claude-sonnet-4.6",
|
||||
)
|
||||
available.append({
|
||||
"id": "openrouter_premium",
|
||||
"label": "Traduction IA Premium",
|
||||
"description": "IA haute précision (GPT-4, Claude) — meilleure qualité littéraire",
|
||||
"description": "IA haut de gamme — excellente qualité littéraire et multimodal",
|
||||
"mode": "llm",
|
||||
"tier": "business",
|
||||
"model": model,
|
||||
})
|
||||
|
||||
# OpenAI direct — if configured
|
||||
# OpenAI direct — if configured with direct API key
|
||||
if _is_enabled("openai", key_var="OPENAI_API_KEY"):
|
||||
oai_cfg = getattr(settings, "openai", None)
|
||||
model = _resolve_model(
|
||||
@@ -156,24 +173,7 @@ async def get_available_providers():
|
||||
"model": model,
|
||||
})
|
||||
|
||||
# DeepSeek direct — if configured
|
||||
if _is_enabled("deepseek", key_var="DEEPSEEK_API_KEY"):
|
||||
ds_cfg = getattr(settings, "deepseek", None)
|
||||
model = _resolve_model(
|
||||
ds_cfg.model if ds_cfg else None,
|
||||
"DEEPSEEK_MODEL",
|
||||
"deepseek-chat",
|
||||
)
|
||||
available.append({
|
||||
"id": "deepseek",
|
||||
"label": "Traduction IA Express",
|
||||
"description": "Traduction IA rapide et optimisée",
|
||||
"mode": "llm",
|
||||
"tier": "pro",
|
||||
"model": model,
|
||||
})
|
||||
|
||||
# MiniMax direct — if configured
|
||||
# MiniMax direct — if configured with direct API key
|
||||
if _is_enabled("minimax", key_var="MINIMAX_API_KEY"):
|
||||
mm_cfg = getattr(settings, "minimax", None)
|
||||
model = _resolve_model(
|
||||
@@ -190,7 +190,7 @@ async def get_available_providers():
|
||||
"model": model,
|
||||
})
|
||||
|
||||
# z.AI / xAI Grok — if configured
|
||||
# z.AI / xAI Grok — if configured with direct API key
|
||||
if _is_enabled("zai", key_var="ZAI_API_KEY"):
|
||||
zai_cfg = getattr(settings, "zai", None)
|
||||
model = _resolve_model(
|
||||
|
||||
@@ -45,7 +45,7 @@ from config import config
|
||||
from translators import ExcelTranslator, WordTranslator, PowerPointTranslator
|
||||
from models.subscription import PlanType
|
||||
from middleware.tier_quota import tier_quota_service
|
||||
from services.auth_service import record_usage
|
||||
from services.auth_service import record_usage, check_usage_limits
|
||||
from middleware.validation import FileValidator, ValidationError, LanguageValidator, webhook_validator
|
||||
from middleware.api_key_auth import get_authenticated_user, get_user_from_api_key
|
||||
from utils import file_handler
|
||||
@@ -135,9 +135,11 @@ file_handler_util = FileHandler()
|
||||
|
||||
|
||||
def _tier_for_quota(plan) -> str:
|
||||
"""Map plan to quota tier: pro (and equivalent) = unlimited, else free."""
|
||||
"""Map plan to quota tier: pro/starter (and equivalent) = unlimited, else free."""
|
||||
if plan in (PlanType.PRO, PlanType.BUSINESS, PlanType.ENTERPRISE):
|
||||
return "pro"
|
||||
if plan == PlanType.STARTER:
|
||||
return "starter"
|
||||
return "free"
|
||||
|
||||
|
||||
@@ -630,6 +632,22 @@ async def translate_document_v1(
|
||||
},
|
||||
headers={"Retry-After": str(retry_after)},
|
||||
)
|
||||
|
||||
# Strict database plan limit check (Starter, Pro, Business, Enterprise)
|
||||
usage = check_usage_limits(current_user)
|
||||
if not usage["can_translate"]:
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail={
|
||||
"error": "QUOTA_EXCEEDED",
|
||||
"message": f"Monthly limit reached ({usage['docs_used']}/{usage['docs_limit']} documents). Upgrade your plan for more.",
|
||||
"details": {
|
||||
"current_usage": usage["docs_used"],
|
||||
"limit": usage["docs_limit"],
|
||||
"tier": tier,
|
||||
},
|
||||
},
|
||||
)
|
||||
rate_limit_remaining = quota.remaining
|
||||
else:
|
||||
rate_limit_remaining = -1
|
||||
@@ -993,7 +1011,9 @@ async def _run_translation_job(
|
||||
return (admin_val or "").strip() or os.getenv(env_var, default)
|
||||
|
||||
api_key = _cfg(_admin_cfg.openrouter.api_key, "OPENROUTER_API_KEY")
|
||||
model = _cfg(_admin_cfg.openrouter.model, "OPENROUTER_MODEL", "deepseek/deepseek-v3.2")
|
||||
model = _cfg(_admin_cfg.openrouter.model, "OPENROUTER_MODEL", "google/gemini-3.5-flash")
|
||||
if model in ("deepseek/deepseek-v3.2", "google/gemini-2.0-flash-001"):
|
||||
model = "google/gemini-3.5-flash"
|
||||
|
||||
# Story 3.10: Retrieve and format glossary terms for LLM prompt
|
||||
glossary_terms = None
|
||||
@@ -1075,7 +1095,7 @@ async def _run_translation_job(
|
||||
)
|
||||
elif _p == "openrouter_premium":
|
||||
premium_key = _cfg(_admin_cfg.openrouter_premium.api_key, "OPENROUTER_API_KEY")
|
||||
premium_model = _cfg(_admin_cfg.openrouter_premium.model, "OPENROUTER_PREMIUM_MODEL", "anthropic/claude-3.5-haiku")
|
||||
premium_model = _cfg(_admin_cfg.openrouter_premium.model, "OPENROUTER_PREMIUM_MODEL", "anthropic/claude-sonnet-4.6")
|
||||
if not premium_key:
|
||||
premium_key = api_key # fall back to main openrouter key
|
||||
if premium_key:
|
||||
@@ -1096,8 +1116,15 @@ async def _run_translation_job(
|
||||
)
|
||||
elif _p == "deepseek":
|
||||
ds_key = _cfg(getattr(_admin_cfg, "deepseek", None) and _admin_cfg.deepseek.api_key, "DEEPSEEK_API_KEY")
|
||||
ds_model = _cfg(getattr(_admin_cfg, "deepseek", None) and _admin_cfg.deepseek.model, "DEEPSEEK_MODEL", "deepseek-chat")
|
||||
if ds_key:
|
||||
ds_model = _cfg(getattr(_admin_cfg, "deepseek", None) and _admin_cfg.deepseek.model, "DEEPSEEK_MODEL", "deepseek/deepseek-chat")
|
||||
if not ds_key and api_key:
|
||||
translation_provider = OpenAITranslationProvider(
|
||||
api_key=api_key,
|
||||
model=ds_model,
|
||||
base_url="https://openrouter.ai/api/v1",
|
||||
timeout=int(os.getenv("OPENROUTER_TIMEOUT", "60")),
|
||||
)
|
||||
elif ds_key:
|
||||
translation_provider = DeepSeekTranslationProvider(
|
||||
api_key=ds_key,
|
||||
model=ds_model,
|
||||
@@ -1295,13 +1322,30 @@ async def _run_translation_job(
|
||||
)
|
||||
|
||||
if user_id:
|
||||
await tier_quota_service.increment_on_success(user_id)
|
||||
# Determine cost factor based on selected provider and model
|
||||
cost_factor = 1
|
||||
provider_lower = (provider or "").lower()
|
||||
|
||||
prov_model = ""
|
||||
if translation_provider:
|
||||
prov_model = getattr(translation_provider, "model", "") or ""
|
||||
|
||||
prov_model_lower = prov_model.lower()
|
||||
if any(k in prov_model_lower for k in ["claude", "fable", "gpt-4"]) or provider_lower == "openrouter_premium":
|
||||
if "haiku" in prov_model_lower:
|
||||
cost_factor = 1
|
||||
else:
|
||||
cost_factor = 5
|
||||
|
||||
for _ in range(cost_factor):
|
||||
await tier_quota_service.increment_on_success(user_id)
|
||||
|
||||
# Persist monthly usage counters in PostgreSQL (docs + pages)
|
||||
pages = await asyncio.to_thread(
|
||||
_estimate_pages, input_path, file_extension
|
||||
)
|
||||
await asyncio.to_thread(record_usage, user_id, pages)
|
||||
logger.info(f"Job {job_id}: usage recorded — {pages} page(s)")
|
||||
await asyncio.to_thread(record_usage, user_id, pages, False, cost_factor)
|
||||
logger.info(f"Job {job_id}: usage recorded — {pages} page(s) with cost factor {cost_factor}")
|
||||
|
||||
# Apply watermark for Free-tier users
|
||||
plan_name = (user_plan or "free").lower()
|
||||
|
||||
Reference in New Issue
Block a user