Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
13 KiB
JSON

{"repo": "binupraj/logiclens", "files": {"backend/services/llm_client.py": {"e9c997edf7bafdbda585f2f2e023b81c18f2e9c1": {"t": 1786068457.6224577, "n": 12146, "h": "bb37ede447c212c2", "c": "\"\"\"\nUnified LLM Gateway Service\n============================\n\n╔══════════════════════════════════════════════════════════════════════════════╗\n║ ARCHITECTURAL COMPLIANCE VERIFICATION ║\n╠══════════════════════════════════════════════════════════════════════════════╣\n║ ✅ VERIFIED: 2024-12-24 ║\n║ ✅ Single AI model for entire application: google/gemma-3-27b-it:free ║\n║ ✅ Single API key management point ║\n║ ✅ Unified rate limiting for ALL clients ║\n║ ✅ NO other direct OpenRouter calls exist in codebase ║\n╚══════════════════════════════════════════════════════════════════════════════╝\n\nThis is the SINGLE entry point for all OpenRouter API calls.\nBoth the chatbot and browser extension MUST use this service.\n\nSecurity Features:\n- API key loaded once from .env (never exposed to frontend)\n- Rate limiting: 10 requests per minute per IP\n- Input size caps: 2000 characters max\n- Output token limits: 1500 tokens max\n- Timeout protection: 30 seconds max\n\nWhy this exists:\n- Ensures one API key for entire system\n- Prevents free-tier abuse\n- Centralizes error handling\n- Makes monitoring easier\n\"\"\"\n\nimport os\nimport json\nimport time\nimport requests\nfrom dotenv import load_dotenv\nfrom collections import defaultdict\n\n# Load environment variables once at module initialization\nload_dotenv()\n\n\nclass RateLimiter:\n \"\"\"\n Simple in-memory rate limiter for free-tier protection.\n No database required - suitable for hackathon/demo use.\n \"\"\"\n \n def __init__(self, max_requests=10, window_seconds=60):\n self.max_requests = max_requests\n self.window_seconds = window_seconds\n # Store request timestamps per IP: {ip: [timestamp1, timestamp2, ...]}\n self.requests = defaultdict(list)\n \n def is_allowed(self, identifier):\n \"\"\"\n Check if request is allowed under rate limit.\n Automatically cleans old requests outside time window.\n \"\"\"\n now = time.time()\n \n # Remove timestamps older than the window\n self.requests[identifier] = [\n ts for ts in self.requests[identifier] \n if now - ts < self.window_seconds\n ]\n \n # Check if under limit\n if len(self.requests[identifier]) >= self.max_requests:\n return False\n \n # Record this request\n self.requests[identifier].append(now)\n return True\n \n def get_remaining(self, identifier):\n \"\"\"Get remaining requests available\"\"\"\n now = time.time()\n self.requests[identifier] = [\n ts for ts in self.requests[identifier] \n if now - ts < self.window_seconds\n ]\n return max(0, self.max_requests - len(self.requests[identifier]))\n\n\nclass LLMClient:\n \"\"\"\n Unified LLM gateway for both chatbot and extension.\n \n CRITICAL: This is the ONLY place that should talk to OpenRouter.\n Any other direct OpenRouter calls are a security/cost violation.\n \n ✅ AUTO-FALLBACK: Automatically tries multiple free models if one fails.\n \"\"\"\n \n # Free-tier safety limits\n # Note: MAX_INPUT_LENGTH applies to TOTAL message length (system + user)\n # This is intentionally generous to allow detailed system prompts\n MAX_INPUT_LENGTH = 10000 # Characters - allows for detailed prompts + user input\n MAX_OUTPUT_TOKENS = 1500 # Tokens - keeps responses concise\n REQUEST_TIMEOUT = 30 # Seconds - prevents hanging\n \n # ===== FREE MODELS WITH AUTO-FALLBACK =====\n # If one model fails, automatically try the next one\n FREE_MODELS = [\n \"google/gemma-3-27b-it:free\", # Google Gemma 3 27B\n \"meta-llama/llama-3.3-70b-instruct:free\", # Meta Llama 3.3 70B\n \"qwen/qwen3-32b:free\", # Qwen 3 32B\n \"mistralai/mistral-small-3.1-24b-instruct:free\", # Mistral Small 3.1\n \"deepseek/deepseek-chat-v3-0324:free\", # DeepSeek Chat V3\n ]\n \n def __init__(self):\n # Load API key ONCE at startup (never expose to frontend)\n self.api_key = os.getenv(\"OPENROUTER_API_KEY\")\n \n if not self.api_key:\n print(\"⚠️ WARNING: OPENROUTER_API_KEY not found in .env\")\n print(\"⚠️ All LLM calls will fail until key is configured\")\n \n # Primary model (can be overridden via env var)\n self.model = os.getenv(\"OPENROUTER_MODEL\", self.FREE_MODELS[0])\n self.base_url = \"https://openrouter.ai/api/v1/chat/completions\"\n \n # Track which model was last successful (for status reporting)\n self.last_successful_model = None\n \n # Rate limiter: 10 requests per minute per IP (suitable for hackathon)\n self.rate_limiter = RateLimiter(max_requests=10, window_seconds=60)\n \n # Headers for OpenRouter (never expose this to frontend)\n self.headers = {\n \"Authorization\": f\"Bearer {self.api_key}\",\n \"Content-Type\": \"application/json\",\n \"HTTP-Referer\": \"http://localhost:5001\",\n \"X-Title\": \"Unified Reasoning Assistant\"\n }\n \n def check_rate_limit(self, client_ip):\n \"\"\"\n Verify request is within rate limits.\n Raises exception if limit exceeded.\n \"\"\"\n if not self.rate_limiter.is_allowed(client_ip):\n remaining_time = self.rate_limiter.window_seconds\n raise Exception(\n f\"Rate limit exceeded. Please wait {remaining_time}s before trying again.\"\n )\n \n def validate_input(self, text):\n \"\"\"\n Enforce input size limits for free tier.\n Prevents abuse and excessive token usage.\n \n Note: This validates total message content, not individual fields.\n System prompts are included in the validation.\n \"\"\"\n if not text:\n raise ValueError(\"Input text cannot be empty\")\n \n if len(text) > self.MAX_INPUT_LENGTH:\n raise ValueError(\n f\"Input too long. Maximum {self.MAX_INPUT_LENGTH} characters allowed. \"\n f\"You provided {len(text)} characters.\"\n )\n \n def chat_completion(self, messages, client_ip, temperature=0.7, json_mode=True):\n \"\"\"\n Single entry point for ALL LLM calls in the system.\n \n ✅ AUTO-FALLBACK: If primary model fails, automatically tries other free models.\n \n This method:\n 1. Validates rate limits\n 2. Validates input size\n 3. Calls OpenRouter (with auto-fallback to other models)\n 4. Handles errors gracefully\n 5. Returns cleaned response\n \n Args:\n messages: List of message dicts with 'role' and 'content'\n client_ip: Client IP address for rate limiting\n temperature: Sampling temperature (0-1), default 0.7\n json_mode: If True, enforces JSON-only output\n \n Returns:\n str: LLM response (JSON string if json_mode=True, else plain text)\n \n Raises:\n Exception: On rate limit, validation, or API errors (after all models fail)\n \"\"\"\n \n # Step 1: Rate limiting check (prevents free-tier abuse)\n self.check_rate_limit(client_ip)\n \n # Step 2: Validate total input length\n total_input = \" \".join([m.get(\"content\", \"\") for m in messages])\n self.validate_input(total_input)\n \n # Step 3: Prepare messages (add JSON enforcement if needed)\n final_messages = messages.copy()\n if json_mode:\n final_messages.append({\n \"role\": \"system\",\n \"content\": \"You must respond with valid JSON only. No markdown, no explanations, no code blocks.\"\n })\n \n # Step 4: Build list of models to try (primary first, then fallbacks)\n models_to_try = [self.model]\n for model in self.FREE_MODELS:\n if model != self.model and model not in models_to_try:\n models_to_try.append(model)\n \n # Step 5: Try each model until one succeeds\n last_error = None\n for model_name in models_to_try:\n try:\n content = self._call_model(model_name, final_messages, temperature, json_mode)\n # Success! Record which model worked\n self.last_successful_model = model_name\n if model_name != self.model:\n print(f\"✅ Fallback successful: {model_name}\")\n return content\n except Exception as e:\n last_error = e\n print(f\"⚠️ Model {model_name} failed: {str(e)[:100]}\")\n continue # Try next model\n \n # All models failed\n raise Exception(f\"All models failed. Last error: {str(last_error)}\")\n \n def _call_model(self, model_name, messages, temperature, json_mode):\n \"\"\"\n Make actual API call to a specific model.\n Separated from chat_completion to enable fallback logic.\n \"\"\"\n payload = {\n \"model\": model_name,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": self.MAX_OUTPUT_TOKENS\n }\n \n try:\n response = requests.post(\n self.base_url,\n headers=self.headers,\n json=payload,\n timeout=self.REQUEST_TIMEOUT\n )\n \n # Handle rate limiting from OpenRouter itself\n if response.status_code == 429:\n raise Exception(f\"Rate limit for {model_name}\")\n \n # Handle other errors\n if response.status_code != 200:\n error_msg = response.text[:200]\n raise Exception(f\"API error ({response.status_code}): {error_msg}\")\n \n # Extract response\n data = response.json()\n content = data[\"choices\"][0][\"message\"][\"content\"]\n \n # Clean response if JSON mode\n if json_mode:\n content = self._clean_json_response(content)\n \n return content\n \n except requests.exceptions.Timeout:\n raise Exception(f\"Timeout for {model_name}\")\n except requests.exceptions.ConnectionError:\n raise Exception(\"Cannot connect to OpenRouter API\")\n except requests.exceptions.RequestException as e:\n raise Exception(f\"Request error: {str(e)}\")\n except KeyError as e:\n raise Exception(f\"Unexpected response format: {str(e)}\")\n \n def _clean_json_response(self, text):\n \"\"\"\n Remove markdown code blocks from JSON responses.\n LLMs often wrap JSON in ```json blocks despite instructions.\n \"\"\"\n if not text:\n return text\n \n cleaned = text.strip()\n \n # Remove markdown fences\n if cleaned.startswith(\"```json\"):\n cleaned = cleaned[7:]\n elif cleaned.startswith(\"```\"):\n cleaned = cleaned[3:]\n \n if cleaned.endswith(\"```\"):\n cleaned = cleaned[:-3]\n \n return cleaned.strip()\n \n def is_configured(self):\n \"\"\"Check if API key is configured\"\"\"\n return bool(self.api_key)\n \n def get_status(self):\n \"\"\"Get client status for health checks\"\"\"\n return {\n \"configured\": self.is_configured(),\n \"primary_model\": self.model,\n \"last_successful_model\": self.last_successful_model,\n \"fallback_models\": self.FREE_MODELS,\n \"max_input_length\": self.MAX_INPUT_LENGTH,\n \"max_output_tokens\": self.MAX_OUTPUT_TOKENS,\n \"rate_limit\": f\"{self.rate_limiter.max_requests} req / {self.rate_limiter.window_seconds}s\"\n }\n\n\n# Global singleton instance\n# This ensures one API key, one rate limiter across entire application\nllm_client = LLMClient()\n"}}}}