llms-py 3.0.0b5__py3-none-any.whl → 3.0.0b7__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/__pycache__/main.cpython-314.pyc +0 -0
- llms/{ui/modules/analytics.mjs → extensions/analytics/ui/index.mjs} +4 -2
- llms/extensions/core_tools/__init__.py +358 -0
- llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/gallery/__init__.py +61 -0
- llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
- llms/extensions/gallery/db.py +298 -0
- llms/extensions/gallery/ui/index.mjs +480 -0
- llms/extensions/providers/__init__.py +18 -0
- llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/{providers → extensions/providers}/__pycache__/anthropic.cpython-314.pyc +0 -0
- llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
- llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
- llms/{providers → extensions/providers}/__pycache__/nvidia.cpython-314.pyc +0 -0
- llms/{providers → extensions/providers}/__pycache__/openai.cpython-314.pyc +0 -0
- llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
- llms/{providers → extensions/providers}/anthropic.py +1 -4
- llms/{providers → extensions/providers}/chutes.py +21 -18
- llms/{providers → extensions/providers}/google.py +99 -27
- llms/{providers → extensions/providers}/nvidia.py +6 -8
- llms/{providers → extensions/providers}/openai.py +3 -6
- llms/{providers → extensions/providers}/openrouter.py +12 -10
- llms/extensions/system_prompts/__init__.py +45 -0
- llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/system_prompts/ui/index.mjs +284 -0
- llms/extensions/system_prompts/ui/prompts.json +1067 -0
- llms/{ui/modules/tools.mjs → extensions/tools/ui/index.mjs} +4 -2
- llms/llms.json +17 -1
- llms/main.py +407 -175
- llms/providers-extra.json +0 -32
- llms/ui/App.mjs +17 -18
- llms/ui/ai.mjs +10 -3
- llms/ui/app.css +1553 -24
- llms/ui/ctx.mjs +70 -12
- llms/ui/index.mjs +13 -8
- llms/ui/modules/chat/ChatBody.mjs +11 -248
- llms/ui/modules/chat/HomeTools.mjs +254 -0
- llms/ui/modules/chat/SettingsDialog.mjs +1 -1
- llms/ui/modules/chat/index.mjs +278 -174
- llms/ui/modules/layout.mjs +2 -26
- llms/ui/modules/model-selector.mjs +1 -1
- llms/ui/modules/threads/index.mjs +5 -11
- llms/ui/modules/threads/threadStore.mjs +56 -2
- llms/ui/utils.mjs +21 -3
- {llms_py-3.0.0b5.dist-info → llms_py-3.0.0b7.dist-info}/METADATA +1 -1
- llms_py-3.0.0b7.dist-info/RECORD +80 -0
- llms/providers/__pycache__/chutes.cpython-314.pyc +0 -0
- llms/providers/__pycache__/google.cpython-314.pyc +0 -0
- llms/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
- llms_py-3.0.0b5.dist-info/RECORD +0 -66
- {llms_py-3.0.0b5.dist-info → llms_py-3.0.0b7.dist-info}/WHEEL +0 -0
- {llms_py-3.0.0b5.dist-info → llms_py-3.0.0b7.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.0b5.dist-info → llms_py-3.0.0b7.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.0b5.dist-info → llms_py-3.0.0b7.dist-info}/top_level.txt +0 -0
llms/main.py
CHANGED
|
@@ -40,12 +40,12 @@ try:
|
|
|
40
40
|
except ImportError:
|
|
41
41
|
HAS_PIL = False
|
|
42
42
|
|
|
43
|
-
VERSION = "3.0.
|
|
43
|
+
VERSION = "3.0.0b7"
|
|
44
44
|
_ROOT = None
|
|
45
|
-
DEBUG =
|
|
46
|
-
MOCK = False
|
|
47
|
-
MOCK_DIR = os.getenv("MOCK_DIR")
|
|
45
|
+
DEBUG = os.getenv("DEBUG") == "1"
|
|
48
46
|
MOCK = os.getenv("MOCK") == "1"
|
|
47
|
+
MOCK_DIR = os.getenv("MOCK_DIR")
|
|
48
|
+
DISABLE_EXTENSIONS = (os.getenv("LLMS_DISABLE") or "").split(",")
|
|
49
49
|
g_config_path = None
|
|
50
50
|
g_config = None
|
|
51
51
|
g_providers = None
|
|
@@ -493,7 +493,7 @@ class HTTPError(Exception):
|
|
|
493
493
|
super().__init__(f"HTTP {status} {reason}")
|
|
494
494
|
|
|
495
495
|
|
|
496
|
-
def
|
|
496
|
+
def save_bytes_to_cache(base64_data, filename, file_info):
|
|
497
497
|
ext = filename.split(".")[-1]
|
|
498
498
|
mimetype = get_file_mime_type(filename)
|
|
499
499
|
content = base64.b64decode(base64_data) if isinstance(base64_data, str) else base64_data
|
|
@@ -505,8 +505,39 @@ def save_image_to_cache(base64_data, filename, image_info):
|
|
|
505
505
|
subdir = sha256_hash[:2]
|
|
506
506
|
relative_path = f"{subdir}/{save_filename}"
|
|
507
507
|
full_path = get_cache_path(relative_path)
|
|
508
|
+
url = f"/~cache/{relative_path}"
|
|
509
|
+
|
|
510
|
+
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
511
|
+
|
|
512
|
+
with open(full_path, "wb") as f:
|
|
513
|
+
f.write(content)
|
|
514
|
+
info = {
|
|
515
|
+
"date": int(time.time()),
|
|
516
|
+
"url": url,
|
|
517
|
+
"size": len(content),
|
|
518
|
+
"type": mimetype,
|
|
519
|
+
"name": filename,
|
|
520
|
+
}
|
|
521
|
+
info.update(file_info)
|
|
522
|
+
|
|
523
|
+
g_app.on_cache_saved_filters({"url": url, "info": info})
|
|
524
|
+
|
|
525
|
+
return url, info
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def save_image_to_cache(base64_data, filename, image_info):
|
|
529
|
+
ext = filename.split(".")[-1]
|
|
530
|
+
mimetype = get_file_mime_type(filename)
|
|
531
|
+
content = base64.b64decode(base64_data) if isinstance(base64_data, str) else base64_data
|
|
532
|
+
sha256_hash = hashlib.sha256(content).hexdigest()
|
|
508
533
|
|
|
509
|
-
|
|
534
|
+
save_filename = f"{sha256_hash}.{ext}" if ext else sha256_hash
|
|
535
|
+
|
|
536
|
+
# Use first 2 chars for subdir to avoid too many files in one dir
|
|
537
|
+
subdir = sha256_hash[:2]
|
|
538
|
+
relative_path = f"{subdir}/{save_filename}"
|
|
539
|
+
full_path = get_cache_path(relative_path)
|
|
540
|
+
url = f"/~cache/{relative_path}"
|
|
510
541
|
|
|
511
542
|
# if file and its .info.json already exists, return it
|
|
512
543
|
info_path = os.path.splitext(full_path)[0] + ".info.json"
|
|
@@ -545,6 +576,8 @@ def save_image_to_cache(base64_data, filename, image_info):
|
|
|
545
576
|
with open(info_path, "w") as f:
|
|
546
577
|
json.dump(info, f)
|
|
547
578
|
|
|
579
|
+
g_app.on_cache_saved_filters({"url": url, "info": info})
|
|
580
|
+
|
|
548
581
|
return url, info
|
|
549
582
|
|
|
550
583
|
|
|
@@ -577,6 +610,12 @@ def chat_to_prompt(chat):
|
|
|
577
610
|
return prompt
|
|
578
611
|
|
|
579
612
|
|
|
613
|
+
def chat_to_username(chat):
|
|
614
|
+
if "metadata" in chat and "user" in chat["metadata"]:
|
|
615
|
+
return chat["metadata"]["user"]
|
|
616
|
+
return None
|
|
617
|
+
|
|
618
|
+
|
|
580
619
|
def last_user_prompt(chat):
|
|
581
620
|
prompt = ""
|
|
582
621
|
if "messages" in chat:
|
|
@@ -593,6 +632,21 @@ def last_user_prompt(chat):
|
|
|
593
632
|
return prompt
|
|
594
633
|
|
|
595
634
|
|
|
635
|
+
def to_file_info(chat, info=None, response=None):
|
|
636
|
+
prompt = last_user_prompt(chat)
|
|
637
|
+
ret = info or {}
|
|
638
|
+
if chat["model"] and "model" not in ret:
|
|
639
|
+
ret["model"] = chat["model"]
|
|
640
|
+
if prompt and "prompt" not in ret:
|
|
641
|
+
ret["prompt"] = prompt
|
|
642
|
+
if "image_config" in chat:
|
|
643
|
+
ret.update(chat["image_config"])
|
|
644
|
+
user = chat_to_username(chat)
|
|
645
|
+
if user:
|
|
646
|
+
ret["user"] = user
|
|
647
|
+
return ret
|
|
648
|
+
|
|
649
|
+
|
|
596
650
|
# Image Generator Providers
|
|
597
651
|
class GeneratorBase:
|
|
598
652
|
def __init__(self, **kwargs):
|
|
@@ -875,13 +929,14 @@ class OpenAiCompatible:
|
|
|
875
929
|
_log(f"POST {self.chat_url}")
|
|
876
930
|
_log(chat_summary(chat))
|
|
877
931
|
# remove metadata if any (conflicts with some providers, e.g. Z.ai)
|
|
878
|
-
chat.pop("metadata", None)
|
|
932
|
+
metadata = chat.pop("metadata", None)
|
|
879
933
|
|
|
880
934
|
async with aiohttp.ClientSession() as session:
|
|
881
935
|
started_at = time.time()
|
|
882
936
|
async with session.post(
|
|
883
937
|
self.chat_url, headers=self.headers, data=json.dumps(chat), timeout=aiohttp.ClientTimeout(total=120)
|
|
884
938
|
) as response:
|
|
939
|
+
chat["metadata"] = metadata
|
|
885
940
|
return self.to_response(await response_json(response), chat, started_at)
|
|
886
941
|
|
|
887
942
|
|
|
@@ -1053,7 +1108,48 @@ def api_providers():
|
|
|
1053
1108
|
return ret
|
|
1054
1109
|
|
|
1055
1110
|
|
|
1056
|
-
|
|
1111
|
+
def to_error_response(e, stacktrace=False):
|
|
1112
|
+
status = {"errorCode": "Error", "message": str(e)}
|
|
1113
|
+
if stacktrace:
|
|
1114
|
+
status["stackTrace"] = traceback.format_exc()
|
|
1115
|
+
return {"responseStatus": status}
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def create_error_response(message, error_code="Error", stack_trace=None):
|
|
1119
|
+
ret = {"responseStatus": {"errorCode": error_code, "message": message}}
|
|
1120
|
+
if stack_trace:
|
|
1121
|
+
ret["responseStatus"]["stackTrace"] = stack_trace
|
|
1122
|
+
return ret
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
def g_chat_request(template=None, text=None, model=None, system_prompt=None):
|
|
1126
|
+
chat_template = g_config["defaults"].get(template or "text")
|
|
1127
|
+
if not chat_template:
|
|
1128
|
+
raise Exception(f"Chat template '{template}' not found")
|
|
1129
|
+
|
|
1130
|
+
chat = chat_template.copy()
|
|
1131
|
+
if model:
|
|
1132
|
+
chat["model"] = model
|
|
1133
|
+
if system_prompt is not None:
|
|
1134
|
+
chat["messages"].insert(0, {"role": "system", "content": system_prompt})
|
|
1135
|
+
if text is not None:
|
|
1136
|
+
if not chat["messages"] or len(chat["messages"]) == 0:
|
|
1137
|
+
chat["messages"] = [{"role": "user", "content": [{"type": "text", "text": ""}]}]
|
|
1138
|
+
|
|
1139
|
+
# replace content of last message if exists, else add
|
|
1140
|
+
last_msg = chat["messages"][-1] if "messages" in chat else None
|
|
1141
|
+
if last_msg and last_msg["role"] == "user":
|
|
1142
|
+
if isinstance(last_msg["content"], list):
|
|
1143
|
+
last_msg["content"][-1]["text"] = text
|
|
1144
|
+
else:
|
|
1145
|
+
last_msg["content"] = text
|
|
1146
|
+
else:
|
|
1147
|
+
chat["messages"].append({"role": "user", "content": text})
|
|
1148
|
+
|
|
1149
|
+
return chat
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
async def g_chat_completion(chat, context=None):
|
|
1057
1153
|
model = chat["model"]
|
|
1058
1154
|
# get first provider that has the model
|
|
1059
1155
|
candidate_providers = [name for name, provider in g_handlers.items() if provider.provider_model(model)]
|
|
@@ -1073,8 +1169,9 @@ async def chat_completion(chat):
|
|
|
1073
1169
|
include_all_tools = False
|
|
1074
1170
|
only_tools = []
|
|
1075
1171
|
if "metadata" in chat:
|
|
1076
|
-
|
|
1077
|
-
include_all_tools =
|
|
1172
|
+
only_tools_str = chat["metadata"].get("only_tools", "")
|
|
1173
|
+
include_all_tools = only_tools_str == "all"
|
|
1174
|
+
only_tools = only_tools_str.split(",")
|
|
1078
1175
|
|
|
1079
1176
|
if include_all_tools or len(only_tools) > 0:
|
|
1080
1177
|
if "tools" not in current_chat:
|
|
@@ -1217,16 +1314,8 @@ async def cli_chat(chat, image=None, audio=None, file=None, args=None, raw=False
|
|
|
1217
1314
|
printdump(chat)
|
|
1218
1315
|
|
|
1219
1316
|
try:
|
|
1220
|
-
|
|
1221
|
-
context = {"chat": chat}
|
|
1222
|
-
for filter_func in g_app.chat_request_filters:
|
|
1223
|
-
chat = await filter_func(chat, context)
|
|
1224
|
-
|
|
1225
|
-
response = await chat_completion(chat)
|
|
1317
|
+
response = await g_app.chat_completion(chat)
|
|
1226
1318
|
|
|
1227
|
-
# Apply post-chat filters
|
|
1228
|
-
for filter_func in g_app.chat_response_filters:
|
|
1229
|
-
response = await filter_func(response, context)
|
|
1230
1319
|
if raw:
|
|
1231
1320
|
print(json.dumps(response, indent=2))
|
|
1232
1321
|
exit(0)
|
|
@@ -1243,13 +1332,17 @@ async def cli_chat(chat, image=None, audio=None, file=None, args=None, raw=False
|
|
|
1243
1332
|
for image in msg["images"]:
|
|
1244
1333
|
image_url = image["image_url"]["url"]
|
|
1245
1334
|
generated_files.append(image_url)
|
|
1335
|
+
if "audios" in msg:
|
|
1336
|
+
for audio in msg["audios"]:
|
|
1337
|
+
audio_url = audio["audio_url"]["url"]
|
|
1338
|
+
generated_files.append(audio_url)
|
|
1246
1339
|
|
|
1247
1340
|
if len(generated_files) > 0:
|
|
1248
1341
|
print("\nSaved files:")
|
|
1249
1342
|
for file in generated_files:
|
|
1250
|
-
if file.startswith("
|
|
1251
|
-
print(get_cache_path(file[
|
|
1252
|
-
|
|
1343
|
+
if file.startswith("/~cache"):
|
|
1344
|
+
print(get_cache_path(file[8:]))
|
|
1345
|
+
print(f"http://localhost:8000/{file}")
|
|
1253
1346
|
else:
|
|
1254
1347
|
print(file)
|
|
1255
1348
|
|
|
@@ -1287,7 +1380,7 @@ def init_llms(config, providers):
|
|
|
1287
1380
|
# iterate over config and replace $ENV with env value
|
|
1288
1381
|
for key, value in g_config.items():
|
|
1289
1382
|
if isinstance(value, str) and value.startswith("$"):
|
|
1290
|
-
g_config[key] = os.
|
|
1383
|
+
g_config[key] = os.getenv(value[1:], "")
|
|
1291
1384
|
|
|
1292
1385
|
# if g_verbose:
|
|
1293
1386
|
# printdump(g_config)
|
|
@@ -1325,11 +1418,11 @@ def create_provider_kwargs(definition, provider=None):
|
|
|
1325
1418
|
if "api_key" in provider:
|
|
1326
1419
|
value = provider["api_key"]
|
|
1327
1420
|
if isinstance(value, str) and value.startswith("$"):
|
|
1328
|
-
provider["api_key"] = os.
|
|
1421
|
+
provider["api_key"] = os.getenv(value[1:], "")
|
|
1329
1422
|
|
|
1330
1423
|
if "api_key" not in provider and "env" in provider:
|
|
1331
1424
|
for env_var in provider["env"]:
|
|
1332
|
-
val = os.
|
|
1425
|
+
val = os.getenv(env_var)
|
|
1333
1426
|
if val:
|
|
1334
1427
|
provider["api_key"] = val
|
|
1335
1428
|
break
|
|
@@ -1466,11 +1559,11 @@ def print_status():
|
|
|
1466
1559
|
|
|
1467
1560
|
|
|
1468
1561
|
def home_llms_path(filename):
|
|
1469
|
-
return f"{os.
|
|
1562
|
+
return f"{os.getenv('HOME')}/.llms/{filename}"
|
|
1470
1563
|
|
|
1471
1564
|
|
|
1472
|
-
def get_cache_path(
|
|
1473
|
-
return home_llms_path(f"cache/{
|
|
1565
|
+
def get_cache_path(path=""):
|
|
1566
|
+
return home_llms_path(f"cache/{path}") if path else home_llms_path("cache")
|
|
1474
1567
|
|
|
1475
1568
|
|
|
1476
1569
|
def get_config_path():
|
|
@@ -1479,8 +1572,8 @@ def get_config_path():
|
|
|
1479
1572
|
"./llms.json",
|
|
1480
1573
|
home_config_path,
|
|
1481
1574
|
]
|
|
1482
|
-
if os.
|
|
1483
|
-
check_paths.insert(0, os.
|
|
1575
|
+
if os.getenv("LLMS_CONFIG_PATH"):
|
|
1576
|
+
check_paths.insert(0, os.getenv("LLMS_CONFIG_PATH"))
|
|
1484
1577
|
|
|
1485
1578
|
for check_path in check_paths:
|
|
1486
1579
|
g_config_path = os.path.normpath(os.path.join(os.path.dirname(__file__), check_path))
|
|
@@ -1955,7 +2048,10 @@ class AppExtensions:
|
|
|
1955
2048
|
self.chat_response_filters = []
|
|
1956
2049
|
self.server_add_get = []
|
|
1957
2050
|
self.server_add_post = []
|
|
1958
|
-
self.
|
|
2051
|
+
self.server_add_put = []
|
|
2052
|
+
self.server_add_delete = []
|
|
2053
|
+
self.server_add_patch = []
|
|
2054
|
+
self.cache_saved_filters = []
|
|
1959
2055
|
self.tools = {}
|
|
1960
2056
|
self.tool_definitions = []
|
|
1961
2057
|
self.all_providers = [
|
|
@@ -1980,6 +2076,62 @@ class AppExtensions:
|
|
|
1980
2076
|
"21:9": "1536×672",
|
|
1981
2077
|
}
|
|
1982
2078
|
|
|
2079
|
+
def get_session(self, request):
|
|
2080
|
+
session_token = get_session_token(request)
|
|
2081
|
+
|
|
2082
|
+
if not session_token or session_token not in g_sessions:
|
|
2083
|
+
return None
|
|
2084
|
+
|
|
2085
|
+
session_data = g_sessions[session_token]
|
|
2086
|
+
return session_data
|
|
2087
|
+
|
|
2088
|
+
def get_username(self, request):
|
|
2089
|
+
session = self.get_session(request)
|
|
2090
|
+
if session:
|
|
2091
|
+
return session.get("userName")
|
|
2092
|
+
return None
|
|
2093
|
+
|
|
2094
|
+
def get_user_path(self, username=None):
|
|
2095
|
+
if username:
|
|
2096
|
+
return home_llms_path(os.path.join("user", username))
|
|
2097
|
+
return home_llms_path(os.path.join("user", "default"))
|
|
2098
|
+
|
|
2099
|
+
def chat_request(self, template=None, text=None, model=None, system_prompt=None):
|
|
2100
|
+
return g_chat_request(template=template, text=text, model=model, system_prompt=system_prompt)
|
|
2101
|
+
|
|
2102
|
+
async def chat_completion(self, chat, context=None):
|
|
2103
|
+
# Apply pre-chat filters
|
|
2104
|
+
if context is None:
|
|
2105
|
+
context = {"chat": chat}
|
|
2106
|
+
elif "request" in context:
|
|
2107
|
+
username = self.get_username(context["request"])
|
|
2108
|
+
if username:
|
|
2109
|
+
if "metadata" not in chat:
|
|
2110
|
+
chat["metadata"] = {}
|
|
2111
|
+
chat["metadata"]["user"] = username
|
|
2112
|
+
|
|
2113
|
+
for filter_func in self.chat_request_filters:
|
|
2114
|
+
chat = await filter_func(chat, context)
|
|
2115
|
+
|
|
2116
|
+
response = await g_chat_completion(chat, context)
|
|
2117
|
+
|
|
2118
|
+
# Apply post-chat filters
|
|
2119
|
+
for filter_func in self.chat_response_filters:
|
|
2120
|
+
response = await filter_func(response, context)
|
|
2121
|
+
|
|
2122
|
+
return response
|
|
2123
|
+
|
|
2124
|
+
def on_cache_saved_filters(self, context):
|
|
2125
|
+
# _log(f"on_cache_saved_filters {len(self.cache_saved_filters)}: {context['url']}")
|
|
2126
|
+
for filter_func in self.cache_saved_filters:
|
|
2127
|
+
filter_func(context)
|
|
2128
|
+
|
|
2129
|
+
|
|
2130
|
+
def handler_name(handler):
|
|
2131
|
+
if hasattr(handler, "__name__"):
|
|
2132
|
+
return handler.__name__
|
|
2133
|
+
return "unknown"
|
|
2134
|
+
|
|
1983
2135
|
|
|
1984
2136
|
class ExtensionContext:
|
|
1985
2137
|
def __init__(self, app, path):
|
|
@@ -1993,6 +2145,7 @@ class ExtensionContext:
|
|
|
1993
2145
|
self.MOCK_DIR = MOCK_DIR
|
|
1994
2146
|
self.debug = DEBUG
|
|
1995
2147
|
self.verbose = g_verbose
|
|
2148
|
+
self.aspect_ratios = app.aspect_ratios
|
|
1996
2149
|
|
|
1997
2150
|
def chat_to_prompt(self, chat):
|
|
1998
2151
|
return chat_to_prompt(chat)
|
|
@@ -2000,9 +2153,15 @@ class ExtensionContext:
|
|
|
2000
2153
|
def last_user_prompt(self, chat):
|
|
2001
2154
|
return last_user_prompt(chat)
|
|
2002
2155
|
|
|
2156
|
+
def to_file_info(self, chat, info=None, response=None):
|
|
2157
|
+
return to_file_info(chat, info=info, response=response)
|
|
2158
|
+
|
|
2003
2159
|
def save_image_to_cache(self, base64_data, filename, image_info):
|
|
2004
2160
|
return save_image_to_cache(base64_data, filename, image_info)
|
|
2005
2161
|
|
|
2162
|
+
def save_bytes_to_cache(self, bytes_data, filename, file_info):
|
|
2163
|
+
return save_bytes_to_cache(bytes_data, filename, file_info)
|
|
2164
|
+
|
|
2006
2165
|
def text_from_file(self, path):
|
|
2007
2166
|
return text_from_file(path)
|
|
2008
2167
|
|
|
@@ -2026,7 +2185,7 @@ class ExtensionContext:
|
|
|
2026
2185
|
print(traceback.format_exc(), flush=True)
|
|
2027
2186
|
|
|
2028
2187
|
def add_provider(self, provider):
|
|
2029
|
-
self.log(f"Registered provider: {provider}")
|
|
2188
|
+
self.log(f"Registered provider: {provider.__name__}")
|
|
2030
2189
|
self.app.all_providers.append(provider)
|
|
2031
2190
|
|
|
2032
2191
|
def register_ui_extension(self, index):
|
|
@@ -2035,13 +2194,17 @@ class ExtensionContext:
|
|
|
2035
2194
|
self.app.ui_extensions.append({"id": self.name, "path": path})
|
|
2036
2195
|
|
|
2037
2196
|
def register_chat_request_filter(self, handler):
|
|
2038
|
-
self.log(f"Registered chat request filter: {handler}")
|
|
2197
|
+
self.log(f"Registered chat request filter: {handler_name(handler)}")
|
|
2039
2198
|
self.app.chat_request_filters.append(handler)
|
|
2040
2199
|
|
|
2041
2200
|
def register_chat_response_filter(self, handler):
|
|
2042
|
-
self.log(f"Registered chat response filter: {handler}")
|
|
2201
|
+
self.log(f"Registered chat response filter: {handler_name(handler)}")
|
|
2043
2202
|
self.app.chat_response_filters.append(handler)
|
|
2044
2203
|
|
|
2204
|
+
def register_cache_saved_filter(self, handler):
|
|
2205
|
+
self.log(f"Registered cache saved filter: {handler_name(handler)}")
|
|
2206
|
+
self.app.cache_saved_filters.append(handler)
|
|
2207
|
+
|
|
2045
2208
|
def add_static_files(self, ext_dir):
|
|
2046
2209
|
self.log(f"Registered static files: {ext_dir}")
|
|
2047
2210
|
|
|
@@ -2062,11 +2225,29 @@ class ExtensionContext:
|
|
|
2062
2225
|
self.dbg(f"Registered POST: {os.path.join(self.ext_prefix, path)}")
|
|
2063
2226
|
self.app.server_add_post.append((os.path.join(self.ext_prefix, path), handler, kwargs))
|
|
2064
2227
|
|
|
2228
|
+
def add_put(self, path, handler, **kwargs):
|
|
2229
|
+
self.dbg(f"Registered PUT: {os.path.join(self.ext_prefix, path)}")
|
|
2230
|
+
self.app.server_add_put.append((os.path.join(self.ext_prefix, path), handler, kwargs))
|
|
2231
|
+
|
|
2232
|
+
def add_delete(self, path, handler, **kwargs):
|
|
2233
|
+
self.dbg(f"Registered DELETE: {os.path.join(self.ext_prefix, path)}")
|
|
2234
|
+
self.app.server_add_delete.append((os.path.join(self.ext_prefix, path), handler, kwargs))
|
|
2235
|
+
|
|
2236
|
+
def add_patch(self, path, handler, **kwargs):
|
|
2237
|
+
self.dbg(f"Registered PATCH: {os.path.join(self.ext_prefix, path)}")
|
|
2238
|
+
self.app.server_add_patch.append((os.path.join(self.ext_prefix, path), handler, kwargs))
|
|
2239
|
+
|
|
2065
2240
|
def get_config(self):
|
|
2066
2241
|
return g_config
|
|
2067
2242
|
|
|
2068
|
-
def
|
|
2069
|
-
return
|
|
2243
|
+
def get_cache_path(self, path=""):
|
|
2244
|
+
return get_cache_path(path)
|
|
2245
|
+
|
|
2246
|
+
def chat_request(self, template=None, text=None, model=None, system_prompt=None):
|
|
2247
|
+
return self.app.chat_request(template=template, text=text, model=model, system_prompt=system_prompt)
|
|
2248
|
+
|
|
2249
|
+
def chat_completion(self, chat, context=None):
|
|
2250
|
+
return self.app.chat_completion(chat, context=context)
|
|
2070
2251
|
|
|
2071
2252
|
def get_providers(self):
|
|
2072
2253
|
return g_handlers
|
|
@@ -2074,21 +2255,6 @@ class ExtensionContext:
|
|
|
2074
2255
|
def get_provider(self, name):
|
|
2075
2256
|
return g_handlers.get(name)
|
|
2076
2257
|
|
|
2077
|
-
def get_session(self, request):
|
|
2078
|
-
session_token = get_session_token(request)
|
|
2079
|
-
|
|
2080
|
-
if not session_token or session_token not in g_sessions:
|
|
2081
|
-
return None
|
|
2082
|
-
|
|
2083
|
-
session_data = g_sessions[session_token]
|
|
2084
|
-
return session_data
|
|
2085
|
-
|
|
2086
|
-
def get_username(self, request):
|
|
2087
|
-
session = self.get_session(request)
|
|
2088
|
-
if session:
|
|
2089
|
-
return session.get("userName")
|
|
2090
|
-
return None
|
|
2091
|
-
|
|
2092
2258
|
def register_tool(self, func, tool_def=None):
|
|
2093
2259
|
if tool_def is None:
|
|
2094
2260
|
tool_def = function_to_tool_definition(func)
|
|
@@ -2098,44 +2264,71 @@ class ExtensionContext:
|
|
|
2098
2264
|
self.app.tools[name] = func
|
|
2099
2265
|
self.app.tool_definitions.append(tool_def)
|
|
2100
2266
|
|
|
2267
|
+
def get_session(self, request):
|
|
2268
|
+
return self.app.get_session(request)
|
|
2101
2269
|
|
|
2102
|
-
def
|
|
2103
|
-
|
|
2104
|
-
if not providers_path.exists():
|
|
2105
|
-
return
|
|
2270
|
+
def get_username(self, request):
|
|
2271
|
+
return self.app.get_username(request)
|
|
2106
2272
|
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
continue
|
|
2273
|
+
def get_user_path(self, username=None):
|
|
2274
|
+
return self.app.get_user_path(username)
|
|
2110
2275
|
|
|
2111
|
-
item_path = providers_path / item
|
|
2112
|
-
module_name = item[:-3]
|
|
2113
2276
|
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
if spec and spec.loader:
|
|
2117
|
-
module = importlib.util.module_from_spec(spec)
|
|
2118
|
-
sys.modules[f"llms.providers.{module_name}"] = module
|
|
2119
|
-
spec.loader.exec_module(module)
|
|
2120
|
-
|
|
2121
|
-
install_func = getattr(module, "__install__", None)
|
|
2122
|
-
if callable(install_func):
|
|
2123
|
-
install_func(ExtensionContext(g_app, item_path))
|
|
2124
|
-
_log(f"Loaded builtin extension: {module_name}")
|
|
2125
|
-
except Exception as e:
|
|
2126
|
-
_err(f"Failed to load builtin extension {module_name}", e)
|
|
2277
|
+
def get_extensions_path():
|
|
2278
|
+
return os.getenv("LLMS_EXTENSIONS_DIR", os.path.join(Path.home(), ".llms", "extensions"))
|
|
2127
2279
|
|
|
2128
2280
|
|
|
2129
|
-
def
|
|
2130
|
-
|
|
2281
|
+
def get_disabled_extensions():
|
|
2282
|
+
ret = DISABLE_EXTENSIONS.copy()
|
|
2283
|
+
if g_config:
|
|
2284
|
+
for ext in g_config.get("disable_extensions", []):
|
|
2285
|
+
if ext not in ret:
|
|
2286
|
+
ret.append(ext)
|
|
2287
|
+
return ret
|
|
2131
2288
|
|
|
2132
2289
|
|
|
2133
|
-
def
|
|
2290
|
+
def get_extensions_dirs():
|
|
2291
|
+
"""
|
|
2292
|
+
Returns a list of extension directories.
|
|
2293
|
+
"""
|
|
2134
2294
|
extensions_path = get_extensions_path()
|
|
2135
2295
|
os.makedirs(extensions_path, exist_ok=True)
|
|
2136
2296
|
|
|
2137
|
-
|
|
2138
|
-
|
|
2297
|
+
# allow overriding builtin extensions
|
|
2298
|
+
override_extensions = []
|
|
2299
|
+
if os.path.exists(extensions_path):
|
|
2300
|
+
override_extensions = os.listdir(extensions_path)
|
|
2301
|
+
|
|
2302
|
+
ret = []
|
|
2303
|
+
disabled_extensions = get_disabled_extensions()
|
|
2304
|
+
|
|
2305
|
+
builtin_extensions_dir = _ROOT / "extensions"
|
|
2306
|
+
if os.path.exists(builtin_extensions_dir):
|
|
2307
|
+
for item in os.listdir(builtin_extensions_dir):
|
|
2308
|
+
if os.path.isdir(os.path.join(builtin_extensions_dir, item)):
|
|
2309
|
+
if item in override_extensions:
|
|
2310
|
+
continue
|
|
2311
|
+
if item in disabled_extensions:
|
|
2312
|
+
continue
|
|
2313
|
+
ret.append(os.path.join(builtin_extensions_dir, item))
|
|
2314
|
+
|
|
2315
|
+
if os.path.exists(extensions_path):
|
|
2316
|
+
for item in os.listdir(extensions_path):
|
|
2317
|
+
if os.path.isdir(os.path.join(extensions_path, item)):
|
|
2318
|
+
if item in disabled_extensions:
|
|
2319
|
+
continue
|
|
2320
|
+
ret.append(os.path.join(extensions_path, item))
|
|
2321
|
+
|
|
2322
|
+
return ret
|
|
2323
|
+
|
|
2324
|
+
|
|
2325
|
+
def init_extensions(parser):
|
|
2326
|
+
"""
|
|
2327
|
+
Initializes extensions by loading their __init__.py files and calling the __parser__ function if it exists.
|
|
2328
|
+
"""
|
|
2329
|
+
for item_path in get_extensions_dirs():
|
|
2330
|
+
item = os.path.basename(item_path)
|
|
2331
|
+
|
|
2139
2332
|
if os.path.isdir(item_path):
|
|
2140
2333
|
try:
|
|
2141
2334
|
# check for __parser__ function if exists in __init.__.py and call it with parser
|
|
@@ -2160,25 +2353,28 @@ def install_extensions():
|
|
|
2160
2353
|
Scans ensure ~/.llms/extensions/ for directories with __init__.py and loads them as extensions.
|
|
2161
2354
|
Calls the `__install__(ctx)` function in the extension module.
|
|
2162
2355
|
"""
|
|
2163
|
-
extensions_path = get_extensions_path()
|
|
2164
|
-
os.makedirs(extensions_path, exist_ok=True)
|
|
2165
2356
|
|
|
2166
|
-
|
|
2357
|
+
extension_dirs = get_extensions_dirs()
|
|
2358
|
+
ext_count = len(list(extension_dirs))
|
|
2167
2359
|
if ext_count == 0:
|
|
2168
2360
|
_log("No extensions found")
|
|
2169
2361
|
return
|
|
2170
2362
|
|
|
2363
|
+
disabled_extensions = get_disabled_extensions()
|
|
2364
|
+
if len(disabled_extensions) > 0:
|
|
2365
|
+
_log(f"Disabled extensions: {', '.join(disabled_extensions)}")
|
|
2366
|
+
|
|
2171
2367
|
_log(f"Installing {ext_count} extension{'' if ext_count == 1 else 's'}...")
|
|
2172
2368
|
|
|
2173
|
-
|
|
2369
|
+
for item_path in extension_dirs:
|
|
2370
|
+
item = os.path.basename(item_path)
|
|
2174
2371
|
|
|
2175
|
-
for item in os.listdir(extensions_path):
|
|
2176
|
-
item_path = os.path.join(extensions_path, item)
|
|
2177
2372
|
if os.path.isdir(item_path):
|
|
2178
|
-
|
|
2179
|
-
|
|
2373
|
+
sys.path.append(item_path)
|
|
2374
|
+
try:
|
|
2180
2375
|
ctx = ExtensionContext(g_app, item_path)
|
|
2181
|
-
|
|
2376
|
+
init_file = os.path.join(item_path, "__init__.py")
|
|
2377
|
+
if os.path.exists(init_file):
|
|
2182
2378
|
spec = importlib.util.spec_from_file_location(item, init_file)
|
|
2183
2379
|
if spec and spec.loader:
|
|
2184
2380
|
module = importlib.util.module_from_spec(spec)
|
|
@@ -2193,20 +2389,20 @@ def install_extensions():
|
|
|
2193
2389
|
_dbg(f"Extension {item} has no __install__ function")
|
|
2194
2390
|
else:
|
|
2195
2391
|
_dbg(f"Extension {item} has no __init__.py")
|
|
2392
|
+
else:
|
|
2393
|
+
_dbg(f"Extension {init_file} not found")
|
|
2196
2394
|
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2395
|
+
# if ui folder exists, serve as static files at /ext/{item}/
|
|
2396
|
+
ui_path = os.path.join(item_path, "ui")
|
|
2397
|
+
if os.path.exists(ui_path):
|
|
2398
|
+
ctx.add_static_files(ui_path)
|
|
2201
2399
|
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2400
|
+
# Register UI extension if index.mjs exists (/ext/{item}/index.mjs)
|
|
2401
|
+
if os.path.exists(os.path.join(ui_path, "index.mjs")):
|
|
2402
|
+
ctx.register_ui_extension("index.mjs")
|
|
2205
2403
|
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
else:
|
|
2209
|
-
_dbg(f"Extension {init_file} not found")
|
|
2404
|
+
except Exception as e:
|
|
2405
|
+
_err(f"Failed to install extension {item}", e)
|
|
2210
2406
|
else:
|
|
2211
2407
|
_dbg(f"Extension {item} not found: {item_path} is not a directory {os.path.exists(item_path)}")
|
|
2212
2408
|
|
|
@@ -2215,11 +2411,9 @@ def run_extension_cli():
|
|
|
2215
2411
|
"""
|
|
2216
2412
|
Run the CLI for an extension.
|
|
2217
2413
|
"""
|
|
2218
|
-
|
|
2219
|
-
|
|
2414
|
+
for item_path in get_extensions_dirs():
|
|
2415
|
+
item = os.path.basename(item_path)
|
|
2220
2416
|
|
|
2221
|
-
for item in os.listdir(extensions_path):
|
|
2222
|
-
item_path = os.path.join(extensions_path, item)
|
|
2223
2417
|
if os.path.isdir(item_path):
|
|
2224
2418
|
init_file = os.path.join(item_path, "__init__.py")
|
|
2225
2419
|
if os.path.exists(init_file):
|
|
@@ -2234,8 +2428,8 @@ def run_extension_cli():
|
|
|
2234
2428
|
# Check for __run__ function if exists in __init__.py and call it with ctx
|
|
2235
2429
|
run_func = getattr(module, "__run__", None)
|
|
2236
2430
|
if callable(run_func):
|
|
2431
|
+
_log(f"Running extension {item}...")
|
|
2237
2432
|
handled = run_func(ctx)
|
|
2238
|
-
_log(f"Extension {item} was run")
|
|
2239
2433
|
return handled
|
|
2240
2434
|
|
|
2241
2435
|
except Exception as e:
|
|
@@ -2246,6 +2440,11 @@ def run_extension_cli():
|
|
|
2246
2440
|
def main():
|
|
2247
2441
|
global _ROOT, g_verbose, g_default_model, g_logprefix, g_providers, g_config, g_config_path, g_app
|
|
2248
2442
|
|
|
2443
|
+
_ROOT = os.getenv("LLMS_ROOT", resolve_root())
|
|
2444
|
+
if not _ROOT:
|
|
2445
|
+
print("Resource root not found")
|
|
2446
|
+
exit(1)
|
|
2447
|
+
|
|
2249
2448
|
parser = argparse.ArgumentParser(description=f"llms v{VERSION}")
|
|
2250
2449
|
parser.add_argument("--config", default=None, help="Path to config file", metavar="FILE")
|
|
2251
2450
|
parser.add_argument("--providers", default=None, help="Path to models.dev providers file", metavar="FILE")
|
|
@@ -2282,9 +2481,7 @@ def main():
|
|
|
2282
2481
|
|
|
2283
2482
|
parser.add_argument("--init", action="store_true", help="Create a default llms.json")
|
|
2284
2483
|
parser.add_argument("--update-providers", action="store_true", help="Update local models.dev providers.json")
|
|
2285
|
-
parser.add_argument("--update-extensions", action="store_true", help="Update installed extensions")
|
|
2286
2484
|
|
|
2287
|
-
parser.add_argument("--root", default=None, help="Change root directory for UI files", metavar="PATH")
|
|
2288
2485
|
parser.add_argument("--logprefix", default="", help="Prefix used in log messages", metavar="PREFIX")
|
|
2289
2486
|
parser.add_argument("--verbose", action="store_true", help="Verbose output")
|
|
2290
2487
|
|
|
@@ -2322,7 +2519,7 @@ def main():
|
|
|
2322
2519
|
g_app = AppExtensions(cli_args, extra_args)
|
|
2323
2520
|
|
|
2324
2521
|
# Check for verbose mode from CLI argument or environment variables
|
|
2325
|
-
verbose_env = os.
|
|
2522
|
+
verbose_env = os.getenv("VERBOSE", "").lower()
|
|
2326
2523
|
if cli_args.verbose or verbose_env in ("1", "true"):
|
|
2327
2524
|
g_verbose = True
|
|
2328
2525
|
# printdump(cli_args)
|
|
@@ -2331,11 +2528,6 @@ def main():
|
|
|
2331
2528
|
if cli_args.logprefix:
|
|
2332
2529
|
g_logprefix = cli_args.logprefix
|
|
2333
2530
|
|
|
2334
|
-
_ROOT = Path(cli_args.root) if cli_args.root else resolve_root()
|
|
2335
|
-
if not _ROOT:
|
|
2336
|
-
print("Resource root not found")
|
|
2337
|
-
exit(1)
|
|
2338
|
-
|
|
2339
2531
|
home_config_path = home_llms_path("llms.json")
|
|
2340
2532
|
home_providers_path = home_llms_path("providers.json")
|
|
2341
2533
|
home_providers_extra_path = home_llms_path("providers-extra.json")
|
|
@@ -2396,7 +2588,7 @@ def main():
|
|
|
2396
2588
|
if (
|
|
2397
2589
|
os.path.exists(home_providers_path)
|
|
2398
2590
|
and (time.time() - os.path.getmtime(home_providers_path)) > 86400
|
|
2399
|
-
and os.
|
|
2591
|
+
and os.getenv("LLMS_DISABLE_UPDATE", "") != "1"
|
|
2400
2592
|
):
|
|
2401
2593
|
try:
|
|
2402
2594
|
asyncio.run(update_providers(home_providers_path))
|
|
@@ -2452,9 +2644,29 @@ def main():
|
|
|
2452
2644
|
requirements_path = os.path.join(target_path, "requirements.txt")
|
|
2453
2645
|
if os.path.exists(requirements_path):
|
|
2454
2646
|
print(f"Installing dependencies from {requirements_path}...")
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2647
|
+
|
|
2648
|
+
# Check if uv is installed
|
|
2649
|
+
has_uv = False
|
|
2650
|
+
try:
|
|
2651
|
+
subprocess.run(
|
|
2652
|
+
["uv", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True
|
|
2653
|
+
)
|
|
2654
|
+
has_uv = True
|
|
2655
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
2656
|
+
pass
|
|
2657
|
+
|
|
2658
|
+
if has_uv:
|
|
2659
|
+
subprocess.run(
|
|
2660
|
+
["uv", "pip", "install", "-p", sys.executable, "-r", "requirements.txt"],
|
|
2661
|
+
cwd=target_path,
|
|
2662
|
+
check=True,
|
|
2663
|
+
)
|
|
2664
|
+
else:
|
|
2665
|
+
subprocess.run(
|
|
2666
|
+
[sys.executable, "-m", "pip", "install", "-r", "requirements.txt"],
|
|
2667
|
+
cwd=target_path,
|
|
2668
|
+
check=True,
|
|
2669
|
+
)
|
|
2458
2670
|
print("Dependencies installed successfully.")
|
|
2459
2671
|
|
|
2460
2672
|
print(f"Extension {target_name} installed successfully.")
|
|
@@ -2533,12 +2745,10 @@ def main():
|
|
|
2533
2745
|
asyncio.run(update_extensions(cli_args.update))
|
|
2534
2746
|
exit(0)
|
|
2535
2747
|
|
|
2536
|
-
|
|
2748
|
+
install_extensions()
|
|
2537
2749
|
|
|
2538
2750
|
asyncio.run(reload_providers())
|
|
2539
2751
|
|
|
2540
|
-
install_extensions()
|
|
2541
|
-
|
|
2542
2752
|
# print names
|
|
2543
2753
|
_log(f"enabled providers: {', '.join(g_handlers.keys())}")
|
|
2544
2754
|
|
|
@@ -2594,6 +2804,7 @@ def main():
|
|
|
2594
2804
|
exit(0)
|
|
2595
2805
|
|
|
2596
2806
|
if cli_args.serve is not None:
|
|
2807
|
+
error_auth_required = create_error_response("Authentication required", "Unauthorized")
|
|
2597
2808
|
# Disable inactive providers and save to config before starting server
|
|
2598
2809
|
all_providers = g_config["providers"].keys()
|
|
2599
2810
|
enabled_providers = list(g_handlers.keys())
|
|
@@ -2624,8 +2835,8 @@ def main():
|
|
|
2624
2835
|
if client_secret.startswith("$"):
|
|
2625
2836
|
client_secret = client_secret[1:]
|
|
2626
2837
|
|
|
2627
|
-
client_id = os.
|
|
2628
|
-
client_secret = os.
|
|
2838
|
+
client_id = os.getenv(client_id, client_id)
|
|
2839
|
+
client_secret = os.getenv(client_secret, client_secret)
|
|
2629
2840
|
|
|
2630
2841
|
if (
|
|
2631
2842
|
not client_id
|
|
@@ -2670,35 +2881,15 @@ def main():
|
|
|
2670
2881
|
# Check authentication if enabled
|
|
2671
2882
|
is_authenticated, user_data = check_auth(request)
|
|
2672
2883
|
if not is_authenticated:
|
|
2673
|
-
return web.json_response(
|
|
2674
|
-
{
|
|
2675
|
-
"error": {
|
|
2676
|
-
"message": "Authentication required",
|
|
2677
|
-
"type": "authentication_error",
|
|
2678
|
-
"code": "unauthorized",
|
|
2679
|
-
}
|
|
2680
|
-
},
|
|
2681
|
-
status=401,
|
|
2682
|
-
)
|
|
2884
|
+
return web.json_response(error_auth_required, status=401)
|
|
2683
2885
|
|
|
2684
2886
|
try:
|
|
2685
2887
|
chat = await request.json()
|
|
2686
|
-
|
|
2687
|
-
# Apply pre-chat filters
|
|
2688
2888
|
context = {"request": request, "chat": chat}
|
|
2689
|
-
|
|
2690
|
-
chat = await filter_func(chat, context)
|
|
2691
|
-
|
|
2692
|
-
response = await chat_completion(chat)
|
|
2693
|
-
|
|
2694
|
-
# Apply post-chat filters
|
|
2695
|
-
# Apply post-chat filters
|
|
2696
|
-
for filter_func in g_app.chat_response_filters:
|
|
2697
|
-
response = await filter_func(response, context)
|
|
2698
|
-
|
|
2889
|
+
response = await g_app.chat_completion(chat, context)
|
|
2699
2890
|
return web.json_response(response)
|
|
2700
2891
|
except Exception as e:
|
|
2701
|
-
return web.json_response(
|
|
2892
|
+
return web.json_response(to_error_response(e), status=500)
|
|
2702
2893
|
|
|
2703
2894
|
app.router.add_post("/v1/chat/completions", chat_handler)
|
|
2704
2895
|
|
|
@@ -2752,16 +2943,7 @@ def main():
|
|
|
2752
2943
|
# Check authentication if enabled
|
|
2753
2944
|
is_authenticated, user_data = check_auth(request)
|
|
2754
2945
|
if not is_authenticated:
|
|
2755
|
-
return web.json_response(
|
|
2756
|
-
{
|
|
2757
|
-
"error": {
|
|
2758
|
-
"message": "Authentication required",
|
|
2759
|
-
"type": "authentication_error",
|
|
2760
|
-
"code": "unauthorized",
|
|
2761
|
-
}
|
|
2762
|
-
},
|
|
2763
|
-
status=401,
|
|
2764
|
-
)
|
|
2946
|
+
return web.json_response(error_auth_required, status=401)
|
|
2765
2947
|
|
|
2766
2948
|
reader = await request.multipart()
|
|
2767
2949
|
|
|
@@ -2771,7 +2953,7 @@ def main():
|
|
|
2771
2953
|
field = await reader.next()
|
|
2772
2954
|
|
|
2773
2955
|
if not field:
|
|
2774
|
-
return web.json_response(
|
|
2956
|
+
return web.json_response(create_error_response("No file provided"), status=400)
|
|
2775
2957
|
|
|
2776
2958
|
filename = field.filename or "file"
|
|
2777
2959
|
content = await field.read()
|
|
@@ -2809,9 +2991,10 @@ def main():
|
|
|
2809
2991
|
with open(full_path, "wb") as f:
|
|
2810
2992
|
f.write(content)
|
|
2811
2993
|
|
|
2994
|
+
url = f"/~cache/{relative_path}"
|
|
2812
2995
|
response_data = {
|
|
2813
2996
|
"date": int(time.time()),
|
|
2814
|
-
"url":
|
|
2997
|
+
"url": url,
|
|
2815
2998
|
"size": len(content),
|
|
2816
2999
|
"type": mimetype,
|
|
2817
3000
|
"name": filename,
|
|
@@ -2831,6 +3014,8 @@ def main():
|
|
|
2831
3014
|
with open(info_path, "w") as f:
|
|
2832
3015
|
json.dump(response_data, f)
|
|
2833
3016
|
|
|
3017
|
+
g_app.on_cache_saved_filters({"url": url, "info": response_data})
|
|
3018
|
+
|
|
2834
3019
|
return web.json_response(response_data)
|
|
2835
3020
|
|
|
2836
3021
|
app.router.add_post("/upload", upload_handler)
|
|
@@ -2856,7 +3041,7 @@ def main():
|
|
|
2856
3041
|
|
|
2857
3042
|
# Check for directory traversal for info path
|
|
2858
3043
|
try:
|
|
2859
|
-
cache_root = Path(get_cache_path(
|
|
3044
|
+
cache_root = Path(get_cache_path())
|
|
2860
3045
|
requested_path = Path(info_path).resolve()
|
|
2861
3046
|
if not str(requested_path).startswith(str(cache_root)):
|
|
2862
3047
|
return web.Response(text="403: Forbidden", status=403)
|
|
@@ -2872,7 +3057,7 @@ def main():
|
|
|
2872
3057
|
|
|
2873
3058
|
# Check for directory traversal
|
|
2874
3059
|
try:
|
|
2875
|
-
cache_root = Path(get_cache_path(
|
|
3060
|
+
cache_root = Path(get_cache_path())
|
|
2876
3061
|
requested_path = Path(full_path).resolve()
|
|
2877
3062
|
if not str(requested_path).startswith(str(cache_root)):
|
|
2878
3063
|
return web.Response(text="403: Forbidden", status=403)
|
|
@@ -2891,7 +3076,7 @@ def main():
|
|
|
2891
3076
|
async def github_auth_handler(request):
|
|
2892
3077
|
"""Initiate GitHub OAuth flow"""
|
|
2893
3078
|
if "auth" not in g_config or "github" not in g_config["auth"]:
|
|
2894
|
-
return web.json_response(
|
|
3079
|
+
return web.json_response(create_error_response("GitHub OAuth not configured"), status=500)
|
|
2895
3080
|
|
|
2896
3081
|
auth_config = g_config["auth"]["github"]
|
|
2897
3082
|
client_id = auth_config.get("client_id", "")
|
|
@@ -2903,11 +3088,11 @@ def main():
|
|
|
2903
3088
|
if redirect_uri.startswith("$"):
|
|
2904
3089
|
redirect_uri = redirect_uri[1:]
|
|
2905
3090
|
|
|
2906
|
-
client_id = os.
|
|
2907
|
-
redirect_uri = os.
|
|
3091
|
+
client_id = os.getenv(client_id, client_id)
|
|
3092
|
+
redirect_uri = os.getenv(redirect_uri, redirect_uri)
|
|
2908
3093
|
|
|
2909
3094
|
if not client_id:
|
|
2910
|
-
return web.json_response(
|
|
3095
|
+
return web.json_response(create_error_response("GitHub client_id not configured"), status=500)
|
|
2911
3096
|
|
|
2912
3097
|
# Generate CSRF state token
|
|
2913
3098
|
state = secrets.token_urlsafe(32)
|
|
@@ -2939,7 +3124,7 @@ def main():
|
|
|
2939
3124
|
if restrict_to.startswith("$"):
|
|
2940
3125
|
restrict_to = restrict_to[1:]
|
|
2941
3126
|
|
|
2942
|
-
restrict_to = os.
|
|
3127
|
+
restrict_to = os.getenv(restrict_to, None if restrict_to == "GITHUB_USERS" else restrict_to)
|
|
2943
3128
|
|
|
2944
3129
|
# If restrict_to is configured, validate the user
|
|
2945
3130
|
if restrict_to:
|
|
@@ -2978,7 +3163,7 @@ def main():
|
|
|
2978
3163
|
g_oauth_states.pop(state)
|
|
2979
3164
|
|
|
2980
3165
|
if "auth" not in g_config or "github" not in g_config["auth"]:
|
|
2981
|
-
return web.json_response(
|
|
3166
|
+
return web.json_response(create_error_response("GitHub OAuth not configured"), status=500)
|
|
2982
3167
|
|
|
2983
3168
|
auth_config = g_config["auth"]["github"]
|
|
2984
3169
|
client_id = auth_config.get("client_id", "")
|
|
@@ -2993,12 +3178,12 @@ def main():
|
|
|
2993
3178
|
if redirect_uri.startswith("$"):
|
|
2994
3179
|
redirect_uri = redirect_uri[1:]
|
|
2995
3180
|
|
|
2996
|
-
client_id = os.
|
|
2997
|
-
client_secret = os.
|
|
2998
|
-
redirect_uri = os.
|
|
3181
|
+
client_id = os.getenv(client_id, client_id)
|
|
3182
|
+
client_secret = os.getenv(client_secret, client_secret)
|
|
3183
|
+
redirect_uri = os.getenv(redirect_uri, redirect_uri)
|
|
2999
3184
|
|
|
3000
3185
|
if not client_id or not client_secret:
|
|
3001
|
-
return web.json_response(
|
|
3186
|
+
return web.json_response(create_error_response("GitHub OAuth credentials not configured"), status=500)
|
|
3002
3187
|
|
|
3003
3188
|
# Exchange code for access token
|
|
3004
3189
|
async with aiohttp.ClientSession() as session:
|
|
@@ -3017,7 +3202,7 @@ def main():
|
|
|
3017
3202
|
|
|
3018
3203
|
if not access_token:
|
|
3019
3204
|
error = token_response.get("error_description", "Failed to get access token")
|
|
3020
|
-
return web.
|
|
3205
|
+
return web.json_response(create_error_response(f"OAuth error: {error}"), status=400)
|
|
3021
3206
|
|
|
3022
3207
|
# Fetch user info
|
|
3023
3208
|
user_url = "https://api.github.com/user"
|
|
@@ -3052,7 +3237,7 @@ def main():
|
|
|
3052
3237
|
session_token = get_session_token(request)
|
|
3053
3238
|
|
|
3054
3239
|
if not session_token or session_token not in g_sessions:
|
|
3055
|
-
return web.json_response(
|
|
3240
|
+
return web.json_response(create_error_response("Invalid or expired session"), status=401)
|
|
3056
3241
|
|
|
3057
3242
|
session_data = g_sessions[session_token]
|
|
3058
3243
|
|
|
@@ -3108,9 +3293,7 @@ def main():
|
|
|
3108
3293
|
# })
|
|
3109
3294
|
|
|
3110
3295
|
# Not authenticated - return error in expected format
|
|
3111
|
-
return web.json_response(
|
|
3112
|
-
{"responseStatus": {"errorCode": "Unauthorized", "message": "Not authenticated"}}, status=401
|
|
3113
|
-
)
|
|
3296
|
+
return web.json_response(error_auth_required, status=401)
|
|
3114
3297
|
|
|
3115
3298
|
app.router.add_get("/auth", auth_handler)
|
|
3116
3299
|
app.router.add_get("/auth/github", github_auth_handler)
|
|
@@ -3170,9 +3353,55 @@ def main():
|
|
|
3170
3353
|
|
|
3171
3354
|
# go through and register all g_app extensions
|
|
3172
3355
|
for handler in g_app.server_add_get:
|
|
3173
|
-
|
|
3356
|
+
handler_fn = handler[1]
|
|
3357
|
+
|
|
3358
|
+
async def managed_handler(request, handler_fn=handler_fn):
|
|
3359
|
+
try:
|
|
3360
|
+
return await handler_fn(request)
|
|
3361
|
+
except Exception as e:
|
|
3362
|
+
return web.json_response(to_error_response(e, stacktrace=g_verbose), status=500)
|
|
3363
|
+
|
|
3364
|
+
app.router.add_get(handler[0], managed_handler, **handler[2])
|
|
3174
3365
|
for handler in g_app.server_add_post:
|
|
3175
|
-
|
|
3366
|
+
handler_fn = handler[1]
|
|
3367
|
+
|
|
3368
|
+
async def managed_handler(request, handler_fn=handler_fn):
|
|
3369
|
+
try:
|
|
3370
|
+
return await handler_fn(request)
|
|
3371
|
+
except Exception as e:
|
|
3372
|
+
return web.json_response(to_error_response(e, stacktrace=g_verbose), status=500)
|
|
3373
|
+
|
|
3374
|
+
app.router.add_post(handler[0], managed_handler, **handler[2])
|
|
3375
|
+
for handler in g_app.server_add_put:
|
|
3376
|
+
handler_fn = handler[1]
|
|
3377
|
+
|
|
3378
|
+
async def managed_handler(request, handler_fn=handler_fn):
|
|
3379
|
+
try:
|
|
3380
|
+
return await handler_fn(request)
|
|
3381
|
+
except Exception as e:
|
|
3382
|
+
return web.json_response(to_error_response(e, stacktrace=g_verbose), status=500)
|
|
3383
|
+
|
|
3384
|
+
app.router.add_put(handler[0], managed_handler, **handler[2])
|
|
3385
|
+
for handler in g_app.server_add_delete:
|
|
3386
|
+
handler_fn = handler[1]
|
|
3387
|
+
|
|
3388
|
+
async def managed_handler(request, handler_fn=handler_fn):
|
|
3389
|
+
try:
|
|
3390
|
+
return await handler_fn(request)
|
|
3391
|
+
except Exception as e:
|
|
3392
|
+
return web.json_response(to_error_response(e, stacktrace=g_verbose), status=500)
|
|
3393
|
+
|
|
3394
|
+
app.router.add_delete(handler[0], managed_handler, **handler[2])
|
|
3395
|
+
for handler in g_app.server_add_patch:
|
|
3396
|
+
handler_fn = handler[1]
|
|
3397
|
+
|
|
3398
|
+
async def managed_handler(request, handler_fn=handler_fn):
|
|
3399
|
+
try:
|
|
3400
|
+
return await handler_fn(request)
|
|
3401
|
+
except Exception as e:
|
|
3402
|
+
return web.json_response(to_error_response(e, stacktrace=g_verbose), status=500)
|
|
3403
|
+
|
|
3404
|
+
app.router.add_patch(handler[0], managed_handler, **handler[2])
|
|
3176
3405
|
|
|
3177
3406
|
# Serve index.html from root
|
|
3178
3407
|
async def index_handler(request):
|
|
@@ -3215,7 +3444,7 @@ def main():
|
|
|
3215
3444
|
|
|
3216
3445
|
for provider in enable_providers:
|
|
3217
3446
|
if provider not in g_config["providers"]:
|
|
3218
|
-
print(f"Provider {provider} not found")
|
|
3447
|
+
print(f"Provider '{provider}' not found")
|
|
3219
3448
|
print(f"Available providers: {', '.join(g_config['providers'].keys())}")
|
|
3220
3449
|
exit(1)
|
|
3221
3450
|
if provider in g_config["providers"]:
|
|
@@ -3303,6 +3532,9 @@ def main():
|
|
|
3303
3532
|
|
|
3304
3533
|
if len(extra_args) > 0:
|
|
3305
3534
|
prompt = " ".join(extra_args)
|
|
3535
|
+
if not chat["messages"] or len(chat["messages"]) == 0:
|
|
3536
|
+
chat["messages"] = [{"role": "user", "content": [{"type": "text", "text": ""}]}]
|
|
3537
|
+
|
|
3306
3538
|
# replace content of last message if exists, else add
|
|
3307
3539
|
last_msg = chat["messages"][-1] if "messages" in chat else None
|
|
3308
3540
|
if last_msg and last_msg["role"] == "user":
|