llms-py 3.0.1__py3-none-any.whl → 3.0.2__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.
- llms/{extensions/app/db_manager.py → db.py} +170 -15
- llms/extensions/app/__init__.py +95 -29
- llms/extensions/app/db.py +16 -124
- llms/extensions/app/ui/threadStore.mjs +20 -2
- llms/extensions/core_tools/__init__.py +37 -0
- llms/extensions/gallery/__init__.py +15 -13
- llms/extensions/gallery/db.py +117 -172
- llms/extensions/gallery/ui/index.mjs +1 -1
- llms/extensions/providers/__init__.py +3 -1
- llms/extensions/providers/anthropic.py +7 -3
- llms/extensions/providers/cerebras.py +37 -0
- llms/extensions/providers/chutes.py +1 -1
- llms/extensions/providers/google.py +131 -28
- llms/extensions/providers/nvidia.py +2 -2
- llms/extensions/providers/openai.py +2 -2
- llms/extensions/providers/openrouter.py +4 -2
- llms/llms.json +3 -0
- llms/main.py +81 -34
- llms/providers.json +1 -1
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +96 -3
- llms/ui/ctx.mjs +21 -0
- llms/ui/index.mjs +2 -0
- llms/ui/modules/chat/ChatBody.mjs +1 -0
- llms/ui/modules/chat/index.mjs +19 -1
- llms/ui/modules/icons.mjs +46 -0
- llms/ui/modules/layout.mjs +28 -0
- llms/ui/modules/model-selector.mjs +0 -40
- llms/ui/utils.mjs +9 -1
- {llms_py-3.0.1.dist-info → llms_py-3.0.2.dist-info}/METADATA +1 -1
- {llms_py-3.0.1.dist-info → llms_py-3.0.2.dist-info}/RECORD +35 -33
- {llms_py-3.0.1.dist-info → llms_py-3.0.2.dist-info}/WHEEL +0 -0
- {llms_py-3.0.1.dist-info → llms_py-3.0.2.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.1.dist-info → llms_py-3.0.2.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.1.dist-info → llms_py-3.0.2.dist-info}/top_level.txt +0 -0
|
@@ -79,16 +79,61 @@ def install_google(ctx):
|
|
|
79
79
|
if "Authorization" in self.headers:
|
|
80
80
|
del self.headers["Authorization"]
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
def provider_model(self, model):
|
|
83
|
+
if model.lower().startswith("gemini-"):
|
|
84
|
+
return model
|
|
85
|
+
return super().provider_model(model)
|
|
86
|
+
|
|
87
|
+
def model_info(self, model):
|
|
88
|
+
info = super().model_info(model)
|
|
89
|
+
if info:
|
|
90
|
+
return info
|
|
91
|
+
if model.lower().startswith("gemini-"):
|
|
92
|
+
return {
|
|
93
|
+
"id": model,
|
|
94
|
+
"name": model,
|
|
95
|
+
"cost": {"input": 0, "output": 0},
|
|
96
|
+
}
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
async def chat(self, chat, context=None):
|
|
83
100
|
chat["model"] = self.provider_model(chat["model"]) or chat["model"]
|
|
101
|
+
model_info = (context.get("modelInfo") if context is not None else None) or self.model_info(chat["model"])
|
|
84
102
|
|
|
85
103
|
chat = await self.process_chat(chat)
|
|
86
104
|
generation_config = {}
|
|
105
|
+
tools = None
|
|
106
|
+
supports_tool_calls = model_info.get("tool_call", False)
|
|
107
|
+
|
|
108
|
+
if "tools" in chat and supports_tool_calls:
|
|
109
|
+
function_declarations = []
|
|
110
|
+
gemini_tools = {}
|
|
111
|
+
|
|
112
|
+
for tool in chat["tools"]:
|
|
113
|
+
if tool["type"] == "function":
|
|
114
|
+
f = tool["function"]
|
|
115
|
+
function_declarations.append(
|
|
116
|
+
{
|
|
117
|
+
"name": f["name"],
|
|
118
|
+
"description": f.get("description"),
|
|
119
|
+
"parameters": f.get("parameters"),
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
elif tool["type"] == "file_search":
|
|
123
|
+
gemini_tools["file_search"] = tool["file_search"]
|
|
124
|
+
|
|
125
|
+
if function_declarations:
|
|
126
|
+
gemini_tools["function_declarations"] = function_declarations
|
|
127
|
+
|
|
128
|
+
tools = [gemini_tools] if gemini_tools else None
|
|
87
129
|
|
|
88
130
|
# Filter out system messages and convert to proper Gemini format
|
|
89
131
|
contents = []
|
|
90
132
|
system_prompt = None
|
|
91
133
|
|
|
134
|
+
# Track tool call IDs to names for response mapping
|
|
135
|
+
tool_id_map = {}
|
|
136
|
+
|
|
92
137
|
async with aiohttp.ClientSession() as session:
|
|
93
138
|
for message in chat["messages"]:
|
|
94
139
|
if message["role"] == "system":
|
|
@@ -101,8 +146,55 @@ def install_google(ctx):
|
|
|
101
146
|
elif isinstance(content, str):
|
|
102
147
|
system_prompt = content
|
|
103
148
|
elif "content" in message:
|
|
149
|
+
role = "user"
|
|
150
|
+
if "role" in message:
|
|
151
|
+
if message["role"] == "user":
|
|
152
|
+
role = "user"
|
|
153
|
+
elif message["role"] == "assistant":
|
|
154
|
+
role = "model"
|
|
155
|
+
elif message["role"] == "tool":
|
|
156
|
+
role = "function"
|
|
157
|
+
|
|
158
|
+
parts = []
|
|
159
|
+
|
|
160
|
+
# Handle tool calls in assistant messages
|
|
161
|
+
if message.get("role") == "assistant" and "tool_calls" in message:
|
|
162
|
+
for tool_call in message["tool_calls"]:
|
|
163
|
+
tool_id_map[tool_call["id"]] = tool_call["function"]["name"]
|
|
164
|
+
parts.append(
|
|
165
|
+
{
|
|
166
|
+
"functionCall": {
|
|
167
|
+
"name": tool_call["function"]["name"],
|
|
168
|
+
"args": json.loads(tool_call["function"]["arguments"]),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Handle tool responses from user
|
|
174
|
+
if message.get("role") == "tool":
|
|
175
|
+
# Gemini expects function response in 'functionResponse' part
|
|
176
|
+
# We need to find the name associated with this tool_call_id
|
|
177
|
+
tool_call_id = message.get("tool_call_id")
|
|
178
|
+
name = tool_id_map.get(tool_call_id)
|
|
179
|
+
# If we can't find the name (maybe from previous turn not in history or restart),
|
|
180
|
+
# we might have an issue. But let's try to proceed.
|
|
181
|
+
# Fallback: if we can't find the name, skip or try to infer?
|
|
182
|
+
# Gemini strict validation requires the name.
|
|
183
|
+
if name:
|
|
184
|
+
# content is the string response
|
|
185
|
+
# Some implementations pass the content directly.
|
|
186
|
+
# Google docs say: response: { "name": "...", "content": { ... } }
|
|
187
|
+
# Actually "response" field in functionResponse is a Struct/Map.
|
|
188
|
+
parts.append(
|
|
189
|
+
{
|
|
190
|
+
"functionResponse": {
|
|
191
|
+
"name": name,
|
|
192
|
+
"response": {"name": name, "content": message["content"]},
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
|
|
104
197
|
if isinstance(message["content"], list):
|
|
105
|
-
parts = []
|
|
106
198
|
for item in message["content"]:
|
|
107
199
|
if "type" in item:
|
|
108
200
|
if item["type"] == "image_url" and "image_url" in item:
|
|
@@ -142,23 +234,14 @@ def install_google(ctx):
|
|
|
142
234
|
if "text" in item:
|
|
143
235
|
text = item["text"]
|
|
144
236
|
parts.append({"text": text})
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if "role" in message and message["role"] == "user"
|
|
150
|
-
else "model",
|
|
151
|
-
"parts": parts,
|
|
152
|
-
}
|
|
153
|
-
)
|
|
154
|
-
else:
|
|
155
|
-
content = message["content"]
|
|
237
|
+
elif message["content"]: # String content
|
|
238
|
+
parts.append({"text": message["content"]})
|
|
239
|
+
|
|
240
|
+
if len(parts) > 0:
|
|
156
241
|
contents.append(
|
|
157
242
|
{
|
|
158
|
-
"role":
|
|
159
|
-
|
|
160
|
-
else "model",
|
|
161
|
-
"parts": [{"text": content}],
|
|
243
|
+
"role": role,
|
|
244
|
+
"parts": parts,
|
|
162
245
|
}
|
|
163
246
|
)
|
|
164
247
|
|
|
@@ -166,6 +249,9 @@ def install_google(ctx):
|
|
|
166
249
|
"contents": contents,
|
|
167
250
|
}
|
|
168
251
|
|
|
252
|
+
if tools:
|
|
253
|
+
gemini_chat["tools"] = tools
|
|
254
|
+
|
|
169
255
|
if self.safety_settings:
|
|
170
256
|
gemini_chat["safetySettings"] = self.safety_settings
|
|
171
257
|
|
|
@@ -192,18 +278,12 @@ def install_google(ctx):
|
|
|
192
278
|
if len(generation_config) > 0:
|
|
193
279
|
gemini_chat["generationConfig"] = generation_config
|
|
194
280
|
|
|
195
|
-
if "tools" in chat:
|
|
196
|
-
# gemini_chat["tools"] = chat["tools"]
|
|
197
|
-
ctx.log("Error: tools not supported in Gemini")
|
|
198
|
-
elif self.tools:
|
|
199
|
-
# gemini_chat["tools"] = self.tools.copy()
|
|
200
|
-
ctx.log("Error: tools not supported in Gemini")
|
|
201
|
-
|
|
202
281
|
if "modalities" in chat:
|
|
203
282
|
generation_config["responseModalities"] = [modality.upper() for modality in chat["modalities"]]
|
|
204
283
|
if "image" in chat["modalities"] and "image_config" in chat:
|
|
205
284
|
# delete thinkingConfig
|
|
206
|
-
|
|
285
|
+
if "thinkingConfig" in generation_config:
|
|
286
|
+
del generation_config["thinkingConfig"]
|
|
207
287
|
config_map = {
|
|
208
288
|
"aspect_ratio": "aspectRatio",
|
|
209
289
|
"image_size": "imageSize",
|
|
@@ -212,11 +292,16 @@ def install_google(ctx):
|
|
|
212
292
|
config_map[k]: v for k, v in chat["image_config"].items() if k in config_map
|
|
213
293
|
}
|
|
214
294
|
if "audio" in chat["modalities"] and self.speech_config:
|
|
215
|
-
|
|
295
|
+
if "thinkingConfig" in generation_config:
|
|
296
|
+
del generation_config["thinkingConfig"]
|
|
216
297
|
generation_config["speechConfig"] = self.speech_config.copy()
|
|
217
298
|
# Currently Google Audio Models only accept AUDIO
|
|
218
299
|
generation_config["responseModalities"] = ["AUDIO"]
|
|
219
300
|
|
|
301
|
+
# Ensure generationConfig is set if we added anything to it
|
|
302
|
+
if len(generation_config) > 0:
|
|
303
|
+
gemini_chat["generationConfig"] = generation_config
|
|
304
|
+
|
|
220
305
|
started_at = int(time.time() * 1000)
|
|
221
306
|
gemini_chat_url = f"https://generativelanguage.googleapis.com/v1beta/models/{chat['model']}:generateContent?key={self.api_key}"
|
|
222
307
|
|
|
@@ -237,6 +322,8 @@ def install_google(ctx):
|
|
|
237
322
|
timeout=aiohttp.ClientTimeout(total=120),
|
|
238
323
|
) as res:
|
|
239
324
|
obj = await self.response_json(res)
|
|
325
|
+
if context is not None:
|
|
326
|
+
context["providerResponse"] = obj
|
|
240
327
|
except Exception as e:
|
|
241
328
|
ctx.log(f"Error: {res.status} {res.reason}: {e}")
|
|
242
329
|
text = await res.text()
|
|
@@ -271,7 +358,7 @@ def install_google(ctx):
|
|
|
271
358
|
"model": obj.get("modelVersion", chat["model"]),
|
|
272
359
|
}
|
|
273
360
|
choices = []
|
|
274
|
-
for i, candidate in enumerate(obj
|
|
361
|
+
for i, candidate in enumerate(obj.get("candidates", [])):
|
|
275
362
|
role = "assistant"
|
|
276
363
|
if "content" in candidate and "role" in candidate["content"]:
|
|
277
364
|
role = "assistant" if candidate["content"]["role"] == "model" else candidate["content"]["role"]
|
|
@@ -281,6 +368,8 @@ def install_google(ctx):
|
|
|
281
368
|
reasoning = ""
|
|
282
369
|
images = []
|
|
283
370
|
audios = []
|
|
371
|
+
tool_calls = []
|
|
372
|
+
|
|
284
373
|
if "content" in candidate and "parts" in candidate["content"]:
|
|
285
374
|
text_parts = []
|
|
286
375
|
reasoning_parts = []
|
|
@@ -290,6 +379,16 @@ def install_google(ctx):
|
|
|
290
379
|
reasoning_parts.append(part["text"])
|
|
291
380
|
else:
|
|
292
381
|
text_parts.append(part["text"])
|
|
382
|
+
if "functionCall" in part:
|
|
383
|
+
fc = part["functionCall"]
|
|
384
|
+
tool_calls.append(
|
|
385
|
+
{
|
|
386
|
+
"id": f"call_{len(tool_calls)}_{int(time.time())}", # Gemini doesn't return ID, generate one
|
|
387
|
+
"type": "function",
|
|
388
|
+
"function": {"name": fc["name"], "arguments": json.dumps(fc["args"])},
|
|
389
|
+
}
|
|
390
|
+
)
|
|
391
|
+
|
|
293
392
|
if "inlineData" in part:
|
|
294
393
|
inline_data = part["inlineData"]
|
|
295
394
|
mime_type = inline_data.get("mimeType", "image/png")
|
|
@@ -354,7 +453,7 @@ def install_google(ctx):
|
|
|
354
453
|
"finish_reason": candidate.get("finishReason", "stop"),
|
|
355
454
|
"message": {
|
|
356
455
|
"role": role,
|
|
357
|
-
"content": content,
|
|
456
|
+
"content": content if content else None,
|
|
358
457
|
},
|
|
359
458
|
}
|
|
360
459
|
if reasoning:
|
|
@@ -363,6 +462,10 @@ def install_google(ctx):
|
|
|
363
462
|
choice["message"]["images"] = images
|
|
364
463
|
if len(audios) > 0:
|
|
365
464
|
choice["message"]["audios"] = audios
|
|
465
|
+
if len(tool_calls) > 0:
|
|
466
|
+
choice["message"]["tool_calls"] = tool_calls
|
|
467
|
+
# If we have tool calls, content can be null but message should probably exist
|
|
468
|
+
|
|
366
469
|
choices.append(choice)
|
|
367
470
|
response["choices"] = choices
|
|
368
471
|
if "usageMetadata" in obj:
|
|
@@ -54,7 +54,7 @@ def install_nvidia(ctx):
|
|
|
54
54
|
}
|
|
55
55
|
raise Exception("No artifacts in response")
|
|
56
56
|
|
|
57
|
-
async def chat(self, chat, provider=None):
|
|
57
|
+
async def chat(self, chat, provider=None, context=None):
|
|
58
58
|
headers = self.get_headers(provider, chat)
|
|
59
59
|
if provider is not None:
|
|
60
60
|
chat["model"] = provider.provider_model(chat["model"]) or chat["model"]
|
|
@@ -100,6 +100,6 @@ def install_nvidia(ctx):
|
|
|
100
100
|
data=json.dumps(gen_request),
|
|
101
101
|
timeout=aiohttp.ClientTimeout(total=120),
|
|
102
102
|
) as response:
|
|
103
|
-
return self.to_response(await self.response_json(response), chat, started_at)
|
|
103
|
+
return self.to_response(await self.response_json(response), chat, started_at, context=context)
|
|
104
104
|
|
|
105
105
|
ctx.add_provider(NvidiaGenAi)
|
|
@@ -113,7 +113,7 @@ def install_openai(ctx):
|
|
|
113
113
|
ctx.log(json.dumps(response, indent=2))
|
|
114
114
|
raise Exception("No 'data' field in response.")
|
|
115
115
|
|
|
116
|
-
async def chat(self, chat, provider=None):
|
|
116
|
+
async def chat(self, chat, provider=None, context=None):
|
|
117
117
|
headers = self.get_headers(provider, chat)
|
|
118
118
|
|
|
119
119
|
if chat["model"] in self.map_image_models:
|
|
@@ -145,7 +145,7 @@ def install_openai(ctx):
|
|
|
145
145
|
text = await response.text()
|
|
146
146
|
ctx.log(text[:1024] + (len(text) > 1024 and "..." or ""))
|
|
147
147
|
if response.status < 300:
|
|
148
|
-
return ctx.log_json(await self.to_response(json.loads(text), chat, started_at))
|
|
148
|
+
return ctx.log_json(await self.to_response(json.loads(text), chat, started_at, context=context))
|
|
149
149
|
else:
|
|
150
150
|
raise Exception(f"Failed to generate image {response.status}")
|
|
151
151
|
|
|
@@ -39,7 +39,7 @@ def install_openrouter(ctx):
|
|
|
39
39
|
|
|
40
40
|
return response
|
|
41
41
|
|
|
42
|
-
async def chat(self, chat, provider=None):
|
|
42
|
+
async def chat(self, chat, provider=None, context=None):
|
|
43
43
|
headers = self.get_headers(provider, chat)
|
|
44
44
|
if provider is not None:
|
|
45
45
|
chat["model"] = provider.provider_model(chat["model"]) or chat["model"]
|
|
@@ -67,6 +67,8 @@ def install_openrouter(ctx):
|
|
|
67
67
|
) as response:
|
|
68
68
|
if metadata:
|
|
69
69
|
chat["metadata"] = metadata
|
|
70
|
-
return ctx.log_json(
|
|
70
|
+
return ctx.log_json(
|
|
71
|
+
self.to_response(await self.response_json(response), chat, started_at, context=context)
|
|
72
|
+
)
|
|
71
73
|
|
|
72
74
|
ctx.add_provider(OpenRouterGenerator)
|
llms/llms.json
CHANGED
llms/main.py
CHANGED
|
@@ -41,7 +41,7 @@ try:
|
|
|
41
41
|
except ImportError:
|
|
42
42
|
HAS_PIL = False
|
|
43
43
|
|
|
44
|
-
VERSION = "3.0.
|
|
44
|
+
VERSION = "3.0.2"
|
|
45
45
|
_ROOT = None
|
|
46
46
|
DEBUG = os.getenv("DEBUG") == "1"
|
|
47
47
|
MOCK = os.getenv("MOCK") == "1"
|
|
@@ -375,7 +375,7 @@ async def process_chat(chat, provider_id=None):
|
|
|
375
375
|
if "stream" not in chat:
|
|
376
376
|
chat["stream"] = False
|
|
377
377
|
# Some providers don't support empty tools
|
|
378
|
-
if "tools" in chat and len(chat["tools"]) == 0:
|
|
378
|
+
if "tools" in chat and (chat["tools"] is None or len(chat["tools"]) == 0):
|
|
379
379
|
del chat["tools"]
|
|
380
380
|
if "messages" not in chat:
|
|
381
381
|
return chat
|
|
@@ -618,7 +618,7 @@ def save_bytes_to_cache(base64_data, filename, file_info, ignore_info=False):
|
|
|
618
618
|
_dbg(f"Cached bytes exists: {relative_path}")
|
|
619
619
|
if ignore_info:
|
|
620
620
|
return url, None
|
|
621
|
-
return url,
|
|
621
|
+
return url, json_from_file(info_path)
|
|
622
622
|
|
|
623
623
|
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
624
624
|
|
|
@@ -665,7 +665,7 @@ def save_image_to_cache(base64_data, filename, image_info, ignore_info=False):
|
|
|
665
665
|
_dbg(f"Saved image exists: {relative_path}")
|
|
666
666
|
if ignore_info:
|
|
667
667
|
return url, None
|
|
668
|
-
return url,
|
|
668
|
+
return url, json_from_file(info_path)
|
|
669
669
|
|
|
670
670
|
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
671
671
|
|
|
@@ -870,7 +870,7 @@ class GeneratorBase:
|
|
|
870
870
|
def to_response(self, response, chat, started_at):
|
|
871
871
|
raise NotImplementedError
|
|
872
872
|
|
|
873
|
-
async def chat(self, chat, provider=None):
|
|
873
|
+
async def chat(self, chat, provider=None, context=None):
|
|
874
874
|
return {
|
|
875
875
|
"choices": [
|
|
876
876
|
{
|
|
@@ -1030,7 +1030,7 @@ class OpenAiCompatible:
|
|
|
1030
1030
|
def response_json(self, response):
|
|
1031
1031
|
return response_json(response)
|
|
1032
1032
|
|
|
1033
|
-
def to_response(self, response, chat, started_at):
|
|
1033
|
+
def to_response(self, response, chat, started_at, context=None):
|
|
1034
1034
|
if "metadata" not in response:
|
|
1035
1035
|
response["metadata"] = {}
|
|
1036
1036
|
response["metadata"]["duration"] = int((time.time() - started_at) * 1000)
|
|
@@ -1038,6 +1038,8 @@ class OpenAiCompatible:
|
|
|
1038
1038
|
pricing = self.model_cost(chat["model"])
|
|
1039
1039
|
if pricing and "input" in pricing and "output" in pricing:
|
|
1040
1040
|
response["metadata"]["pricing"] = f"{pricing['input']}/{pricing['output']}"
|
|
1041
|
+
if context is not None:
|
|
1042
|
+
context["providerResponse"] = response
|
|
1041
1043
|
return response
|
|
1042
1044
|
|
|
1043
1045
|
def chat_summary(self, chat):
|
|
@@ -1046,17 +1048,18 @@ class OpenAiCompatible:
|
|
|
1046
1048
|
def process_chat(self, chat, provider_id=None):
|
|
1047
1049
|
return process_chat(chat, provider_id)
|
|
1048
1050
|
|
|
1049
|
-
async def chat(self, chat):
|
|
1051
|
+
async def chat(self, chat, context=None):
|
|
1050
1052
|
chat["model"] = self.provider_model(chat["model"]) or chat["model"]
|
|
1051
1053
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
+
modalities = chat.get("modalities") or []
|
|
1055
|
+
if len(modalities) > 0:
|
|
1056
|
+
for modality in modalities:
|
|
1054
1057
|
# use default implementation for text modalities
|
|
1055
1058
|
if modality == "text":
|
|
1056
1059
|
continue
|
|
1057
1060
|
modality_provider = self.modalities.get(modality)
|
|
1058
1061
|
if modality_provider:
|
|
1059
|
-
return await modality_provider.chat(chat, self)
|
|
1062
|
+
return await modality_provider.chat(chat, self, context=context)
|
|
1060
1063
|
else:
|
|
1061
1064
|
raise Exception(f"Provider {self.name} does not support '{modality}' modality")
|
|
1062
1065
|
|
|
@@ -1110,7 +1113,7 @@ class OpenAiCompatible:
|
|
|
1110
1113
|
self.chat_url, headers=self.headers, data=json.dumps(chat), timeout=aiohttp.ClientTimeout(total=120)
|
|
1111
1114
|
) as response:
|
|
1112
1115
|
chat["metadata"] = metadata
|
|
1113
|
-
return self.to_response(await response_json(response), chat, started_at)
|
|
1116
|
+
return self.to_response(await response_json(response), chat, started_at, context=context)
|
|
1114
1117
|
|
|
1115
1118
|
|
|
1116
1119
|
class MistralProvider(OpenAiCompatible):
|
|
@@ -1282,6 +1285,9 @@ def api_providers():
|
|
|
1282
1285
|
|
|
1283
1286
|
|
|
1284
1287
|
def to_error_message(e):
|
|
1288
|
+
# check if has 'message' attribute
|
|
1289
|
+
if hasattr(e, "message"):
|
|
1290
|
+
return e.message
|
|
1285
1291
|
return str(e)
|
|
1286
1292
|
|
|
1287
1293
|
|
|
@@ -1375,21 +1381,7 @@ async def g_chat_completion(chat, context=None):
|
|
|
1375
1381
|
accumulated_cost = 0.0
|
|
1376
1382
|
|
|
1377
1383
|
# Inject global tools if present
|
|
1378
|
-
current_chat = chat.
|
|
1379
|
-
if g_app.tool_definitions:
|
|
1380
|
-
only_tools_str = context.get("tools", "all")
|
|
1381
|
-
include_all_tools = only_tools_str == "all"
|
|
1382
|
-
only_tools = only_tools_str.split(",")
|
|
1383
|
-
|
|
1384
|
-
if include_all_tools or len(only_tools) > 0:
|
|
1385
|
-
if "tools" not in current_chat:
|
|
1386
|
-
current_chat["tools"] = []
|
|
1387
|
-
|
|
1388
|
-
existing_tools = {t["function"]["name"] for t in current_chat["tools"]}
|
|
1389
|
-
for tool_def in g_app.tool_definitions:
|
|
1390
|
-
name = tool_def["function"]["name"]
|
|
1391
|
-
if name not in existing_tools and (include_all_tools or name in only_tools):
|
|
1392
|
-
current_chat["tools"].append(tool_def)
|
|
1384
|
+
current_chat = g_app.create_chat_with_tools(chat, use_tools=context.get("tools", "all"))
|
|
1393
1385
|
|
|
1394
1386
|
# Apply pre-chat filters ONCE
|
|
1395
1387
|
context["chat"] = current_chat
|
|
@@ -1405,7 +1397,7 @@ async def g_chat_completion(chat, context=None):
|
|
|
1405
1397
|
if should_cancel_thread(context):
|
|
1406
1398
|
return
|
|
1407
1399
|
|
|
1408
|
-
response = await provider.chat(current_chat)
|
|
1400
|
+
response = await provider.chat(current_chat, context=context)
|
|
1409
1401
|
|
|
1410
1402
|
if should_cancel_thread(context):
|
|
1411
1403
|
return
|
|
@@ -1427,8 +1419,9 @@ async def g_chat_completion(chat, context=None):
|
|
|
1427
1419
|
choice = response.get("choices", [])[0] if response.get("choices") else {}
|
|
1428
1420
|
message = choice.get("message", {})
|
|
1429
1421
|
tool_calls = message.get("tool_calls")
|
|
1422
|
+
supports_tool_calls = model_info.get("tool_call", False)
|
|
1430
1423
|
|
|
1431
|
-
if tool_calls:
|
|
1424
|
+
if tool_calls and supports_tool_calls:
|
|
1432
1425
|
# Append the assistant's message with tool calls to history
|
|
1433
1426
|
if "messages" not in current_chat:
|
|
1434
1427
|
current_chat["messages"] = []
|
|
@@ -2179,6 +2172,13 @@ def text_from_file(filename):
|
|
|
2179
2172
|
return None
|
|
2180
2173
|
|
|
2181
2174
|
|
|
2175
|
+
def json_from_file(filename):
|
|
2176
|
+
if os.path.exists(filename):
|
|
2177
|
+
with open(filename, encoding="utf-8") as f:
|
|
2178
|
+
return json.load(f)
|
|
2179
|
+
return None
|
|
2180
|
+
|
|
2181
|
+
|
|
2182
2182
|
async def text_from_resource_or_url(filename):
|
|
2183
2183
|
text = text_from_resource(filename)
|
|
2184
2184
|
if not text:
|
|
@@ -2472,6 +2472,27 @@ class AppExtensions:
|
|
|
2472
2472
|
_dbg(f"exit({exit_code})")
|
|
2473
2473
|
sys.exit(exit_code)
|
|
2474
2474
|
|
|
2475
|
+
def create_chat_with_tools(self, chat, use_tools="all"):
|
|
2476
|
+
# Inject global tools if present
|
|
2477
|
+
current_chat = chat.copy()
|
|
2478
|
+
tools = current_chat.get("tools")
|
|
2479
|
+
if tools is None:
|
|
2480
|
+
tools = current_chat["tools"] = []
|
|
2481
|
+
if self.tool_definitions and len(tools) == 0:
|
|
2482
|
+
include_all_tools = use_tools == "all"
|
|
2483
|
+
only_tools_list = use_tools.split(",")
|
|
2484
|
+
|
|
2485
|
+
if include_all_tools or len(only_tools_list) > 0:
|
|
2486
|
+
if "tools" not in current_chat:
|
|
2487
|
+
current_chat["tools"] = []
|
|
2488
|
+
|
|
2489
|
+
existing_tools = {t["function"]["name"] for t in current_chat["tools"]}
|
|
2490
|
+
for tool_def in self.tool_definitions:
|
|
2491
|
+
name = tool_def["function"]["name"]
|
|
2492
|
+
if name not in existing_tools and (include_all_tools or name in only_tools_list):
|
|
2493
|
+
current_chat["tools"].append(tool_def)
|
|
2494
|
+
return current_chat
|
|
2495
|
+
|
|
2475
2496
|
|
|
2476
2497
|
def handler_name(handler):
|
|
2477
2498
|
if hasattr(handler, "__name__"):
|
|
@@ -2496,6 +2517,7 @@ class ExtensionContext:
|
|
|
2496
2517
|
self.verbose = g_verbose
|
|
2497
2518
|
self.aspect_ratios = app.aspect_ratios
|
|
2498
2519
|
self.request_args = app.request_args
|
|
2520
|
+
self.disabled = False
|
|
2499
2521
|
|
|
2500
2522
|
def chat_to_prompt(self, chat):
|
|
2501
2523
|
return chat_to_prompt(chat)
|
|
@@ -2521,6 +2543,9 @@ class ExtensionContext:
|
|
|
2521
2543
|
def text_from_file(self, path):
|
|
2522
2544
|
return text_from_file(path)
|
|
2523
2545
|
|
|
2546
|
+
def json_from_file(self, path):
|
|
2547
|
+
return json_from_file(path)
|
|
2548
|
+
|
|
2524
2549
|
def log(self, message):
|
|
2525
2550
|
if self.verbose:
|
|
2526
2551
|
print(f"[{self.name}] {message}", flush=True)
|
|
@@ -2626,6 +2651,9 @@ class ExtensionContext:
|
|
|
2626
2651
|
def get_cache_path(self, path=""):
|
|
2627
2652
|
return get_cache_path(path)
|
|
2628
2653
|
|
|
2654
|
+
def get_file_mime_type(self, filename):
|
|
2655
|
+
return get_file_mime_type(filename)
|
|
2656
|
+
|
|
2629
2657
|
def chat_request(self, template=None, text=None, model=None, system_prompt=None):
|
|
2630
2658
|
return self.app.chat_request(template=template, text=text, model=model, system_prompt=system_prompt)
|
|
2631
2659
|
|
|
@@ -2668,6 +2696,9 @@ class ExtensionContext:
|
|
|
2668
2696
|
def to_content(self, result):
|
|
2669
2697
|
return to_content(result)
|
|
2670
2698
|
|
|
2699
|
+
def create_chat_with_tools(self, chat, use_tools="all"):
|
|
2700
|
+
return self.app.create_chat_with_tools(chat, use_tools)
|
|
2701
|
+
|
|
2671
2702
|
|
|
2672
2703
|
def get_extensions_path():
|
|
2673
2704
|
return os.getenv("LLMS_EXTENSIONS_DIR", os.path.join(Path.home(), ".llms", "extensions"))
|
|
@@ -2787,6 +2818,10 @@ def install_extensions():
|
|
|
2787
2818
|
else:
|
|
2788
2819
|
_dbg(f"Extension {init_file} not found")
|
|
2789
2820
|
|
|
2821
|
+
if ctx.disabled:
|
|
2822
|
+
_log(f"Extension {item} was disabled")
|
|
2823
|
+
continue
|
|
2824
|
+
|
|
2790
2825
|
# if ui folder exists, serve as static files at /ext/{item}/
|
|
2791
2826
|
ui_path = os.path.join(item_path, "ui")
|
|
2792
2827
|
if os.path.exists(ui_path):
|
|
@@ -3366,7 +3401,7 @@ def main():
|
|
|
3366
3401
|
# if file and its .info.json already exists, return it
|
|
3367
3402
|
info_path = os.path.splitext(full_path)[0] + ".info.json"
|
|
3368
3403
|
if os.path.exists(full_path) and os.path.exists(info_path):
|
|
3369
|
-
return web.json_response(
|
|
3404
|
+
return web.json_response(json_from_file(info_path))
|
|
3370
3405
|
|
|
3371
3406
|
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
3372
3407
|
|
|
@@ -3415,9 +3450,9 @@ def main():
|
|
|
3415
3450
|
async def cache_handler(request):
|
|
3416
3451
|
path = request.match_info["tail"]
|
|
3417
3452
|
full_path = get_cache_path(path)
|
|
3453
|
+
info_path = os.path.splitext(full_path)[0] + ".info.json"
|
|
3418
3454
|
|
|
3419
3455
|
if "info" in request.query:
|
|
3420
|
-
info_path = os.path.splitext(full_path)[0] + ".info.json"
|
|
3421
3456
|
if not os.path.exists(info_path):
|
|
3422
3457
|
return web.Response(text="404: Not Found", status=404)
|
|
3423
3458
|
|
|
@@ -3446,11 +3481,23 @@ def main():
|
|
|
3446
3481
|
except Exception:
|
|
3447
3482
|
return web.Response(text="403: Forbidden", status=403)
|
|
3448
3483
|
|
|
3449
|
-
with open(full_path, "rb") as f:
|
|
3450
|
-
content = f.read()
|
|
3451
|
-
|
|
3452
3484
|
mimetype = get_file_mime_type(full_path)
|
|
3453
|
-
|
|
3485
|
+
if "download" in request.query:
|
|
3486
|
+
# download file as an attachment
|
|
3487
|
+
info = json_from_file(info_path) or {}
|
|
3488
|
+
mimetype = info.get("type", mimetype)
|
|
3489
|
+
filename = info.get("name") or os.path.basename(full_path)
|
|
3490
|
+
mtime = info.get("date", os.path.getmtime(full_path))
|
|
3491
|
+
mdate = datetime.fromtimestamp(mtime).isoformat()
|
|
3492
|
+
return web.FileResponse(
|
|
3493
|
+
full_path,
|
|
3494
|
+
headers={
|
|
3495
|
+
"Content-Disposition": f'attachment; filename="{filename}"; modification-date="{mdate}"',
|
|
3496
|
+
"Content-Type": mimetype,
|
|
3497
|
+
},
|
|
3498
|
+
)
|
|
3499
|
+
else:
|
|
3500
|
+
return web.FileResponse(full_path, headers={"Content-Type": mimetype})
|
|
3454
3501
|
|
|
3455
3502
|
app.router.add_get("/~cache/{tail:.*}", cache_handler)
|
|
3456
3503
|
|