diff --git a/gemini_flash.py b/gemini_flash.py index a81d35e..ecf02f0 100644 --- a/gemini_flash.py +++ b/gemini_flash.py @@ -1,4 +1,4 @@ -import json, http.client, ssl, os, logging, base64, secrets, time, hashlib +import json, http.client, ssl, os, logging, base64, time, hashlib from flask import Response, request, abort from searx.plugins import Plugin, PluginInfo from searx.result_types import EngineResults @@ -7,6 +7,10 @@ from markupsafe import Markup logger = logging.getLogger(__name__) +# Constants +TOKEN_EXPIRY_SEC = 60 +CONNECTION_TIMEOUT_SEC = 30 + class SXNGPlugin(Plugin): id = "gemini_flash" @@ -21,11 +25,21 @@ class SXNGPlugin(Plugin): self.provider = os.getenv('LLM_PROVIDER', 'openrouter').lower() self.api_key = os.getenv('OPENROUTER_API_KEY') if self.provider == 'openrouter' else os.getenv('GEMINI_API_KEY') self.model = os.getenv('GEMINI_MODEL', 'gemma-3-27b-it') if self.provider == 'gemini' else os.getenv('OPENROUTER_MODEL', 'google/gemma-3-27b-it:free') - self.max_tokens = int(os.getenv('GEMINI_MAX_TOKENS', 500)) - self.temperature = float(os.getenv('GEMINI_TEMPERATURE', 0.2)) + try: + self.max_tokens = int(os.getenv('GEMINI_MAX_TOKENS', 500)) + except ValueError: + self.max_tokens = 500 + try: + self.temperature = float(os.getenv('GEMINI_TEMPERATURE', 0.2)) + except ValueError: + self.temperature = 0.2 self.base_url = os.getenv('OPENROUTER_BASE_URL', 'openrouter.ai') # Stable secret for multi-worker environments - self.secret = os.getenv('SXNG_LLM_SECRET') or hashlib.sha256(self.api_key.encode()).hexdigest() + if self.api_key: + self.secret = os.getenv('SXNG_LLM_SECRET') or hashlib.sha256(self.api_key.encode()).hexdigest() + else: + self.secret = os.getenv('SXNG_LLM_SECRET', '') + logger.warning("Gemini Flash plugin: No API key configured, plugin will be inactive") def init(self, app): @app.route('/gemini-stream', methods=['POST']) @@ -38,9 +52,10 @@ class SXNGPlugin(Plugin): ts, sig = token.split('.', 1) query_clean = q.strip() expected = hashlib.sha256(f"{ts}{query_clean}{self.secret}".encode()).hexdigest() - if sig != expected or (time.time() - float(ts)) > 60: + if sig != expected or (time.time() - float(ts)) > TOKEN_EXPIRY_SEC: abort(403) - except: abort(403) + except (ValueError, KeyError, AttributeError): + abort(403) context_text = data.get('context', '') if not self.api_key or not q: @@ -60,7 +75,7 @@ class SXNGPlugin(Plugin): host = "generativelanguage.googleapis.com" path = f"/v1/models/{self.model}:streamGenerateContent?key={self.api_key}" try: - conn = http.client.HTTPSConnection(host, context=ssl.create_default_context()) + conn = http.client.HTTPSConnection(host, timeout=CONNECTION_TIMEOUT_SEC, context=ssl.create_default_context()) payload = {"contents": [{"parts": [{"text": prompt}]}], "generationConfig": {"maxOutputTokens": self.max_tokens, "temperature": self.temperature}} conn.request("POST", path, body=json.dumps(payload), headers={"Content-Type": "application/json"}) res = conn.getresponse() @@ -93,7 +108,7 @@ class SXNGPlugin(Plugin): def generate_openrouter(): try: - conn = http.client.HTTPSConnection(self.base_url, context=ssl.create_default_context()) + conn = http.client.HTTPSConnection(self.base_url, timeout=CONNECTION_TIMEOUT_SEC, context=ssl.create_default_context()) payload = { "model": self.model, "messages": [{"role": "user", "content": prompt}], @@ -104,8 +119,8 @@ class SXNGPlugin(Plugin): headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", - "HTTP-Referer": "https://github.com/cra88y/searxng-stream-gemini", - "X-Title": "SearXNG Stream" + "HTTP-Referer": "https://github.com/searxng/searxng", + "X-Title": "SearXNG LLM Plugin" } conn.request("POST", "/api/v1/chat/completions", body=json.dumps(payload), headers=headers) res = conn.getresponse() @@ -128,7 +143,8 @@ class SXNGPlugin(Plugin): obj, _ = decoder.raw_decode(data_str) content = obj.get("choices", [{}])[0].get("delta", {}).get("content", "") if content: yield content - except: pass + except json.JSONDecodeError: + pass conn.close() except Exception as e: logger.error(f"OpenRouter Stream Exception: {e}") @@ -138,83 +154,95 @@ class SXNGPlugin(Plugin): def post_search(self, request, search) -> EngineResults: results = EngineResults() - if not self.active or not self.api_key or search.search_query.pageno > 1: - return results + try: + if not self.active or not self.api_key or search.search_query.pageno > 1: + return results - raw_results = search.result_container.get_ordered_results() - context_list = [f"[{i+1}] {r.get('title')}: {r.get('content')}" for i, r in enumerate(raw_results[:6])] - context_str = "\n".join(context_list) + raw_results = search.result_container.get_ordered_results() + context_list = [f"[{i+1}] {r.get('title')}: {r.get('content')}" for i, r in enumerate(raw_results[:6])] + context_str = "\n".join(context_list) - # Stateless Handshake - ts = str(int(time.time())) - q_clean = search.search_query.query.strip() - sig = hashlib.sha256(f"{ts}{q_clean}{self.secret}".encode()).hexdigest() - tk = f"{ts}.{sig}" + # Stateless Handshake + ts = str(int(time.time())) + q_clean = search.search_query.query.strip() + sig = hashlib.sha256(f"{ts}{q_clean}{self.secret}".encode()).hexdigest() + tk = f"{ts}.{sig}" - b64_context = base64.b64encode(context_str.encode('utf-8')).decode('utf-8') - js_q = json.dumps(q_clean) + b64_context = base64.b64encode(context_str.encode('utf-8')).decode('utf-8') + js_q = json.dumps(q_clean) - html_payload = f''' - - - - ''' - search.result_container.answers.add(results.types.Answer(answer=Markup(html_payload))) + #sxng-stream-box {{ + max-height: 0; + overflow: hidden; + transition: max-height 0.4s ease-out; + }} + #sxng-stream-box.sxng-open {{ + max-height: 30em; + }} + +
+

+
+ + ''' + search.result_container.answers.add(results.types.Answer(answer=Markup(html_payload))) + except Exception as e: + logger.error(f"Gemini Flash plugin error: {e}") return results