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:
@@ -43,7 +43,7 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"api_key": null,
|
"api_key": null,
|
||||||
"base_url": null,
|
"base_url": null,
|
||||||
"model": "deepseek/deepseek-v3.2",
|
"model": "google/gemini-3.5-flash",
|
||||||
"timeout": 30,
|
"timeout": 30,
|
||||||
"max_retries": 3
|
"max_retries": 3
|
||||||
},
|
},
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"api_key": null,
|
"api_key": null,
|
||||||
"base_url": null,
|
"base_url": null,
|
||||||
"model": "anthropic/claude-3.5-haiku",
|
"model": "anthropic/claude-sonnet-4.6",
|
||||||
"timeout": 30,
|
"timeout": 30,
|
||||||
"max_retries": 3
|
"max_retries": 3
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -535,7 +535,7 @@ export default function AdminSettingsPage() {
|
|||||||
|
|
||||||
<ProviderCard
|
<ProviderCard
|
||||||
title="Traduction IA Premium"
|
title="Traduction IA Premium"
|
||||||
description="Affichée aux utilisateurs comme 'Traduction IA Premium'. Modèles haute qualité : openai/gpt-4o, anthropic/claude-3.5-sonnet, google/gemini-1.5-pro. Partage la même clé OpenRouter."
|
description="Affichée aux utilisateurs comme 'Traduction IA Premium'. Modèles haute qualité : openai/gpt-4o, anthropic/claude-sonnet-4.6, google/gemini-3.5-pro. Partage la même clé OpenRouter."
|
||||||
enabled={config.openrouter_premium.enabled}
|
enabled={config.openrouter_premium.enabled}
|
||||||
onToggle={(enabled) => updateProvider("openrouter_premium", { enabled })}
|
onToggle={(enabled) => updateProvider("openrouter_premium", { enabled })}
|
||||||
onTest={() => testProvider("openrouter_premium")}
|
onTest={() => testProvider("openrouter_premium")}
|
||||||
@@ -552,10 +552,10 @@ export default function AdminSettingsPage() {
|
|||||||
isLoading={loadingModelsProvider === "openrouter"}
|
isLoading={loadingModelsProvider === "openrouter"}
|
||||||
onFetchModels={() => fetchModels("openrouter")}
|
onFetchModels={() => fetchModels("openrouter")}
|
||||||
providerLabel="OpenRouter"
|
providerLabel="OpenRouter"
|
||||||
placeholder="openai/gpt-4o-mini"
|
placeholder="anthropic/claude-sonnet-4.6"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Recommandé : <code>openai/gpt-4o-mini</code> (~€0.15/doc) ou <code>anthropic/claude-3.5-haiku</code> (~€0.20/doc)
|
Recommandé : <code>anthropic/claude-sonnet-4.6</code> (~€0.20/doc) ou <code>openai/gpt-4o</code> (~€0.30/doc)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</ProviderCard>
|
</ProviderCard>
|
||||||
|
|||||||
@@ -296,13 +296,22 @@ export default function GlossariesPage() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{glossaries.length > 0 && (
|
{glossaries.length > 0 && (
|
||||||
<Link
|
<>
|
||||||
href="/dashboard/translate"
|
<button
|
||||||
className="flex items-center gap-1.5 text-[11px] font-bold text-brand-accent hover:underline shrink-0"
|
onClick={() => setCreateDialogOpen(true)}
|
||||||
>
|
className="premium-button px-4 py-2 text-[11px] uppercase tracking-widest !rounded-lg inline-flex items-center gap-1.5 cursor-pointer font-bold shrink-0 shadow-sm"
|
||||||
<ExternalLink size={12} />
|
>
|
||||||
{t('glossaries.grid.goToTranslate')}
|
<Plus size={12} />
|
||||||
</Link>
|
{t('glossaries.createNew') || "Créer un glossaire"}
|
||||||
|
</button>
|
||||||
|
<Link
|
||||||
|
href="/dashboard/translate"
|
||||||
|
className="flex items-center gap-1.5 text-[11px] font-bold text-brand-accent hover:underline shrink-0"
|
||||||
|
>
|
||||||
|
<ExternalLink size={12} />
|
||||||
|
{t('glossaries.grid.goToTranslate')}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ export const openaiModels = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const openrouterModels = [
|
export const openrouterModels = [
|
||||||
{ id: "deepseek/deepseek-chat-v3-0324", name: "DeepSeek V3" },
|
{ id: "google/gemini-3.5-flash", name: "Gemini 3.5 Flash" },
|
||||||
{ id: "anthropic/claude-3.5-haiku", name: "Claude 3.5 Haiku" },
|
{ id: "deepseek/deepseek-chat", name: "DeepSeek Chat" },
|
||||||
{ id: "google/gemini-2.0-flash-001", name: "Gemini 2.0 Flash" },
|
{ id: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface TranslationSettings {
|
interface TranslationSettings {
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ import os
|
|||||||
# NOTE: Stripe price IDs must be set via env vars in production.
|
# NOTE: Stripe price IDs must be set via env vars in production.
|
||||||
# Create products/prices in Stripe Dashboard: https://dashboard.stripe.com/products
|
# Create products/prices in Stripe Dashboard: https://dashboard.stripe.com/products
|
||||||
#
|
#
|
||||||
# LLM models used (via OpenRouter — March 2026):
|
# LLM models used (via OpenRouter — June 2026):
|
||||||
# Essentielle : deepseek/deepseek-v3.2 ($0.25 / $0.38 per 1M tokens)
|
# Essentielle : deepseek/deepseek-chat (DeepSeek V4/V3)
|
||||||
# Premium : anthropic/claude-3.5-haiku ($0.25 / $1.25 per 1M tokens)
|
# Premium : anthropic/claude-sonnet-4.6 (Claude Sonnet 4.6)
|
||||||
# or google/gemini-3-flash ($0.15 / $0.60)
|
# or google/gemini-3.5-flash (Gemini 3.5 Flash)
|
||||||
PLANS = {
|
PLANS = {
|
||||||
PlanType.FREE: {
|
PlanType.FREE: {
|
||||||
"name": "Free",
|
"name": "Free",
|
||||||
@@ -97,7 +97,7 @@ PLANS = {
|
|||||||
"max_file_size_mb": 25,
|
"max_file_size_mb": 25,
|
||||||
"max_chars_per_month": 2_000_000,
|
"max_chars_per_month": 2_000_000,
|
||||||
"providers": ["google", "google_cloud", "deepl", "openrouter"],
|
"providers": ["google", "google_cloud", "deepl", "openrouter"],
|
||||||
"ai_model_essential": "deepseek/deepseek-v3.2",
|
"ai_model_essential": "deepseek/deepseek-chat",
|
||||||
"features": [
|
"features": [
|
||||||
"pricing.plans.pro.feat1",
|
"pricing.plans.pro.feat1",
|
||||||
"pricing.plans.pro.feat2",
|
"pricing.plans.pro.feat2",
|
||||||
@@ -127,8 +127,8 @@ PLANS = {
|
|||||||
"max_file_size_mb": 50,
|
"max_file_size_mb": 50,
|
||||||
"max_chars_per_month": 10_000_000,
|
"max_chars_per_month": 10_000_000,
|
||||||
"providers": ["google", "google_cloud", "deepl", "openrouter", "openrouter_premium", "openai", "zai"],
|
"providers": ["google", "google_cloud", "deepl", "openrouter", "openrouter_premium", "openai", "zai"],
|
||||||
"ai_model_essential": "deepseek/deepseek-v3.2",
|
"ai_model_essential": "deepseek/deepseek-chat",
|
||||||
"ai_model_premium": "anthropic/claude-3.5-haiku",
|
"ai_model_premium": "anthropic/claude-sonnet-4.6",
|
||||||
"features": [
|
"features": [
|
||||||
"pricing.plans.business.feat1",
|
"pricing.plans.business.feat1",
|
||||||
"pricing.plans.business.feat2",
|
"pricing.plans.business.feat2",
|
||||||
|
|||||||
@@ -913,10 +913,10 @@ async def get_settings(admin_id: str = Depends(require_admin)):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
payload = settings.model_dump()
|
payload = settings.model_dump()
|
||||||
# Essentielle : DeepSeek V3.2 — meilleur rapport qualité/prix (mars 2026)
|
# 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="deepseek/deepseek-v3.2")
|
payload["openrouter"] = _merge_env(settings.openrouter, key_env="OPENROUTER_API_KEY", model_env="OPENROUTER_MODEL", default_model="google/gemini-3.5-flash")
|
||||||
# Premium : Claude 3.5 Haiku — précision maximale sur documents complexes
|
# 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-3.5-haiku")
|
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["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["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")
|
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",
|
"tier": "pro",
|
||||||
})
|
})
|
||||||
|
|
||||||
# AI Essentielle (OpenRouter — cheap model)
|
# AI Essentielle (OpenRouter — cheap model / Eco)
|
||||||
if _is_enabled("openrouter", key_var="OPENROUTER_API_KEY"):
|
if _is_enabled("openrouter", key_var="OPENROUTER_API_KEY"):
|
||||||
or_cfg = getattr(settings, "openrouter", None)
|
or_cfg = getattr(settings, "openrouter", None)
|
||||||
model = _resolve_model(
|
model = _resolve_model(
|
||||||
or_cfg.model if or_cfg else None,
|
or_cfg.model if or_cfg else None,
|
||||||
"OPENROUTER_MODEL",
|
"OPENROUTER_MODEL",
|
||||||
"deepseek/deepseek-v3.2",
|
"google/gemini-3.5-flash",
|
||||||
)
|
)
|
||||||
available.append({
|
available.append({
|
||||||
"id": "openrouter",
|
"id": "openrouter",
|
||||||
"label": "Traduction IA Essentielle",
|
"label": "Traduction IA Éco",
|
||||||
"description": "IA rapide et économique — idéale pour documents techniques",
|
"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",
|
"mode": "llm",
|
||||||
"tier": "pro",
|
"tier": "pro",
|
||||||
"model": model,
|
"model": model,
|
||||||
})
|
})
|
||||||
|
|
||||||
# AI Premium (OpenRouter — premium 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)
|
orp_cfg = getattr(settings, "openrouter_premium", None)
|
||||||
model = _resolve_model(
|
model = _resolve_model(
|
||||||
orp_cfg.model if orp_cfg else None,
|
orp_cfg.model if orp_cfg else None,
|
||||||
"OPENROUTER_PREMIUM_MODEL",
|
"OPENROUTER_PREMIUM_MODEL",
|
||||||
"anthropic/claude-3.5-haiku",
|
"anthropic/claude-sonnet-4.6",
|
||||||
)
|
)
|
||||||
available.append({
|
available.append({
|
||||||
"id": "openrouter_premium",
|
"id": "openrouter_premium",
|
||||||
"label": "Traduction IA 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",
|
"mode": "llm",
|
||||||
"tier": "business",
|
"tier": "business",
|
||||||
"model": model,
|
"model": model,
|
||||||
})
|
})
|
||||||
|
|
||||||
# OpenAI direct — if configured
|
# OpenAI direct — if configured with direct API key
|
||||||
if _is_enabled("openai", key_var="OPENAI_API_KEY"):
|
if _is_enabled("openai", key_var="OPENAI_API_KEY"):
|
||||||
oai_cfg = getattr(settings, "openai", None)
|
oai_cfg = getattr(settings, "openai", None)
|
||||||
model = _resolve_model(
|
model = _resolve_model(
|
||||||
@@ -156,24 +173,7 @@ async def get_available_providers():
|
|||||||
"model": model,
|
"model": model,
|
||||||
})
|
})
|
||||||
|
|
||||||
# DeepSeek direct — if configured
|
# MiniMax direct — if configured with direct API key
|
||||||
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
|
|
||||||
if _is_enabled("minimax", key_var="MINIMAX_API_KEY"):
|
if _is_enabled("minimax", key_var="MINIMAX_API_KEY"):
|
||||||
mm_cfg = getattr(settings, "minimax", None)
|
mm_cfg = getattr(settings, "minimax", None)
|
||||||
model = _resolve_model(
|
model = _resolve_model(
|
||||||
@@ -190,7 +190,7 @@ async def get_available_providers():
|
|||||||
"model": model,
|
"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"):
|
if _is_enabled("zai", key_var="ZAI_API_KEY"):
|
||||||
zai_cfg = getattr(settings, "zai", None)
|
zai_cfg = getattr(settings, "zai", None)
|
||||||
model = _resolve_model(
|
model = _resolve_model(
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ from config import config
|
|||||||
from translators import ExcelTranslator, WordTranslator, PowerPointTranslator
|
from translators import ExcelTranslator, WordTranslator, PowerPointTranslator
|
||||||
from models.subscription import PlanType
|
from models.subscription import PlanType
|
||||||
from middleware.tier_quota import tier_quota_service
|
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.validation import FileValidator, ValidationError, LanguageValidator, webhook_validator
|
||||||
from middleware.api_key_auth import get_authenticated_user, get_user_from_api_key
|
from middleware.api_key_auth import get_authenticated_user, get_user_from_api_key
|
||||||
from utils import file_handler
|
from utils import file_handler
|
||||||
@@ -135,9 +135,11 @@ file_handler_util = FileHandler()
|
|||||||
|
|
||||||
|
|
||||||
def _tier_for_quota(plan) -> str:
|
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):
|
if plan in (PlanType.PRO, PlanType.BUSINESS, PlanType.ENTERPRISE):
|
||||||
return "pro"
|
return "pro"
|
||||||
|
if plan == PlanType.STARTER:
|
||||||
|
return "starter"
|
||||||
return "free"
|
return "free"
|
||||||
|
|
||||||
|
|
||||||
@@ -630,6 +632,22 @@ async def translate_document_v1(
|
|||||||
},
|
},
|
||||||
headers={"Retry-After": str(retry_after)},
|
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
|
rate_limit_remaining = quota.remaining
|
||||||
else:
|
else:
|
||||||
rate_limit_remaining = -1
|
rate_limit_remaining = -1
|
||||||
@@ -993,7 +1011,9 @@ async def _run_translation_job(
|
|||||||
return (admin_val or "").strip() or os.getenv(env_var, default)
|
return (admin_val or "").strip() or os.getenv(env_var, default)
|
||||||
|
|
||||||
api_key = _cfg(_admin_cfg.openrouter.api_key, "OPENROUTER_API_KEY")
|
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
|
# Story 3.10: Retrieve and format glossary terms for LLM prompt
|
||||||
glossary_terms = None
|
glossary_terms = None
|
||||||
@@ -1075,7 +1095,7 @@ async def _run_translation_job(
|
|||||||
)
|
)
|
||||||
elif _p == "openrouter_premium":
|
elif _p == "openrouter_premium":
|
||||||
premium_key = _cfg(_admin_cfg.openrouter_premium.api_key, "OPENROUTER_API_KEY")
|
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:
|
if not premium_key:
|
||||||
premium_key = api_key # fall back to main openrouter key
|
premium_key = api_key # fall back to main openrouter key
|
||||||
if premium_key:
|
if premium_key:
|
||||||
@@ -1096,8 +1116,15 @@ async def _run_translation_job(
|
|||||||
)
|
)
|
||||||
elif _p == "deepseek":
|
elif _p == "deepseek":
|
||||||
ds_key = _cfg(getattr(_admin_cfg, "deepseek", None) and _admin_cfg.deepseek.api_key, "DEEPSEEK_API_KEY")
|
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")
|
ds_model = _cfg(getattr(_admin_cfg, "deepseek", None) and _admin_cfg.deepseek.model, "DEEPSEEK_MODEL", "deepseek/deepseek-chat")
|
||||||
if ds_key:
|
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(
|
translation_provider = DeepSeekTranslationProvider(
|
||||||
api_key=ds_key,
|
api_key=ds_key,
|
||||||
model=ds_model,
|
model=ds_model,
|
||||||
@@ -1295,13 +1322,30 @@ async def _run_translation_job(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if user_id:
|
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)
|
# Persist monthly usage counters in PostgreSQL (docs + pages)
|
||||||
pages = await asyncio.to_thread(
|
pages = await asyncio.to_thread(
|
||||||
_estimate_pages, input_path, file_extension
|
_estimate_pages, input_path, file_extension
|
||||||
)
|
)
|
||||||
await asyncio.to_thread(record_usage, user_id, pages)
|
await asyncio.to_thread(record_usage, user_id, pages, False, cost_factor)
|
||||||
logger.info(f"Job {job_id}: usage recorded — {pages} page(s)")
|
logger.info(f"Job {job_id}: usage recorded — {pages} page(s) with cost factor {cost_factor}")
|
||||||
|
|
||||||
# Apply watermark for Free-tier users
|
# Apply watermark for Free-tier users
|
||||||
plan_name = (user_plan or "free").lower()
|
plan_name = (user_plan or "free").lower()
|
||||||
|
|||||||
@@ -501,19 +501,21 @@ def check_usage_limits(user: User) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def record_usage(user_id: str, pages_count: int, use_credits: bool = False) -> bool:
|
def record_usage(
|
||||||
"""Record document translation usage"""
|
user_id: str, pages_count: int, use_credits: bool = False, cost_factor: int = 1
|
||||||
|
) -> bool:
|
||||||
|
"""Record document translation usage with optional cost factor depending on AI model"""
|
||||||
user = get_user_by_id(user_id)
|
user = get_user_by_id(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
updates = {
|
updates = {
|
||||||
"docs_translated_this_month": user.docs_translated_this_month + 1,
|
"docs_translated_this_month": user.docs_translated_this_month + cost_factor,
|
||||||
"pages_translated_this_month": user.pages_translated_this_month + pages_count,
|
"pages_translated_this_month": user.pages_translated_this_month + (pages_count * cost_factor),
|
||||||
}
|
}
|
||||||
|
|
||||||
if use_credits:
|
if use_credits:
|
||||||
updates["extra_credits"] = max(0, user.extra_credits - pages_count)
|
updates["extra_credits"] = max(0, user.extra_credits - (pages_count * cost_factor))
|
||||||
|
|
||||||
result = update_user(user_id, updates)
|
result = update_user(user_id, updates)
|
||||||
return result is not None
|
return result is not None
|
||||||
|
|||||||
@@ -903,11 +903,12 @@ RULES:
|
|||||||
else "image/png"
|
else "image/png"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Determine a vision model. If the current model doesn't support vision,
|
|
||||||
# use a fast vision fallback model like google/gemini-2.0-flash-001
|
|
||||||
vision_model = self.model
|
vision_model = self.model
|
||||||
if "deepseek" in vision_model:
|
vision_supported_keywords = ["gpt-4", "claude", "gemini", "fable", "pixtral", "llama-3.2", "grok-2"]
|
||||||
vision_model = "google/gemini-2.0-flash-001"
|
has_vision = any(kw in vision_model.lower() for kw in vision_supported_keywords)
|
||||||
|
|
||||||
|
if not has_vision:
|
||||||
|
vision_model = "google/gemini-3.5-flash"
|
||||||
|
|
||||||
session = self._get_session()
|
session = self._get_session()
|
||||||
payload = {
|
payload = {
|
||||||
@@ -1083,7 +1084,10 @@ ADDITIONAL CONTEXT AND INSTRUCTIONS:
|
|||||||
try:
|
try:
|
||||||
import openai
|
import openai
|
||||||
|
|
||||||
client = openai.OpenAI(api_key=self.api_key)
|
client_kwargs = {"api_key": self.api_key}
|
||||||
|
if self.base_url:
|
||||||
|
client_kwargs["base_url"] = self.base_url
|
||||||
|
client = openai.OpenAI(**client_kwargs)
|
||||||
|
|
||||||
# Read and encode image
|
# Read and encode image
|
||||||
with open(image_path, "rb") as img_file:
|
with open(image_path, "rb") as img_file:
|
||||||
@@ -1097,8 +1101,20 @@ ADDITIONAL CONTEXT AND INSTRUCTIONS:
|
|||||||
else "image/png"
|
else "image/png"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Determine a vision model. If the current model doesn't support vision,
|
||||||
|
# use a fast vision fallback model
|
||||||
|
vision_model = self.model
|
||||||
|
vision_supported_keywords = ["gpt-4", "claude", "gemini", "fable", "pixtral", "llama-3.2", "grok-2"]
|
||||||
|
has_vision = any(kw in vision_model.lower() for kw in vision_supported_keywords)
|
||||||
|
|
||||||
|
if not has_vision:
|
||||||
|
if self.base_url and "openrouter.ai" in self.base_url:
|
||||||
|
vision_model = "google/gemini-3.5-flash"
|
||||||
|
else:
|
||||||
|
vision_model = "gpt-4o-mini"
|
||||||
|
|
||||||
response = client.chat.completions.create(
|
response = client.chat.completions.create(
|
||||||
model=self.model, # gpt-4o and gpt-4o-mini support vision
|
model=vision_model,
|
||||||
messages=[
|
messages=[
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
@@ -1121,7 +1137,7 @@ ADDITIONAL CONTEXT AND INSTRUCTIONS:
|
|||||||
|
|
||||||
return response.choices[0].message.content.strip()
|
return response.choices[0].message.content.strip()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("openai_vision_error", error_type=type(e).__name__)
|
logger.warning("openai_vision_error", error_type=type(e).__name__, error=str(e))
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user