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.
- lollms_client/__init__.py +3 -2
- lollms_client/llm_bindings/azure_openai/__init__.py +364 -0
- lollms_client/llm_bindings/claude/__init__.py +549 -0
- lollms_client/llm_bindings/gemini/__init__.py +501 -0
- lollms_client/llm_bindings/grok/__init__.py +536 -0
- lollms_client/llm_bindings/groq/__init__.py +292 -0
- lollms_client/llm_bindings/hugging_face_inference_api/__init__.py +307 -0
- lollms_client/llm_bindings/litellm/__init__.py +201 -0
- lollms_client/llm_bindings/lollms/__init__.py +2 -0
- lollms_client/llm_bindings/mistral/__init__.py +298 -0
- lollms_client/llm_bindings/open_router/__init__.py +304 -0
- lollms_client/llm_bindings/openai/__init__.py +30 -9
- lollms_client/lollms_core.py +338 -162
- lollms_client/lollms_discussion.py +135 -37
- lollms_client/lollms_llm_binding.py +4 -0
- lollms_client/lollms_types.py +9 -1
- lollms_client/lollms_utilities.py +68 -0
- lollms_client/mcp_bindings/remote_mcp/__init__.py +82 -4
- lollms_client-0.27.0.dist-info/METADATA +604 -0
- {lollms_client-0.24.2.dist-info → lollms_client-0.27.0.dist-info}/RECORD +23 -14
- lollms_client-0.24.2.dist-info/METADATA +0 -239
- {lollms_client-0.24.2.dist-info → lollms_client-0.27.0.dist-info}/WHEEL +0 -0
- {lollms_client-0.24.2.dist-info → lollms_client-0.27.0.dist-info}/licenses/LICENSE +0 -0
- {lollms_client-0.24.2.dist-info → lollms_client-0.27.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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:
|