File size: 6,955 Bytes
1bc3f18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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": []
            }