Spaces:
Paused
Paused
| from stores.llm.LLMInterface import LLMInterface | |
| import logging | |
| import requests | |
| import re | |
| import os | |
| class OpenRouterProvider(LLMInterface): | |
| def __init__(self, url: str = None, model: str = None, | |
| default_input_max_characters: int = 1000, | |
| default_generation_max_output_tokens: int = 1000, | |
| default_generation_temperature: float = 0.1, api_key: str = None): | |
| self.url = url or "https://openrouter.ai/api/v1" | |
| self.api_key = api_key or os.getenv("OPENROUTER_API_KEY") | |
| self.model = model | |
| self.generation_model_id = None | |
| self.embedding_model = None | |
| self.embedding_model_id = None | |
| self.embedding_size = None | |
| self.default_input_max_characters = default_input_max_characters | |
| self.default_generation_max_output_tokens = default_generation_max_output_tokens | |
| self.default_generation_temperature = default_generation_temperature | |
| self.logger = logging.getLogger(__name__) | |
| def set_generation_model(self, model_id: str): | |
| if model_id: | |
| self.model = model_id | |
| def set_embedding_model(self, model_id: str, embedding_size: int): | |
| if model_id: | |
| self.embedding_model = model_id | |
| self.embedding_size = embedding_size | |
| self.embedding_model_id = model_id | |
| def process_text(self, text: str): | |
| if not text: | |
| return "" | |
| return str(text).strip() | |
| def generate_text(self, prompt: str, chat_history: list = None, | |
| max_output_tokens: int = None, temperature: float = None): | |
| try: | |
| chat_history = chat_history or [] | |
| clean_prompt = self.process_text(prompt) | |
| messages = [] | |
| for entry in chat_history: | |
| messages.append({ | |
| "role": entry.get("role", "user"), | |
| "content": entry.get("content", "") | |
| }) | |
| messages.append({"role": "user", "content": clean_prompt}) | |
| payload = { | |
| "model": self.model, | |
| "messages": messages, | |
| "max_tokens": int(max_output_tokens or self.default_generation_max_output_tokens), | |
| "temperature": float(temperature or self.default_generation_temperature), | |
| } | |
| url = self.url.rstrip("/") + "/chat/completions" | |
| headers = { | |
| "Authorization": f"Bearer {self.api_key}", | |
| "Content-Type": "application/json", | |
| # Recommended by OpenRouter for usage tracking | |
| "HTTP-Referer": os.getenv("OPENROUTER_SITE_URL", "http://localhost"), | |
| "X-Title": os.getenv("OPENROUTER_APP_NAME", "LLMApp"), | |
| } | |
| resp = requests.post(url, json=payload, headers=headers, timeout=6000) | |
| if resp.status_code != 200: | |
| self.logger.error("OpenRouter generate failed: %s %s", resp.status_code, resp.text) | |
| return None | |
| data = resp.json() | |
| try: | |
| generated_text = data["choices"][0]["message"]["content"].strip() | |
| except (KeyError, IndexError, TypeError): | |
| self.logger.error("Unexpected OpenRouter response structure: %s", data) | |
| return None | |
| if not generated_text: | |
| return None | |
| usage = data.get("usage", {}) | |
| return { | |
| "model": data.get("model"), | |
| "response": generated_text, | |
| "tokens_generated": usage.get("completion_tokens"), | |
| "total_duration_ms": None, | |
| "prompt_eval_tokens": usage.get("prompt_tokens"), | |
| } | |
| except Exception as e: | |
| self.logger.exception("Error in OpenRouterProvider.generate_text: %s", e) | |
| return None | |
| def embed_text(self, text: str, document_type: str = None): | |
| """OpenRouter does not support embeddings natively — returns None.""" | |
| self.logger.warning("OpenRouterProvider does not support embeddings.") | |
| return None | |
| def construct_prompt(self, prompt: str, role: str): | |
| return { | |
| "role": role, | |
| "content": self.process_text(prompt) | |
| } | |
| def embed_text_batch(self, texts: list[str], batch_size: int = 32): | |
| """OpenRouter does not support embeddings natively — returns None.""" | |
| self.logger.warning("OpenRouterProvider does not support embeddings.") | |
| return None | |
| def clean_content(self, text: str) -> str: | |
| text = re.sub(r'\[.*?\]\(.*?\)', '', text) | |
| text = re.sub(r'\[[^\]]*\]', '', text) | |
| text = re.sub(r'\n+', '\n', text).strip() | |
| return text | |
| def web_search(self, query: str): | |
| """ | |
| OpenRouter supports online models (e.g. perplexity/sonar-online) that have | |
| built-in web search. Route the query through one of those models if available, | |
| otherwise fall back to a not-supported notice. | |
| """ | |
| try: | |
| online_model = os.getenv("OPENROUTER_SEARCH_MODEL", "perplexity/sonar-online") | |
| payload = { | |
| "model": online_model, | |
| "messages": [{"role": "user", "content": query}], | |
| "max_tokens": int(self.default_generation_max_output_tokens), | |
| "temperature": float(self.default_generation_temperature), | |
| } | |
| url = self.url.rstrip("/") + "/chat/completions" | |
| headers = { | |
| "Authorization": f"Bearer {self.api_key}", | |
| "Content-Type": "application/json", | |
| "HTTP-Referer": os.getenv("OPENROUTER_SITE_URL", "http://localhost"), | |
| "X-Title": os.getenv("OPENROUTER_APP_NAME", "LLMApp"), | |
| } | |
| resp = requests.post(url, json=payload, headers=headers, timeout=6000) | |
| if not resp or resp.status_code != 200: | |
| return { | |
| "text": "No relevant external results found.", | |
| "references": [] | |
| } | |
| data = resp.json() | |
| combined_text = [] | |
| references = set() | |
| try: | |
| text_content = data["choices"][0]["message"]["content"] | |
| combined_text.append(self.clean_content(text_content)) | |
| except (KeyError, IndexError, TypeError): | |
| pass | |
| # Extract any URLs from the response text | |
| for found_url in re.findall(r"https?://[^\s)]+", "\n".join(combined_text)): | |
| references.add(found_url) | |
| return { | |
| "text": "\n\n".join(combined_text[:3]), | |
| "references": list(references) | |
| } | |
| except Exception as e: | |
| self.logger.error("OpenRouter web search failed: %s", e) | |
| return { | |
| "text": f"OpenRouter search error: {str(e)}", | |
| "references": [] | |
| } | |