""" api_pool.py — Multi-Provider API Key Pool with Round-Robin Rotation & Auto-Fallback ====================================================================================== TIER STRUCTURE (Optimized for LARGE CHUNKS & TRANSLATION QUALITY as of Aug 16, 2026): ====================================================================================== TIER 0 : Local API Gateway (Super AI Gate) - localhost fallback TIER 1 : BEST FREE 1M+ CONTEXT MODELS (Nemotron 3 Ultra 550B, Nemotron 3.5 Lightning, Gemma 4 31B) TIER 2 : HIGH QUALITY 128K+ CONTEXT (GPT-OSS 120B, Llama 3.3 70B, Nemotron Super 120B) TIER 3 : FAST & RELIABLE 128K (Groq Llama 70B, Qwen 27B, Nvidia Llama 70B) TIER 4 : OPENROUTER PRO TIER (Qwen 72B, Hermes 405B, Gemini 2.5 Flash, Llama 70B) TIER 5 : OPENROUTER MID TIER (Nemotron 70B, Llama 3.3 70B, etc.) TIER 6 : OPENROUTER FAST TIER (Mistral, Gemma, Qwen 14B, Phi-4) TIER 7 : DIRECT CLOUD (SiliconFlow, TogetherAI, Gemini API) TIER 99 : Ollama Local (ultimate fallback) Load keys từ .env ở root project. Format: OPENROUTER_KEY_1=sk-or-... GEMINI_KEY_1=AIza... GROQ_KEY_1=gsk_... SILICONFLOW_KEY_1=sk-... TOGETHERAI_KEY_1=... NVIDIA_KEY_1=nvapi-... """ import json import os import time import threading import urllib.request import urllib.error from pathlib import Path # ────────────────────────────────────────────────────────────────────────────── # Provider Definitions (all OpenAI-compatible endpoints) # ────────────────────────────────────────────────────────────────────────────── _OR_HEADERS = {"HTTP-Referer": "https://trungsangviet.local", "X-Title": "TrungSangViet"} _OR_URL = "https://openrouter.ai/api/v1/chat/completions" def _or(name, model, tier, max_tokens=8192): """Helper tạo slot OpenRouter nhanh.""" return { "tier": tier, "provider_group": "openrouter", "url": _OR_URL, "model": model, "max_tokens": max_tokens, "extra_headers": _OR_HEADERS, } def _gate(name, model, tier=0, max_tokens=8192): """Helper tạo slot local gateway (Super AI Gate) chạy localhost.""" return { "tier": tier, "provider_group": "super_ai_gate", "url": "http://127.0.0.1:8000/v1/chat/completions", "model": model, "max_tokens": max_tokens, "extra_headers": {}, } PROVIDER_CONFIGS = { # ════════════════════════════════════════════════════════════════════════ # TIER 0 — Local API Gateway (Super AI Gate ở localhost) # ════════════════════════════════════════════════════════════════════════ "gate_qwen3_235b": _gate("gate_qwen3_235b", "qwen/qwen-2.5-72b-instruct:free", max_tokens=8192), "gate_deepseek_r1": _gate("gate_deepseek_r1", "nousresearch/hermes-3-llama-3.1-405b:free", max_tokens=8192), "gate_deepseek_v3": _gate("gate_deepseek_v3", "meta-llama/llama-3.3-70b-instruct:free", max_tokens=8192), "gate_gemini25_flash": _gate("gate_gemini25_flash", "google/gemini-2.5-flash:free", max_tokens=8192), "gate_nemotron_70b": _gate("gate_nemotron_70b", "meta-llama/llama-3.3-70b-instruct:free", max_tokens=8192), # ════════════════════════════════════════════════════════════════════════ # TIER 1 — BEST FREE 1M+ CONTEXT MODELS (PRIORITY FOR LARGE CHUNKS) # These handle full video transcripts in ONE SHOT without chunking # ════════════════════════════════════════════════════════════════════════ # 🏆 Nemotron 3 Ultra 550B — BEST REASONING, 1M CONTEXT, free on OpenRouter "or_nemotron3_ultra_550b": _or("or_nemotron3_ultra_550b", "nvidia/nemotron-3-ultra-550b-a55b:free", tier=1, max_tokens=8192), # 🏆 Nemotron 3.5 Lightning 30B — FAST + 1M CONTEXT, free on OpenRouter "or_nemotron35_lightning": _or("or_nemotron35_lightning", "nvidia/nemotron-3.5-lightning:free", tier=1, max_tokens=8192), # 🏆 Gemma 4 31B — 262K CONTEXT, strong multilingual, free on OpenRouter "or_gemma4_31b": _or("or_gemma4_31b", "google/gemma-4-31b-it:free", tier=1, max_tokens=8192), # 🏆 Nemotron 3 Super 120B — 128K context, reasoning fine-tuned, free on OpenRouter "or_nemotron3_super_120b": _or("or_nemotron3_super_120b", "nvidia/nemotron-3-super-120b-a12b:free", tier=1, max_tokens=8192), # 🏆 GPT-OSS 120B — 128K context, open weight, free on OpenRouter "or_gpt_oss_120b": _or("or_gpt_oss_120b", "openai/gpt-oss-20b:free", tier=1, max_tokens=8192), # 🏆 Llama 3.3 70B Versatile — 128K context, reliable, free on OpenRouter "or_llama33_70b_tier1": _or("or_llama33_70b_tier1", "poolside/laguna-s-2.1:free", tier=1, max_tokens=8192), # 🏆 Qwen 2.5 72B — Strong Chinese→Vietnamese, 128K context, free on OpenRouter "or_qwen25_72b": _or("or_qwen25_72b", "google/gemma-4-31b-it:free", tier=1, max_tokens=8192), # 🏆 Gemini 2.5 Flash — 1M CONTEXT, excellent Asian languages, free on OpenRouter "or_gemini25_flash_tier1": _or("or_gemini25_flash_tier1", "google/gemma-4-26b-a4b-it:free", tier=1, max_tokens=8192), # 🏆 DeepSeek V4 Flash — 128K context, cheap/fast, free on OpenRouter "or_deepseek_v4_flash": _or("or_deepseek_v4_flash", "nvidia/nemotron-3-nano-30b-a3b:free", tier=1, max_tokens=8192), # ════════════════════════════════════════════════════════════════════════ # TIER 2 — HIGH QUALITY 128K+ CONTEXT (DIRECT CLOUD - NEED KEYS) # ════════════════════════════════════════════════════════════════════════ # --- Groq: Siêu nhanh (LPU hardware), 128K context --- "groq_gpt_oss_120b": { "tier": 2, "provider_group": "groq", "url": "https://api.groq.com/openai/v1/chat/completions", "model": "openai/gpt-oss-120b", "max_tokens": 4096, "extra_headers": {}, }, "groq_llama33_70b": { "tier": 2, "provider_group": "groq", "url": "https://api.groq.com/openai/v1/chat/completions", "model": "openai/gpt-oss-20b", "max_tokens": 4096, "extra_headers": {}, }, # --- Nvidia NIM: Cloud GPU, 128K context --- "nvidia_nemotron3_ultra_550b": { "tier": 2, "provider_group": "nvidia", "url": "https://integrate.api.nvidia.com/v1/chat/completions", "model": "nvidia/nemotron-3-ultra-550b-a55b", "max_tokens": 8192, "extra_headers": {}, }, "nvidia_nemotron35_lightning": { "tier": 2, "provider_group": "nvidia", "url": "https://integrate.api.nvidia.com/v1/chat/completions", "model": "nvidia/nemotron-3.5-lightning-30b-a3b", "max_tokens": 8192, "extra_headers": {}, }, "nvidia_nemotron3_super": { "tier": 2, "provider_group": "nvidia", "url": "https://integrate.api.nvidia.com/v1/chat/completions", "model": "nvidia/llama-3.3-nemotron-super-49b-v1.5", "max_tokens": 8192, "extra_headers": {}, }, "nvidia_deepseek_v4_flash": { "tier": 2, "provider_group": "nvidia", "url": "https://integrate.api.nvidia.com/v1/chat/completions", "model": "meta/llama-3.1-70b-instruct", "max_tokens": 8192, "extra_headers": {}, }, # ════════════════════════════════════════════════════════════════════════ # TIER 3 — FAST & RELIABLE 128K (DIRECT CLOUD) # ════════════════════════════════════════════════════════════════════════ # Groq Qwen 27B — 32K context, has reasoning mode "groq_qwen3_27b": { "tier": 3, "provider_group": "groq", "url": "https://api.groq.com/openai/v1/chat/completions", "model": "qwen/qwen3.6-27b", "max_tokens": 4096, "extra_headers": {}, "extra_body": {"reasoning_format": "hidden"}, }, # Groq GPT-OSS 20B — fast, verified working on this org "groq_llama_8b": { "tier": 3, "provider_group": "groq", "url": "https://api.groq.com/openai/v1/chat/completions", "model": "openai/gpt-oss-20b", "max_tokens": 4096, "extra_headers": {}, }, # Nvidia Llama 3.1 70B — 128K context, solid base model "nvidia_llama70b": { "tier": 3, "provider_group": "nvidia", "url": "https://integrate.api.nvidia.com/v1/chat/completions", "model": "meta/llama-3.1-70b-instruct", "max_tokens": 8192, "extra_headers": {}, }, # Nvidia Gemma 4 31B — 128K context "nvidia_gemma4": { "tier": 3, "provider_group": "nvidia", "url": "https://integrate.api.nvidia.com/v1/chat/completions", "model": "google/gemma-4-31b-it", "max_tokens": 8192, "extra_headers": {}, }, # ════════════════════════════════════════════════════════════════════════ # TIER 4 — OPENROUTER PRO (High quality, 128K+ context) # ════════════════════════════════════════════════════════════════════════ "or_hermes_405b": _or("or_hermes_405b", "nvidia/nemotron-3-super-120b-a12b:free", tier=4, max_tokens=8192), "or_llama4_maverick": _or("or_llama4_maverick", "google/gemma-4-31b-it:free", tier=4, max_tokens=8192), "or_gemini20_flash": _or("or_gemini20_flash", "google/gemma-4-26b-a4b-it:free", tier=4, max_tokens=8192), "or_llama4_scout": _or("or_llama4_scout", "google/gemma-4-26b-a4b-it:free", tier=4, max_tokens=8192), # ════════════════════════════════════════════════════════════════════════ # TIER 5 — OPENROUTER MID (70B class) # ════════════════════════════════════════════════════════════════════════ "or_nemotron_70b": _or("or_nemotron_70b", "nvidia/nemotron-3-nano-30b-a3b:free", tier=5, max_tokens=8192), "or_llama33_70b": _or("or_llama33_70b", "poolside/laguna-s-2.1:free", tier=5, max_tokens=8192), "or_deepseek_r1": _or("or_deepseek_r1", "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free", tier=5, max_tokens=8192), "or_deepseek_v3": _or("or_deepseek_v3", "poolside/laguna-s-2.1:free", tier=5, max_tokens=8192), "or_qwen3_30b": _or("or_qwen3_30b", "openai/gpt-oss-20b:free", tier=5, max_tokens=8192), # ════════════════════════════════════════════════════════════════════════ # TIER 6 — OPENROUTER FAST (14-27B) # ════════════════════════════════════════════════════════════════════════ "or_mistral_small": _or("or_mistral_small", "openai/gpt-oss-20b:free", tier=6, max_tokens=4096), "or_gemma3_27b": _or("or_gemma3_27b", "google/gemma-4-26b-a4b-it:free", tier=6, max_tokens=4096), "or_qwen3_14b": _or("or_qwen3_14b", "nvidia/nemotron-3-nano-30b-a3b:free", tier=6, max_tokens=4096), "or_r1_distill_32b": _or("or_r1_distill_32b", "z-ai/glm-5.2:free", tier=6, max_tokens=4096), "or_phi4": _or("or_phi4", "z-ai/glm-5.2:free", tier=6, max_tokens=4096), # ════════════════════════════════════════════════════════════════════════ # TIER 7 — DIRECT CLOUD (need separate keys) # ════════════════════════════════════════════════════════════════════════ "siliconflow_pro": { "tier": 7, "provider_group": "siliconflow", "url": "https://api.siliconflow.cn/v1/chat/completions", "model": "Qwen/Qwen2.5-72B-Instruct", "max_tokens": 4096, "extra_headers": {}, }, "gemini": { "tier": 7, "provider_group": "gemini", "url": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", "model": "gemini-3.5-flash", "max_tokens": 4096, "extra_headers": {}, }, "togetherai": { "tier": 7, "provider_group": "togetherai", "url": "https://api.together.xyz/v1/chat/completions", "model": "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", "max_tokens": 2048, "extra_headers": {}, }, "siliconflow_free": { "tier": 8, "provider_group": "siliconflow", "url": "https://api.siliconflow.cn/v1/chat/completions", "model": "Qwen/Qwen2.5-7B-Instruct", "max_tokens": 2048, "extra_headers": {}, }, # ════════════════════════════════════════════════════════════════════════ # TIER 99 — Ollama Local (ultimate fallback, never fails) # ════════════════════════════════════════════════════════════════════════ "ollama": { "tier": 99, "provider_group": "ollama", "url": "http://127.0.0.1:11434/v1/chat/completions", "model": "hf.co/lmstudio-community/Qwen3.5-9B-GGUF:Q6_K", "max_tokens": 2048, "extra_headers": {}, }, } # ────────────────────────────────────────────────────────────────────────────── # Map: .env key prefix → provider name(s) nhận key đó # ────────────────────────────────────────────────────────────────────────────── KEY_ENV_MAP = { "SUPER_AI_GATE_KEY": [ "gate_qwen3_235b", "gate_deepseek_r1", "gate_deepseek_v3", "gate_gemini25_flash", "gate_nemotron_70b" ], # 1 OpenRouter key → TẤT CẢ slot OpenRouter (Tier 1-6), xoay vòng tự động # Ưu tiên Tier 1 (1M+ context models) lên đầu danh sách "OPENROUTER_KEY": [ # TIER 1 — BEST FREE 1M+ CONTEXT (Priority for large chunks) "or_nemotron3_ultra_550b", "or_nemotron35_lightning", "or_gemma4_31b", "or_nemotron3_super_120b", "or_gpt_oss_120b", "or_llama33_70b_tier1", "or_qwen25_72b", "or_gemini25_flash_tier1", "or_deepseek_v4_flash", # TIER 4 — OPENROUTER PRO "or_hermes_405b", "or_llama4_maverick", "or_gemini20_flash", "or_llama4_scout", # TIER 5 — OPENROUTER MID "or_nemotron_70b", "or_llama33_70b", "or_deepseek_r1", "or_deepseek_v3", "or_qwen3_30b", # TIER 6 — OPENROUTER FAST "or_mistral_small", "or_gemma3_27b", "or_qwen3_14b", "or_r1_distill_32b", "or_phi4", ], "SILICONFLOW_KEY": ["siliconflow_pro", "siliconflow_free"], "GEMINI_KEY": ["gemini"], # 1 Groq key → 4 slot groq, xoay vòng round-robin tự động "GROQ_KEY": ["groq_gpt_oss_120b", "groq_llama33_70b", "groq_qwen3_27b", "groq_llama_8b"], "TOGETHERAI_KEY": ["togetherai"], # 1 Nvidia key → 6 slot nvidia, xoay vòng round-robin tự động "NVIDIA_KEY": [ "nvidia_nemotron3_ultra_550b", "nvidia_nemotron35_lightning", "nvidia_nemotron3_super", "nvidia_deepseek_v4_flash", "nvidia_llama70b", "nvidia_gemma4", ], } # Cooldown durations COOLDOWN_429 = 60 # giây — sau khi hit rate limit COOLDOWN_5XX = 30 # giây — sau khi server error REQUEST_TIMEOUT = 120 # ────────────────────────────────────────────────────────────────────────────── # Custom Exceptions # ────────────────────────────────────────────────────────────────────────────── class RateLimitError(Exception): pass class ServerError(Exception): pass # ────────────────────────────────────────────────────────────────────────────── # ApiPool — Thread-safe singleton # ────────────────────────────────────────────────────────────────────────────── class ApiPool: """ Multi-provider API key pool. - 1 OpenRouter key → 5 model slots, tự xoay vòng - Nhiều key cùng provider → thêm slot, tăng throughput - Rate limit → cooldown slot đó, nhảy sang slot tiếp theo - Tất cả cloud fail → Ollama Local """ _instance = None _init_lock = threading.Lock() @classmethod def instance(cls): if cls._instance is None: with cls._init_lock: if cls._instance is None: cls._instance = cls() return cls._instance def __init__(self): self._slot_lock = threading.Lock() self._rr_index = {} # provider_group → round-robin counter self._env = {} self._slots = [] self._session_slots = {} # session_id -> slot_id (cố định slot cho cùng session) self._load_env() self._build_slots() # ── .env loading ────────────────────────────────────────────────────────── def _load_env(self): root = Path(__file__).parent.parent env_path = root / ".env" self._env = {} # 1. ƯU TIÊN load từ os.environ (HF Secrets Variables and secrets) — FIX 2026-09-02 # HF Spaces inject secrets as env vars, không có file .env for k, v in os.environ.items(): if not v or not v.strip(): continue # Chỉ lấy key liên quan tới pool (tránh rác env) if (k.startswith("GEMINI_KEY") or k.startswith("GROQ_KEY") or k.startswith("OPENROUTER_KEY") or k.startswith("XKIRO_KEY") or k.startswith("NVIDIA_KEY") or k.startswith("SILICONFLOW_KEY") or k.startswith("TOGETHERAI_KEY") or k.startswith("SUPER_AI_GATE") or k.startswith("NIM_")): self._env[k] = v.strip().strip('"').strip("'") if env_path.exists(): with open(env_path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line or line.startswith("#") or "=" not in line: continue k, v = line.split("=", 1) k, v = k.strip(), v.strip().strip('"').strip("'") if v: self._env[k] = v # Tự động nạp thêm API keys từ config.json nếu chưa có trong .env cfg_path = root / "config.json" if cfg_path.exists(): try: with open(cfg_path, "r", encoding="utf-8") as f: cfg_json = json.load(f) if "groq_key" in cfg_json and cfg_json["groq_key"] and "GROQ_KEY_1" not in self._env: self._env["GROQ_KEY_1"] = cfg_json["groq_key"].strip() if "gemini_key" in cfg_json and cfg_json["gemini_key"] and "GEMINI_KEY_1" not in self._env: self._env["GEMINI_KEY_1"] = cfg_json["gemini_key"].strip() if "nvidia_key" in cfg_json and cfg_json["nvidia_key"] and "NVIDIA_KEY_1" not in self._env: self._env["NVIDIA_KEY_1"] = cfg_json["nvidia_key"].strip() if "nim_api_key" in cfg_json and cfg_json["nim_api_key"] and "NVIDIA_KEY_1" not in self._env: self._env["NVIDIA_KEY_1"] = cfg_json["nim_api_key"].strip() except Exception: pass # Tự động nạp cấu hình và key của cổng gateway từ D:\TOOL GOM API\.env nếu có gate_env_path = Path("D:/TOOL GOM API/.env") if gate_env_path.exists(): try: with open(gate_env_path, "r", encoding="utf-8") as f: gate_env = {} for line in f: line = line.strip() if not line or line.startswith("#") or "=" not in line: continue k, v = line.split("=", 1) k, v = k.strip(), v.strip().strip('"').strip("'") if v: gate_env[k] = v # Ưu tiên cấu hình trong tool .env trước, nếu chưa có thì lấy từ gate .env làm mặc định if "SUPER_AI_GATE_KEY_1" not in self._env and "MASTER_API_KEY" in gate_env: self._env["SUPER_AI_GATE_KEY_1"] = gate_env["MASTER_API_KEY"] if "SUPER_AI_GATE_PORT" not in self._env and "PORT" in gate_env: self._env["SUPER_AI_GATE_PORT"] = gate_env["PORT"] # OpenRouter key thật nằm trong gate .env (OPENROUTER_API_KEY) if "OPENROUTER_KEY_1" not in self._env and "OPENROUTER_API_KEY" in gate_env: self._env["OPENROUTER_KEY_1"] = gate_env["OPENROUTER_API_KEY"] except Exception: pass # Cài port mặc định nếu không cấu hình if "SUPER_AI_GATE_PORT" not in self._env: self._env["SUPER_AI_GATE_PORT"] = "8000" def reload(self): """Reload .env và rebuild slots (dùng sau khi user thêm key).""" self._load_env() self._build_slots() def _collect_keys(self, prefix): """Lấy OPENROUTER_KEY_1, _2, ... từ env.""" keys = [] for i in range(1, 50): v = self._env.get(f"{prefix}_{i}") if v: keys.append(v) return keys # ── Slot building ───────────────────────────────────────────────────────── def _build_slots(self): slots = [] gate_port = self._env.get("SUPER_AI_GATE_PORT", "8000") gate_url = f"http://127.0.0.1:{gate_port}/v1/chat/completions" for env_prefix, provider_names in KEY_ENV_MAP.items(): keys = self._collect_keys(env_prefix) if not keys: continue for key in keys: for pname in provider_names: cfg = PROVIDER_CONFIGS[pname] # Tự động trỏ URL về localhost cổng gate cho các provider gate_* url = gate_url if pname.startswith("gate_") else cfg["url"] slots.append({ "id": f"{pname}@{self._mask_key(key)}", "provider": pname, "provider_group": cfg.get("provider_group", pname), "tier": cfg["tier"], "url": url, "model": cfg["model"], "max_tokens": cfg["max_tokens"], "key": key, "extra_headers": dict(cfg.get("extra_headers", {})), "extra_body": dict(cfg.get("extra_body", {})), "cooldown_until": 0.0, "error_count": 0, }) # Ollama luôn có, không cần key ollama_cfg = PROVIDER_CONFIGS["ollama"] slots.append({ "id": "ollama_local", "provider": "ollama", "provider_group": "ollama", "tier": 99, "url": ollama_cfg["url"], "model": ollama_cfg["model"], "max_tokens": ollama_cfg["max_tokens"], "key": None, "extra_headers": {}, "extra_body": {}, "cooldown_until": 0.0, "error_count": 0, }) slots.sort(key=lambda s: s["tier"]) with self._slot_lock: self._slots = slots self._rr_index = {} # ── Public API ──────────────────────────────────────────────────────────── def translate(self, json_text: str, system_prompt: str, log_fn=None, session_id=None, context_str=None) -> str: """ Dịch json_text qua pool, hỗ trợ session stickiness và truyền ngữ cảnh. """ with self._slot_lock: snapshot = list(self._slots) now = time.time() last_err = None # 1. Thử dùng slot đã cố định cho session này trước sticky_slot = None if session_id: with self._slot_lock: sticky_slot_id = self._session_slots.get(session_id) if sticky_slot_id: for s in snapshot: if s["id"] == sticky_slot_id: if s["cooldown_until"] <= now: sticky_slot = s break if sticky_slot: try: result = self._call_slot(sticky_slot, json_text, system_prompt, context_str) sticky_slot["error_count"] = 0 if log_fn: log_fn(f" ✅ [Sticky: {sticky_slot['provider']}] model={sticky_slot['model'].split('/')[-1]} — OK") return result except Exception as e: if log_fn: log_fn(f" ⚠️ [Sticky: {sticky_slot['provider']}] Lỗi: {str(e)[:100]}, chuyển đổi slot khác...") with self._slot_lock: if isinstance(e, RateLimitError): sticky_slot["cooldown_until"] = now + COOLDOWN_429 elif isinstance(e, ServerError): sticky_slot["cooldown_until"] = now + COOLDOWN_5XX else: sticky_slot["cooldown_until"] = now + 10 sticky_slot["error_count"] = sticky_slot.get("error_count", 0) + 1 if session_id in self._session_slots: del self._session_slots[session_id] last_err = e # 2. Nếu không có sticky_slot hoặc sticky_slot bị lỗi, tìm slot mới từ các Tier tiers = sorted(set(s["tier"] for s in snapshot)) for tier in tiers: tier_slots = [s for s in snapshot if s["tier"] == tier] by_group = {} for s in tier_slots: by_group.setdefault(s["provider_group"], []).append(s) ordered = self._interleave_rr(by_group) for slot in ordered: if slot["cooldown_until"] > now: rem = int(slot["cooldown_until"] - now) if log_fn and last_err is None: # chỉ log bỏ qua ở lượt quét đầu log_fn(f" ⏭ [{slot['provider']}] cooldown {rem}s, bỏ qua...") continue try: result = self._call_slot(slot, json_text, system_prompt, context_str) slot["error_count"] = 0 if log_fn: log_fn(f" ✅ [{slot['provider']}] model={slot['model'].split('/')[-1]} — OK") if session_id: with self._slot_lock: self._session_slots[session_id] = slot["id"] return result except RateLimitError as e: # FIX: ghi cooldown/error_count dưới lock (translate() chạy đa luồng) with self._slot_lock: slot["cooldown_until"] = time.time() + COOLDOWN_429 if log_fn: log_fn(f" ⚡ [{slot['provider']}] Rate limit — cooldown {COOLDOWN_429}s") last_err = e except ServerError as e: with self._slot_lock: slot["cooldown_until"] = time.time() + COOLDOWN_5XX slot["error_count"] += 1 if log_fn: log_fn(f" 🔴 [{slot['provider']}] Server error — cooldown {COOLDOWN_5XX}s") last_err = e except Exception as e: err_msg = str(e) # FIX: match r"\b404\b" chính xác (trước đây "404" khớp cả "4040"), # và 404 = endpoint/model sai vĩnh viễn -> cooldown dài để khỏi phí quota. import re as _re is_http_404 = bool(_re.search(r"\b404\b", err_msg)) if "10061" in err_msg or "refused" in err_msg.lower(): _cd = 60 elif is_http_404: _cd = 1800 else: _cd = 0 with self._slot_lock: if _cd: slot["cooldown_until"] = time.time() + _cd slot["error_count"] += 1 if log_fn: log_fn(f" ⚠️ [{slot['provider']}] {type(e).__name__}: {str(e)[:120]}") last_err = e raise Exception(f"Tất cả {len(snapshot)} slot đều fail. Lỗi cuối: {last_err}") def count_healthy(self) -> int: now = time.time() with self._slot_lock: return sum(1 for s in self._slots if s["cooldown_until"] <= now) def count_slots(self) -> int: with self._slot_lock: return len(self._slots) def status_report(self) -> str: """Trả về chuỗi báo cáo trạng thái pool.""" now = time.time() lines = [] with self._slot_lock: for s in self._slots: cd = max(0, int(s["cooldown_until"] - now)) icon = "✅" if cd == 0 else "⏸" cd_str = f" (cooldown {cd}s)" if cd > 0 else "" lines.append(f" {icon} [{s['provider']}] {s['model'].split('/')[-1]}{cd_str}") return "\n".join(lines) # ── Internal ────────────────────────────────────────────────────────────── def _interleave_rr(self, by_group: dict) -> list: """Trộn các nhóm provider theo round-robin để phân tải đều.""" # FIX: toàn bộ đọc/ghi _rr_index dưới lock + so sánh identity (trước đây # dict== nuốt slot khi 2 slot nội dung bằng nhau tuyệt đối). with self._slot_lock: result = [] groups = list(by_group.keys()) if not groups: return result max_len = max(len(v) for v in by_group.values()) for i in range(max_len): for g in groups: slots = by_group[g] rr = self._rr_index.get(g, 0) idx = (rr + i) % len(slots) s = slots[idx] if not any(s is r for r in result): result.append(s) # Advance RR index for g in groups: self._rr_index[g] = (self._rr_index.get(g, 0) + 1) % len(by_group[g]) return result def _call_slot(self, slot: dict, json_text: str, system_prompt: str, context_str: str = None) -> str: user_content = json_text if context_str: user_content = f"{context_str}\n\nJSON cần dịch:\n{json_text}" payload = { "model": slot["model"], "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_content}, ], "temperature": 0.2, "top_p": 0.95, "max_tokens": slot["max_tokens"], "stream": False, } if slot.get("extra_body"): payload.update(slot["extra_body"]) data = json.dumps(payload).encode("utf-8") headers = { "Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" } if slot["key"]: headers["Authorization"] = f"Bearer {slot['key']}" headers.update(slot.get("extra_headers", {})) req = urllib.request.Request(slot["url"], data=data, headers=headers) try: with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp: res = json.loads(resp.read().decode("utf-8")) content = res["choices"][0]["message"]["content"] return (content or "").strip() except urllib.error.HTTPError as e: code = e.code body = "" try: body = e.read().decode("utf-8") except Exception: pass if code == 429: raise RateLimitError(f"429: {body[:150]}") if code >= 500: raise ServerError(f"{code}: {body[:150]}") raise Exception(f"HTTP {code}: {body[:200]}") except urllib.error.URLError as e: raise Exception(f"Network error: {e.reason}") @staticmethod def _mask_key(key): if not key: return "nokey" return key[:6] + "..." + key[-4:] if len(key) > 10 else "****" # ────────────────────────────────────────────────────────────────────────────── # Shared system prompt # ────────────────────────────────────────────────────────────────────────────── TRANSLATION_SYSTEM_PROMPT = ( "Bạn là một biên dịch viên xuất sắc và dịch giả phụ đề phim chuyên nghiệp từ tiếng Trung sang tiếng Việt.\n" "Hãy dịch các câu thoại (value) trong JSON dưới đây sang tiếng Việt tự nhiên, mượt mà, " "cuốn hút, giàu cảm xúc và diễn đạt trôi chảy theo văn phong phim ảnh.\n" "Khi dịch văn bản OCR sang tiếng Việt, giữ nguyên các từ khóa tiếng Anh, thương hiệu thời trang đường phố (streetwear brands) và các thuật ngữ thịnh hành (ví dụ: Syna, outfits, BAPE, v.v.) như gốc trong phụ đề tiếng Việt cuối cùng. Không dịch hoặc chuyển âm (transliterate) các từ tiếng Anh đặc thù này sang tiếng Việt.\n" "Lựa chọn đại từ nhân xưng linh hoạt, chính xác và đồng nhất theo ngữ cảnh cuộc hội thoại (anh/em, cô/tôi, ta/ngươi, sư phụ/đồ đệ...).\n" "Không dịch từ-qua-từ (word-by-word) một cách khô cứng. Hãy dịch thoát ý.\n" "Giữ nguyên các key (số ID). Chỉ trả về duy nhất chuỗi JSON đã dịch, " "không kèm giải thích, markdown, hay ký tự thừa. JSON thuần." )