lollms-client 0.24.2__py3-none-any.whl → 0.27.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of lollms-client might be problematic. Click here for more details.

@@ -0,0 +1,304 @@
1
+ import os
2
+ from typing import Optional, Callable, List, Union, Dict
3
+
4
+ from lollms_client.lollms_discussion import LollmsDiscussion, LollmsMessage
5
+ from lollms_client.lollms_llm_binding import LollmsLLMBinding
6
+ from lollms_client.lollms_types import MSG_TYPE
7
+ from ascii_colors import ASCIIColors, trace_exception
8
+
9
+ import pipmaster as pm
10
+
11
+ # Ensure the required packages are installed
12
+ pm.ensure_packages(["openai", "pillow", "tiktoken"])
13
+
14
+ import openai
15
+ from PIL import Image, ImageDraw
16
+ import tiktoken
17
+
18
+ BindingName = "OpenRouterBinding"
19
+
20
+ class OpenRouterBinding(LollmsLLMBinding):
21
+ """
22
+ OpenRouter API binding implementation.
23
+
24
+ This binding allows communication with the OpenRouter service, which acts as a
25
+ aggregator for a vast number of AI models from different providers. It uses
26
+ an OpenAI-compatible API structure.
27
+ """
28
+ BASE_URL = "https://openrouter.ai/api/v1"
29
+
30
+ def __init__(self,
31
+ model_name: str = "google/gemini-flash-1.5", # A good, fast default
32
+ open_router_api_key: str = None,
33
+ **kwargs
34
+ ):
35
+ """
36
+ Initialize the OpenRouterBinding.
37
+
38
+ Args:
39
+ model_name (str): The name of the model to use from OpenRouter (e.g., 'anthropic/claude-3-haiku-20240307').
40
+ open_router_api_key (str): The API key for the OpenRouter service.
41
+ """
42
+ super().__init__(binding_name=BindingName)
43
+ self.model_name = model_name
44
+ self.api_key = open_router_api_key or os.getenv("OPENROUTER_API_KEY")
45
+
46
+ if not self.api_key:
47
+ raise ValueError("OpenRouter API key is required. Set it via 'open_router_api_key' or OPENROUTER_API_KEY env var.")
48
+
49
+ try:
50
+ self.client = openai.OpenAI(
51
+ base_url=self.BASE_URL,
52
+ api_key=self.api_key,
53
+ )
54
+ except Exception as e:
55
+ ASCIIColors.error(f"Failed to configure OpenRouter client: {e}")
56
+ self.client = None
57
+ raise ConnectionError(f"Could not configure OpenRouter client: {e}") from e
58
+
59
+ def _construct_parameters(self,
60
+ temperature: float,
61
+ top_p: float,
62
+ n_predict: int,
63
+ seed: Optional[int]) -> Dict[str, any]:
64
+ """Builds a parameters dictionary for the API."""
65
+ params = {}
66
+ if temperature is not None: params['temperature'] = float(temperature)
67
+ if top_p is not None: params['top_p'] = top_p
68
+ if n_predict is not None: params['max_tokens'] = n_predict
69
+ if seed is not None: params['seed'] = seed
70
+ return params
71
+
72
+ def _prepare_messages(self, discussion: LollmsDiscussion, branch_tip_id: Optional[str] = None) -> List[Dict[str, any]]:
73
+ """Prepares the message list for the API from a LollmsDiscussion."""
74
+ history = []
75
+ if discussion.system_prompt:
76
+ history.append({"role": "system", "content": discussion.system_prompt})
77
+
78
+ for msg in discussion.get_messages(branch_tip_id):
79
+ role = 'user' if msg.sender_type == "user" else 'assistant'
80
+ # Note: Vision support depends on the specific model being called via OpenRouter.
81
+ # We will not implement it in this generic binding to avoid complexity,
82
+ # as different models might expect different formats.
83
+ if msg.content:
84
+ history.append({'role': role, 'content': msg.content})
85
+ return history
86
+
87
+ def generate_text(self, prompt: str, **kwargs) -> Union[str, dict]:
88
+ """
89
+ Generate text using OpenRouter. This is a wrapper around the chat method.
90
+ """
91
+ temp_discussion = LollmsDiscussion.from_messages([
92
+ LollmsMessage.new_message(sender_type="user", content=prompt)
93
+ ])
94
+ if kwargs.get("system_prompt"):
95
+ temp_discussion.system_prompt = kwargs.get("system_prompt")
96
+
97
+ return self.chat(temp_discussion, **kwargs)
98
+
99
+ def chat(self,
100
+ discussion: LollmsDiscussion,
101
+ branch_tip_id: Optional[str] = None,
102
+ n_predict: Optional[int] = 2048,
103
+ stream: Optional[bool] = False,
104
+ temperature: float = 0.7,
105
+ top_p: float = 0.9,
106
+ seed: Optional[int] = None,
107
+ streaming_callback: Optional[Callable[[str, MSG_TYPE], None]] = None,
108
+ **kwargs
109
+ ) -> Union[str, dict]:
110
+ """
111
+ Conduct a chat session with a model via OpenRouter.
112
+ """
113
+ if not self.client:
114
+ return {"status": "error", "message": "OpenRouter client not initialized."}
115
+
116
+ messages = self._prepare_messages(discussion, branch_tip_id)
117
+ api_params = self._construct_parameters(temperature, top_p, n_predict, seed)
118
+ full_response_text = ""
119
+
120
+ try:
121
+ response = self.client.chat.completions.create(
122
+ model=self.model_name,
123
+ messages=messages,
124
+ stream=stream,
125
+ **api_params
126
+ )
127
+
128
+ if stream:
129
+ for chunk in response:
130
+ delta = chunk.choices[0].delta.content
131
+ if delta:
132
+ full_response_text += delta
133
+ if streaming_callback:
134
+ if not streaming_callback(delta, MSG_TYPE.MSG_TYPE_CHUNK):
135
+ break
136
+ return full_response_text
137
+ else:
138
+ return response.choices[0].message.content
139
+
140
+ except Exception as ex:
141
+ error_message = f"An unexpected error occurred with OpenRouter API: {str(ex)}"
142
+ trace_exception(ex)
143
+ return {"status": "error", "message": error_message}
144
+
145
+ def tokenize(self, text: str) -> list:
146
+ """Tokenize text using tiktoken as a general-purpose fallback."""
147
+ try:
148
+ encoding = tiktoken.get_encoding("cl100k_base")
149
+ return encoding.encode(text)
150
+ except Exception:
151
+ return list(text.encode('utf-8'))
152
+
153
+ def detokenize(self, tokens: list) -> str:
154
+ """Detokenize tokens using tiktoken."""
155
+ try:
156
+ encoding = tiktoken.get_encoding("cl100k_base")
157
+ return encoding.decode(tokens)
158
+ except Exception:
159
+ return bytes(tokens).decode('utf-8', errors='ignore')
160
+
161
+ def count_tokens(self, text: str) -> int:
162
+ """Count tokens in a text using the fallback tokenizer."""
163
+ return len(self.tokenize(text))
164
+
165
+ def embed(self, text: str, **kwargs) -> List[float]:
166
+ """
167
+ Get embeddings for the input text using an OpenRouter embedding model.
168
+ """
169
+ if not self.client:
170
+ raise Exception("OpenRouter client not initialized.")
171
+
172
+ # User must specify an embedding model, e.g., 'text-embedding-ada-002'
173
+ embedding_model = kwargs.get("model")
174
+ if not embedding_model:
175
+ raise ValueError("An embedding model name must be provided via the 'model' kwarg for the embed method.")
176
+
177
+ try:
178
+ # The client is already configured for OpenRouter's base URL
179
+ response = self.client.embeddings.create(
180
+ model=embedding_model,
181
+ input=text
182
+ )
183
+ return response.data[0].embedding
184
+ except Exception as ex:
185
+ trace_exception(ex)
186
+ raise Exception(f"OpenRouter embedding failed: {str(ex)}") from ex
187
+
188
+ def get_model_info(self) -> dict:
189
+ """Return information about the current OpenRouter setup."""
190
+ return {
191
+ "name": self.binding_name,
192
+ "version": openai.__version__,
193
+ "host_address": self.BASE_URL,
194
+ "model_name": self.model_name,
195
+ "supports_structured_output": False,
196
+ "supports_vision": "Depends on the specific model selected. This generic binding does not support vision.",
197
+ }
198
+
199
+ def listModels(self) -> List[Dict[str, str]]:
200
+ """Lists available models from the OpenRouter service."""
201
+ if not self.client:
202
+ ASCIIColors.error("OpenRouter client not initialized. Cannot list models.")
203
+ return []
204
+ try:
205
+ ASCIIColors.debug("Listing OpenRouter models...")
206
+ models = self.client.models.list()
207
+ model_info_list = []
208
+ for m in models.data:
209
+ model_info_list.append({
210
+ 'model_name': m.id,
211
+ 'display_name': m.name if hasattr(m, 'name') else m.id,
212
+ 'description': m.description if hasattr(m, 'description') else "No description available.",
213
+ 'owned_by': m.id.split('/')[0] # Heuristic to get the provider
214
+ })
215
+ return model_info_list
216
+ except Exception as ex:
217
+ trace_exception(ex)
218
+ return []
219
+
220
+ def load_model(self, model_name: str) -> bool:
221
+ """Sets the model name for subsequent operations."""
222
+ self.model_name = model_name
223
+ ASCIIColors.info(f"OpenRouter model set to: {model_name}. It will be used on the next API call.")
224
+ return True
225
+
226
+ if __name__ == '__main__':
227
+ # Environment variable to set for testing:
228
+ # OPENROUTER_API_KEY: Your OpenRouter API key (starts with sk-or-...)
229
+
230
+ if "OPENROUTER_API_KEY" not in os.environ:
231
+ ASCIIColors.red("Error: OPENROUTER_API_KEY environment variable not set.")
232
+ print("Please get your key from https://openrouter.ai/keys and set it.")
233
+ exit(1)
234
+
235
+ ASCIIColors.yellow("--- Testing OpenRouterBinding ---")
236
+
237
+ try:
238
+ # --- Initialization ---
239
+ ASCIIColors.cyan("\n--- Initializing Binding ---")
240
+ # Initialize with a fast, cheap, and well-known model
241
+ binding = OpenRouterBinding(model_name="mistralai/mistral-7b-instruct")
242
+ ASCIIColors.green("Binding initialized successfully.")
243
+
244
+ # --- List Models ---
245
+ ASCIIColors.cyan("\n--- Listing Models ---")
246
+ models = binding.listModels()
247
+ if models:
248
+ ASCIIColors.green(f"Successfully fetched {len(models)} models from OpenRouter.")
249
+ ASCIIColors.info("Sample of available models:")
250
+ # Print a few examples from different providers
251
+ providers_seen = set()
252
+ count = 0
253
+ for m in models:
254
+ provider = m['owned_by']
255
+ if provider not in providers_seen:
256
+ print(f"- {m['model_name']}")
257
+ providers_seen.add(provider)
258
+ count += 1
259
+ if count >= 5:
260
+ break
261
+ else:
262
+ ASCIIColors.warning("No models found or failed to list models.")
263
+
264
+ # --- Text Generation (Testing with a Claude model) ---
265
+ ASCIIColors.cyan("\n--- Text Generation (Claude via OpenRouter) ---")
266
+ binding.load_model("anthropic/claude-3-haiku-20240307")
267
+ prompt_text = "Why is Claude Haiku a good choice for fast-paced chat applications?"
268
+ generated_text = binding.generate_text(prompt_text, n_predict=100, stream=False)
269
+ if isinstance(generated_text, str):
270
+ ASCIIColors.green(f"Generated text:\n{generated_text}")
271
+ else:
272
+ ASCIIColors.error(f"Generation failed: {generated_text}")
273
+
274
+ # --- Text Generation (Streaming with a Groq model) ---
275
+ ASCIIColors.cyan("\n--- Text Generation (Llama3 on Groq via OpenRouter) ---")
276
+ binding.load_model("meta-llama/llama-3-8b-instruct:free") # Use the free tier on OpenRouter
277
+ full_streamed_text = ""
278
+ def stream_callback(chunk: str, msg_type: int):
279
+ nonlocal full_streamed_text
280
+ ASCIIColors.green(chunk, end="", flush=True)
281
+ full_streamed_text += chunk
282
+ return True
283
+
284
+ stream_prompt = "Write a very short, 3-line poem about the speed of Groq."
285
+ result = binding.generate_text(stream_prompt, n_predict=50, stream=True, streaming_callback=stream_callback)
286
+ print("\n--- End of Stream ---")
287
+ ASCIIColors.green(f"Full streamed text (for verification): {result}")
288
+
289
+ # --- Embeddings Test ---
290
+ ASCIIColors.cyan("\n--- Embeddings (OpenAI model via OpenRouter) ---")
291
+ try:
292
+ embedding_model = "openai/text-embedding-ada-002"
293
+ embedding_text = "OpenRouter simplifies everything."
294
+ embedding_vector = binding.embed(embedding_text, model=embedding_model)
295
+ ASCIIColors.green(f"Embedding for '{embedding_text}' (first 5 dims): {embedding_vector[:5]}...")
296
+ ASCIIColors.info(f"Embedding vector dimension: {len(embedding_vector)}")
297
+ except Exception as e:
298
+ ASCIIColors.error(f"Embedding test failed: {e}")
299
+
300
+ except Exception as e:
301
+ ASCIIColors.error(f"An error occurred during testing: {e}")
302
+ trace_exception(e)
303
+
304
+ ASCIIColors.yellow("\nOpenRouterBinding test finished.")
@@ -30,7 +30,8 @@ class OpenAIBinding(LollmsLLMBinding):
30
30
  model_name: str = "",
31
31
  service_key: str = None,
32
32
  verify_ssl_certificate: bool = True,
33
- default_completion_format: ELF_COMPLETION_FORMAT = ELF_COMPLETION_FORMAT.Chat):
33
+ default_completion_format: ELF_COMPLETION_FORMAT = ELF_COMPLETION_FORMAT.Chat,
34
+ **kwargs):
34
35
  """
35
36
  Initialize the OpenAI binding.
36
37
 
@@ -52,7 +53,7 @@ class OpenAIBinding(LollmsLLMBinding):
52
53
 
53
54
  if not self.service_key:
54
55
  self.service_key = os.getenv("OPENAI_API_KEY", self.service_key)
55
- self.client = openai.OpenAI(api_key=self.service_key, base_url=host_address)
56
+ self.client = openai.OpenAI(api_key=self.service_key, base_url=None if host_address is None else host_address if len(host_address)>0 else None)
56
57
  self.completion_format = ELF_COMPLETION_FORMAT.Chat
57
58
 
58
59
 
@@ -103,15 +104,15 @@ class OpenAIBinding(LollmsLLMBinding):
103
104
  """
104
105
  count = 0
105
106
  output = ""
107
+ messages = [
108
+ {
109
+ "role": "system",
110
+ "content": system_prompt or "You are a helpful assistant.",
111
+ }
112
+ ]
106
113
 
107
114
  # Prepare messages based on whether images are provided
108
115
  if images:
109
- messages = [
110
- {
111
- "role": "system",
112
- "content": system_prompt,
113
- }
114
- ]
115
116
  if split:
116
117
  messages += self.split_discussion(prompt,user_keyword=user_keyword, ai_keyword=ai_keyword)
117
118
  if images:
@@ -150,7 +151,27 @@ class OpenAIBinding(LollmsLLMBinding):
150
151
  )
151
152
 
152
153
  else:
153
- messages = [{"role": "user", "content": prompt}]
154
+
155
+ if split:
156
+ messages += self.split_discussion(prompt,user_keyword=user_keyword, ai_keyword=ai_keyword)
157
+ if images:
158
+ messages[-1]["content"] = [
159
+ {
160
+ "type": "text",
161
+ "text": messages[-1]["content"]
162
+ }
163
+ ]
164
+ else:
165
+ messages.append({
166
+ 'role': 'user',
167
+ 'content': [
168
+ {
169
+ "type": "text",
170
+ "text": prompt
171
+ }
172
+ ]
173
+ }
174
+ )
154
175
 
155
176
  # Generate text using the OpenAI API
156
177
  if self.completion_format == ELF_COMPLETION_FORMAT.Chat: