llms-py 3.0.0b6__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 +381 -170
- 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.0b6.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.0b6.dist-info/RECORD +0 -66
- {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b7.dist-info}/WHEEL +0 -0
- {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b7.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b7.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.0b6.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}"
|
|
508
509
|
|
|
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()
|
|
533
|
+
|
|
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)]
|
|
@@ -1218,16 +1314,8 @@ async def cli_chat(chat, image=None, audio=None, file=None, args=None, raw=False
|
|
|
1218
1314
|
printdump(chat)
|
|
1219
1315
|
|
|
1220
1316
|
try:
|
|
1221
|
-
|
|
1222
|
-
context = {"chat": chat}
|
|
1223
|
-
for filter_func in g_app.chat_request_filters:
|
|
1224
|
-
chat = await filter_func(chat, context)
|
|
1317
|
+
response = await g_app.chat_completion(chat)
|
|
1225
1318
|
|
|
1226
|
-
response = await chat_completion(chat)
|
|
1227
|
-
|
|
1228
|
-
# Apply post-chat filters
|
|
1229
|
-
for filter_func in g_app.chat_response_filters:
|
|
1230
|
-
response = await filter_func(response, context)
|
|
1231
1319
|
if raw:
|
|
1232
1320
|
print(json.dumps(response, indent=2))
|
|
1233
1321
|
exit(0)
|
|
@@ -1244,13 +1332,17 @@ async def cli_chat(chat, image=None, audio=None, file=None, args=None, raw=False
|
|
|
1244
1332
|
for image in msg["images"]:
|
|
1245
1333
|
image_url = image["image_url"]["url"]
|
|
1246
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)
|
|
1247
1339
|
|
|
1248
1340
|
if len(generated_files) > 0:
|
|
1249
1341
|
print("\nSaved files:")
|
|
1250
1342
|
for file in generated_files:
|
|
1251
|
-
if file.startswith("
|
|
1252
|
-
print(get_cache_path(file[
|
|
1253
|
-
|
|
1343
|
+
if file.startswith("/~cache"):
|
|
1344
|
+
print(get_cache_path(file[8:]))
|
|
1345
|
+
print(f"http://localhost:8000/{file}")
|
|
1254
1346
|
else:
|
|
1255
1347
|
print(file)
|
|
1256
1348
|
|
|
@@ -1288,7 +1380,7 @@ def init_llms(config, providers):
|
|
|
1288
1380
|
# iterate over config and replace $ENV with env value
|
|
1289
1381
|
for key, value in g_config.items():
|
|
1290
1382
|
if isinstance(value, str) and value.startswith("$"):
|
|
1291
|
-
g_config[key] = os.
|
|
1383
|
+
g_config[key] = os.getenv(value[1:], "")
|
|
1292
1384
|
|
|
1293
1385
|
# if g_verbose:
|
|
1294
1386
|
# printdump(g_config)
|
|
@@ -1326,11 +1418,11 @@ def create_provider_kwargs(definition, provider=None):
|
|
|
1326
1418
|
if "api_key" in provider:
|
|
1327
1419
|
value = provider["api_key"]
|
|
1328
1420
|
if isinstance(value, str) and value.startswith("$"):
|
|
1329
|
-
provider["api_key"] = os.
|
|
1421
|
+
provider["api_key"] = os.getenv(value[1:], "")
|
|
1330
1422
|
|
|
1331
1423
|
if "api_key" not in provider and "env" in provider:
|
|
1332
1424
|
for env_var in provider["env"]:
|
|
1333
|
-
val = os.
|
|
1425
|
+
val = os.getenv(env_var)
|
|
1334
1426
|
if val:
|
|
1335
1427
|
provider["api_key"] = val
|
|
1336
1428
|
break
|
|
@@ -1467,11 +1559,11 @@ def print_status():
|
|
|
1467
1559
|
|
|
1468
1560
|
|
|
1469
1561
|
def home_llms_path(filename):
|
|
1470
|
-
return f"{os.
|
|
1562
|
+
return f"{os.getenv('HOME')}/.llms/{filename}"
|
|
1471
1563
|
|
|
1472
1564
|
|
|
1473
|
-
def get_cache_path(
|
|
1474
|
-
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")
|
|
1475
1567
|
|
|
1476
1568
|
|
|
1477
1569
|
def get_config_path():
|
|
@@ -1480,8 +1572,8 @@ def get_config_path():
|
|
|
1480
1572
|
"./llms.json",
|
|
1481
1573
|
home_config_path,
|
|
1482
1574
|
]
|
|
1483
|
-
if os.
|
|
1484
|
-
check_paths.insert(0, os.
|
|
1575
|
+
if os.getenv("LLMS_CONFIG_PATH"):
|
|
1576
|
+
check_paths.insert(0, os.getenv("LLMS_CONFIG_PATH"))
|
|
1485
1577
|
|
|
1486
1578
|
for check_path in check_paths:
|
|
1487
1579
|
g_config_path = os.path.normpath(os.path.join(os.path.dirname(__file__), check_path))
|
|
@@ -1956,7 +2048,10 @@ class AppExtensions:
|
|
|
1956
2048
|
self.chat_response_filters = []
|
|
1957
2049
|
self.server_add_get = []
|
|
1958
2050
|
self.server_add_post = []
|
|
1959
|
-
self.
|
|
2051
|
+
self.server_add_put = []
|
|
2052
|
+
self.server_add_delete = []
|
|
2053
|
+
self.server_add_patch = []
|
|
2054
|
+
self.cache_saved_filters = []
|
|
1960
2055
|
self.tools = {}
|
|
1961
2056
|
self.tool_definitions = []
|
|
1962
2057
|
self.all_providers = [
|
|
@@ -1981,6 +2076,62 @@ class AppExtensions:
|
|
|
1981
2076
|
"21:9": "1536×672",
|
|
1982
2077
|
}
|
|
1983
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
|
+
|
|
1984
2135
|
|
|
1985
2136
|
class ExtensionContext:
|
|
1986
2137
|
def __init__(self, app, path):
|
|
@@ -1994,6 +2145,7 @@ class ExtensionContext:
|
|
|
1994
2145
|
self.MOCK_DIR = MOCK_DIR
|
|
1995
2146
|
self.debug = DEBUG
|
|
1996
2147
|
self.verbose = g_verbose
|
|
2148
|
+
self.aspect_ratios = app.aspect_ratios
|
|
1997
2149
|
|
|
1998
2150
|
def chat_to_prompt(self, chat):
|
|
1999
2151
|
return chat_to_prompt(chat)
|
|
@@ -2001,9 +2153,15 @@ class ExtensionContext:
|
|
|
2001
2153
|
def last_user_prompt(self, chat):
|
|
2002
2154
|
return last_user_prompt(chat)
|
|
2003
2155
|
|
|
2156
|
+
def to_file_info(self, chat, info=None, response=None):
|
|
2157
|
+
return to_file_info(chat, info=info, response=response)
|
|
2158
|
+
|
|
2004
2159
|
def save_image_to_cache(self, base64_data, filename, image_info):
|
|
2005
2160
|
return save_image_to_cache(base64_data, filename, image_info)
|
|
2006
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
|
+
|
|
2007
2165
|
def text_from_file(self, path):
|
|
2008
2166
|
return text_from_file(path)
|
|
2009
2167
|
|
|
@@ -2027,7 +2185,7 @@ class ExtensionContext:
|
|
|
2027
2185
|
print(traceback.format_exc(), flush=True)
|
|
2028
2186
|
|
|
2029
2187
|
def add_provider(self, provider):
|
|
2030
|
-
self.log(f"Registered provider: {provider}")
|
|
2188
|
+
self.log(f"Registered provider: {provider.__name__}")
|
|
2031
2189
|
self.app.all_providers.append(provider)
|
|
2032
2190
|
|
|
2033
2191
|
def register_ui_extension(self, index):
|
|
@@ -2036,13 +2194,17 @@ class ExtensionContext:
|
|
|
2036
2194
|
self.app.ui_extensions.append({"id": self.name, "path": path})
|
|
2037
2195
|
|
|
2038
2196
|
def register_chat_request_filter(self, handler):
|
|
2039
|
-
self.log(f"Registered chat request filter: {handler}")
|
|
2197
|
+
self.log(f"Registered chat request filter: {handler_name(handler)}")
|
|
2040
2198
|
self.app.chat_request_filters.append(handler)
|
|
2041
2199
|
|
|
2042
2200
|
def register_chat_response_filter(self, handler):
|
|
2043
|
-
self.log(f"Registered chat response filter: {handler}")
|
|
2201
|
+
self.log(f"Registered chat response filter: {handler_name(handler)}")
|
|
2044
2202
|
self.app.chat_response_filters.append(handler)
|
|
2045
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
|
+
|
|
2046
2208
|
def add_static_files(self, ext_dir):
|
|
2047
2209
|
self.log(f"Registered static files: {ext_dir}")
|
|
2048
2210
|
|
|
@@ -2063,11 +2225,29 @@ class ExtensionContext:
|
|
|
2063
2225
|
self.dbg(f"Registered POST: {os.path.join(self.ext_prefix, path)}")
|
|
2064
2226
|
self.app.server_add_post.append((os.path.join(self.ext_prefix, path), handler, kwargs))
|
|
2065
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
|
+
|
|
2066
2240
|
def get_config(self):
|
|
2067
2241
|
return g_config
|
|
2068
2242
|
|
|
2069
|
-
def
|
|
2070
|
-
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)
|
|
2071
2251
|
|
|
2072
2252
|
def get_providers(self):
|
|
2073
2253
|
return g_handlers
|
|
@@ -2075,21 +2255,6 @@ class ExtensionContext:
|
|
|
2075
2255
|
def get_provider(self, name):
|
|
2076
2256
|
return g_handlers.get(name)
|
|
2077
2257
|
|
|
2078
|
-
def get_session(self, request):
|
|
2079
|
-
session_token = get_session_token(request)
|
|
2080
|
-
|
|
2081
|
-
if not session_token or session_token not in g_sessions:
|
|
2082
|
-
return None
|
|
2083
|
-
|
|
2084
|
-
session_data = g_sessions[session_token]
|
|
2085
|
-
return session_data
|
|
2086
|
-
|
|
2087
|
-
def get_username(self, request):
|
|
2088
|
-
session = self.get_session(request)
|
|
2089
|
-
if session:
|
|
2090
|
-
return session.get("userName")
|
|
2091
|
-
return None
|
|
2092
|
-
|
|
2093
2258
|
def register_tool(self, func, tool_def=None):
|
|
2094
2259
|
if tool_def is None:
|
|
2095
2260
|
tool_def = function_to_tool_definition(func)
|
|
@@ -2099,44 +2264,71 @@ class ExtensionContext:
|
|
|
2099
2264
|
self.app.tools[name] = func
|
|
2100
2265
|
self.app.tool_definitions.append(tool_def)
|
|
2101
2266
|
|
|
2267
|
+
def get_session(self, request):
|
|
2268
|
+
return self.app.get_session(request)
|
|
2102
2269
|
|
|
2103
|
-
def
|
|
2104
|
-
|
|
2105
|
-
if not providers_path.exists():
|
|
2106
|
-
return
|
|
2270
|
+
def get_username(self, request):
|
|
2271
|
+
return self.app.get_username(request)
|
|
2107
2272
|
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
continue
|
|
2273
|
+
def get_user_path(self, username=None):
|
|
2274
|
+
return self.app.get_user_path(username)
|
|
2111
2275
|
|
|
2112
|
-
item_path = providers_path / item
|
|
2113
|
-
module_name = item[:-3]
|
|
2114
2276
|
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
if spec and spec.loader:
|
|
2118
|
-
module = importlib.util.module_from_spec(spec)
|
|
2119
|
-
sys.modules[f"llms.providers.{module_name}"] = module
|
|
2120
|
-
spec.loader.exec_module(module)
|
|
2121
|
-
|
|
2122
|
-
install_func = getattr(module, "__install__", None)
|
|
2123
|
-
if callable(install_func):
|
|
2124
|
-
install_func(ExtensionContext(g_app, item_path))
|
|
2125
|
-
_log(f"Loaded builtin extension: {module_name}")
|
|
2126
|
-
except Exception as e:
|
|
2127
|
-
_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"))
|
|
2128
2279
|
|
|
2129
2280
|
|
|
2130
|
-
def
|
|
2131
|
-
|
|
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
|
|
2132
2288
|
|
|
2133
2289
|
|
|
2134
|
-
def
|
|
2290
|
+
def get_extensions_dirs():
|
|
2291
|
+
"""
|
|
2292
|
+
Returns a list of extension directories.
|
|
2293
|
+
"""
|
|
2135
2294
|
extensions_path = get_extensions_path()
|
|
2136
2295
|
os.makedirs(extensions_path, exist_ok=True)
|
|
2137
2296
|
|
|
2138
|
-
|
|
2139
|
-
|
|
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
|
+
|
|
2140
2332
|
if os.path.isdir(item_path):
|
|
2141
2333
|
try:
|
|
2142
2334
|
# check for __parser__ function if exists in __init.__.py and call it with parser
|
|
@@ -2161,25 +2353,28 @@ def install_extensions():
|
|
|
2161
2353
|
Scans ensure ~/.llms/extensions/ for directories with __init__.py and loads them as extensions.
|
|
2162
2354
|
Calls the `__install__(ctx)` function in the extension module.
|
|
2163
2355
|
"""
|
|
2164
|
-
extensions_path = get_extensions_path()
|
|
2165
|
-
os.makedirs(extensions_path, exist_ok=True)
|
|
2166
2356
|
|
|
2167
|
-
|
|
2357
|
+
extension_dirs = get_extensions_dirs()
|
|
2358
|
+
ext_count = len(list(extension_dirs))
|
|
2168
2359
|
if ext_count == 0:
|
|
2169
2360
|
_log("No extensions found")
|
|
2170
2361
|
return
|
|
2171
2362
|
|
|
2363
|
+
disabled_extensions = get_disabled_extensions()
|
|
2364
|
+
if len(disabled_extensions) > 0:
|
|
2365
|
+
_log(f"Disabled extensions: {', '.join(disabled_extensions)}")
|
|
2366
|
+
|
|
2172
2367
|
_log(f"Installing {ext_count} extension{'' if ext_count == 1 else 's'}...")
|
|
2173
2368
|
|
|
2174
|
-
|
|
2369
|
+
for item_path in extension_dirs:
|
|
2370
|
+
item = os.path.basename(item_path)
|
|
2175
2371
|
|
|
2176
|
-
for item in os.listdir(extensions_path):
|
|
2177
|
-
item_path = os.path.join(extensions_path, item)
|
|
2178
2372
|
if os.path.isdir(item_path):
|
|
2179
|
-
|
|
2180
|
-
|
|
2373
|
+
sys.path.append(item_path)
|
|
2374
|
+
try:
|
|
2181
2375
|
ctx = ExtensionContext(g_app, item_path)
|
|
2182
|
-
|
|
2376
|
+
init_file = os.path.join(item_path, "__init__.py")
|
|
2377
|
+
if os.path.exists(init_file):
|
|
2183
2378
|
spec = importlib.util.spec_from_file_location(item, init_file)
|
|
2184
2379
|
if spec and spec.loader:
|
|
2185
2380
|
module = importlib.util.module_from_spec(spec)
|
|
@@ -2194,20 +2389,20 @@ def install_extensions():
|
|
|
2194
2389
|
_dbg(f"Extension {item} has no __install__ function")
|
|
2195
2390
|
else:
|
|
2196
2391
|
_dbg(f"Extension {item} has no __init__.py")
|
|
2392
|
+
else:
|
|
2393
|
+
_dbg(f"Extension {init_file} not found")
|
|
2197
2394
|
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
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)
|
|
2202
2399
|
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
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")
|
|
2206
2403
|
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
else:
|
|
2210
|
-
_dbg(f"Extension {init_file} not found")
|
|
2404
|
+
except Exception as e:
|
|
2405
|
+
_err(f"Failed to install extension {item}", e)
|
|
2211
2406
|
else:
|
|
2212
2407
|
_dbg(f"Extension {item} not found: {item_path} is not a directory {os.path.exists(item_path)}")
|
|
2213
2408
|
|
|
@@ -2216,11 +2411,9 @@ def run_extension_cli():
|
|
|
2216
2411
|
"""
|
|
2217
2412
|
Run the CLI for an extension.
|
|
2218
2413
|
"""
|
|
2219
|
-
|
|
2220
|
-
|
|
2414
|
+
for item_path in get_extensions_dirs():
|
|
2415
|
+
item = os.path.basename(item_path)
|
|
2221
2416
|
|
|
2222
|
-
for item in os.listdir(extensions_path):
|
|
2223
|
-
item_path = os.path.join(extensions_path, item)
|
|
2224
2417
|
if os.path.isdir(item_path):
|
|
2225
2418
|
init_file = os.path.join(item_path, "__init__.py")
|
|
2226
2419
|
if os.path.exists(init_file):
|
|
@@ -2235,8 +2428,8 @@ def run_extension_cli():
|
|
|
2235
2428
|
# Check for __run__ function if exists in __init__.py and call it with ctx
|
|
2236
2429
|
run_func = getattr(module, "__run__", None)
|
|
2237
2430
|
if callable(run_func):
|
|
2431
|
+
_log(f"Running extension {item}...")
|
|
2238
2432
|
handled = run_func(ctx)
|
|
2239
|
-
_log(f"Extension {item} was run")
|
|
2240
2433
|
return handled
|
|
2241
2434
|
|
|
2242
2435
|
except Exception as e:
|
|
@@ -2247,6 +2440,11 @@ def run_extension_cli():
|
|
|
2247
2440
|
def main():
|
|
2248
2441
|
global _ROOT, g_verbose, g_default_model, g_logprefix, g_providers, g_config, g_config_path, g_app
|
|
2249
2442
|
|
|
2443
|
+
_ROOT = os.getenv("LLMS_ROOT", resolve_root())
|
|
2444
|
+
if not _ROOT:
|
|
2445
|
+
print("Resource root not found")
|
|
2446
|
+
exit(1)
|
|
2447
|
+
|
|
2250
2448
|
parser = argparse.ArgumentParser(description=f"llms v{VERSION}")
|
|
2251
2449
|
parser.add_argument("--config", default=None, help="Path to config file", metavar="FILE")
|
|
2252
2450
|
parser.add_argument("--providers", default=None, help="Path to models.dev providers file", metavar="FILE")
|
|
@@ -2283,9 +2481,7 @@ def main():
|
|
|
2283
2481
|
|
|
2284
2482
|
parser.add_argument("--init", action="store_true", help="Create a default llms.json")
|
|
2285
2483
|
parser.add_argument("--update-providers", action="store_true", help="Update local models.dev providers.json")
|
|
2286
|
-
parser.add_argument("--update-extensions", action="store_true", help="Update installed extensions")
|
|
2287
2484
|
|
|
2288
|
-
parser.add_argument("--root", default=None, help="Change root directory for UI files", metavar="PATH")
|
|
2289
2485
|
parser.add_argument("--logprefix", default="", help="Prefix used in log messages", metavar="PREFIX")
|
|
2290
2486
|
parser.add_argument("--verbose", action="store_true", help="Verbose output")
|
|
2291
2487
|
|
|
@@ -2323,7 +2519,7 @@ def main():
|
|
|
2323
2519
|
g_app = AppExtensions(cli_args, extra_args)
|
|
2324
2520
|
|
|
2325
2521
|
# Check for verbose mode from CLI argument or environment variables
|
|
2326
|
-
verbose_env = os.
|
|
2522
|
+
verbose_env = os.getenv("VERBOSE", "").lower()
|
|
2327
2523
|
if cli_args.verbose or verbose_env in ("1", "true"):
|
|
2328
2524
|
g_verbose = True
|
|
2329
2525
|
# printdump(cli_args)
|
|
@@ -2332,11 +2528,6 @@ def main():
|
|
|
2332
2528
|
if cli_args.logprefix:
|
|
2333
2529
|
g_logprefix = cli_args.logprefix
|
|
2334
2530
|
|
|
2335
|
-
_ROOT = Path(cli_args.root) if cli_args.root else resolve_root()
|
|
2336
|
-
if not _ROOT:
|
|
2337
|
-
print("Resource root not found")
|
|
2338
|
-
exit(1)
|
|
2339
|
-
|
|
2340
2531
|
home_config_path = home_llms_path("llms.json")
|
|
2341
2532
|
home_providers_path = home_llms_path("providers.json")
|
|
2342
2533
|
home_providers_extra_path = home_llms_path("providers-extra.json")
|
|
@@ -2397,7 +2588,7 @@ def main():
|
|
|
2397
2588
|
if (
|
|
2398
2589
|
os.path.exists(home_providers_path)
|
|
2399
2590
|
and (time.time() - os.path.getmtime(home_providers_path)) > 86400
|
|
2400
|
-
and os.
|
|
2591
|
+
and os.getenv("LLMS_DISABLE_UPDATE", "") != "1"
|
|
2401
2592
|
):
|
|
2402
2593
|
try:
|
|
2403
2594
|
asyncio.run(update_providers(home_providers_path))
|
|
@@ -2554,12 +2745,10 @@ def main():
|
|
|
2554
2745
|
asyncio.run(update_extensions(cli_args.update))
|
|
2555
2746
|
exit(0)
|
|
2556
2747
|
|
|
2557
|
-
|
|
2748
|
+
install_extensions()
|
|
2558
2749
|
|
|
2559
2750
|
asyncio.run(reload_providers())
|
|
2560
2751
|
|
|
2561
|
-
install_extensions()
|
|
2562
|
-
|
|
2563
2752
|
# print names
|
|
2564
2753
|
_log(f"enabled providers: {', '.join(g_handlers.keys())}")
|
|
2565
2754
|
|
|
@@ -2615,6 +2804,7 @@ def main():
|
|
|
2615
2804
|
exit(0)
|
|
2616
2805
|
|
|
2617
2806
|
if cli_args.serve is not None:
|
|
2807
|
+
error_auth_required = create_error_response("Authentication required", "Unauthorized")
|
|
2618
2808
|
# Disable inactive providers and save to config before starting server
|
|
2619
2809
|
all_providers = g_config["providers"].keys()
|
|
2620
2810
|
enabled_providers = list(g_handlers.keys())
|
|
@@ -2645,8 +2835,8 @@ def main():
|
|
|
2645
2835
|
if client_secret.startswith("$"):
|
|
2646
2836
|
client_secret = client_secret[1:]
|
|
2647
2837
|
|
|
2648
|
-
client_id = os.
|
|
2649
|
-
client_secret = os.
|
|
2838
|
+
client_id = os.getenv(client_id, client_id)
|
|
2839
|
+
client_secret = os.getenv(client_secret, client_secret)
|
|
2650
2840
|
|
|
2651
2841
|
if (
|
|
2652
2842
|
not client_id
|
|
@@ -2691,35 +2881,15 @@ def main():
|
|
|
2691
2881
|
# Check authentication if enabled
|
|
2692
2882
|
is_authenticated, user_data = check_auth(request)
|
|
2693
2883
|
if not is_authenticated:
|
|
2694
|
-
return web.json_response(
|
|
2695
|
-
{
|
|
2696
|
-
"error": {
|
|
2697
|
-
"message": "Authentication required",
|
|
2698
|
-
"type": "authentication_error",
|
|
2699
|
-
"code": "unauthorized",
|
|
2700
|
-
}
|
|
2701
|
-
},
|
|
2702
|
-
status=401,
|
|
2703
|
-
)
|
|
2884
|
+
return web.json_response(error_auth_required, status=401)
|
|
2704
2885
|
|
|
2705
2886
|
try:
|
|
2706
2887
|
chat = await request.json()
|
|
2707
|
-
|
|
2708
|
-
# Apply pre-chat filters
|
|
2709
2888
|
context = {"request": request, "chat": chat}
|
|
2710
|
-
|
|
2711
|
-
chat = await filter_func(chat, context)
|
|
2712
|
-
|
|
2713
|
-
response = await chat_completion(chat)
|
|
2714
|
-
|
|
2715
|
-
# Apply post-chat filters
|
|
2716
|
-
# Apply post-chat filters
|
|
2717
|
-
for filter_func in g_app.chat_response_filters:
|
|
2718
|
-
response = await filter_func(response, context)
|
|
2719
|
-
|
|
2889
|
+
response = await g_app.chat_completion(chat, context)
|
|
2720
2890
|
return web.json_response(response)
|
|
2721
2891
|
except Exception as e:
|
|
2722
|
-
return web.json_response(
|
|
2892
|
+
return web.json_response(to_error_response(e), status=500)
|
|
2723
2893
|
|
|
2724
2894
|
app.router.add_post("/v1/chat/completions", chat_handler)
|
|
2725
2895
|
|
|
@@ -2773,16 +2943,7 @@ def main():
|
|
|
2773
2943
|
# Check authentication if enabled
|
|
2774
2944
|
is_authenticated, user_data = check_auth(request)
|
|
2775
2945
|
if not is_authenticated:
|
|
2776
|
-
return web.json_response(
|
|
2777
|
-
{
|
|
2778
|
-
"error": {
|
|
2779
|
-
"message": "Authentication required",
|
|
2780
|
-
"type": "authentication_error",
|
|
2781
|
-
"code": "unauthorized",
|
|
2782
|
-
}
|
|
2783
|
-
},
|
|
2784
|
-
status=401,
|
|
2785
|
-
)
|
|
2946
|
+
return web.json_response(error_auth_required, status=401)
|
|
2786
2947
|
|
|
2787
2948
|
reader = await request.multipart()
|
|
2788
2949
|
|
|
@@ -2792,7 +2953,7 @@ def main():
|
|
|
2792
2953
|
field = await reader.next()
|
|
2793
2954
|
|
|
2794
2955
|
if not field:
|
|
2795
|
-
return web.json_response(
|
|
2956
|
+
return web.json_response(create_error_response("No file provided"), status=400)
|
|
2796
2957
|
|
|
2797
2958
|
filename = field.filename or "file"
|
|
2798
2959
|
content = await field.read()
|
|
@@ -2830,9 +2991,10 @@ def main():
|
|
|
2830
2991
|
with open(full_path, "wb") as f:
|
|
2831
2992
|
f.write(content)
|
|
2832
2993
|
|
|
2994
|
+
url = f"/~cache/{relative_path}"
|
|
2833
2995
|
response_data = {
|
|
2834
2996
|
"date": int(time.time()),
|
|
2835
|
-
"url":
|
|
2997
|
+
"url": url,
|
|
2836
2998
|
"size": len(content),
|
|
2837
2999
|
"type": mimetype,
|
|
2838
3000
|
"name": filename,
|
|
@@ -2852,6 +3014,8 @@ def main():
|
|
|
2852
3014
|
with open(info_path, "w") as f:
|
|
2853
3015
|
json.dump(response_data, f)
|
|
2854
3016
|
|
|
3017
|
+
g_app.on_cache_saved_filters({"url": url, "info": response_data})
|
|
3018
|
+
|
|
2855
3019
|
return web.json_response(response_data)
|
|
2856
3020
|
|
|
2857
3021
|
app.router.add_post("/upload", upload_handler)
|
|
@@ -2877,7 +3041,7 @@ def main():
|
|
|
2877
3041
|
|
|
2878
3042
|
# Check for directory traversal for info path
|
|
2879
3043
|
try:
|
|
2880
|
-
cache_root = Path(get_cache_path(
|
|
3044
|
+
cache_root = Path(get_cache_path())
|
|
2881
3045
|
requested_path = Path(info_path).resolve()
|
|
2882
3046
|
if not str(requested_path).startswith(str(cache_root)):
|
|
2883
3047
|
return web.Response(text="403: Forbidden", status=403)
|
|
@@ -2893,7 +3057,7 @@ def main():
|
|
|
2893
3057
|
|
|
2894
3058
|
# Check for directory traversal
|
|
2895
3059
|
try:
|
|
2896
|
-
cache_root = Path(get_cache_path(
|
|
3060
|
+
cache_root = Path(get_cache_path())
|
|
2897
3061
|
requested_path = Path(full_path).resolve()
|
|
2898
3062
|
if not str(requested_path).startswith(str(cache_root)):
|
|
2899
3063
|
return web.Response(text="403: Forbidden", status=403)
|
|
@@ -2912,7 +3076,7 @@ def main():
|
|
|
2912
3076
|
async def github_auth_handler(request):
|
|
2913
3077
|
"""Initiate GitHub OAuth flow"""
|
|
2914
3078
|
if "auth" not in g_config or "github" not in g_config["auth"]:
|
|
2915
|
-
return web.json_response(
|
|
3079
|
+
return web.json_response(create_error_response("GitHub OAuth not configured"), status=500)
|
|
2916
3080
|
|
|
2917
3081
|
auth_config = g_config["auth"]["github"]
|
|
2918
3082
|
client_id = auth_config.get("client_id", "")
|
|
@@ -2924,11 +3088,11 @@ def main():
|
|
|
2924
3088
|
if redirect_uri.startswith("$"):
|
|
2925
3089
|
redirect_uri = redirect_uri[1:]
|
|
2926
3090
|
|
|
2927
|
-
client_id = os.
|
|
2928
|
-
redirect_uri = os.
|
|
3091
|
+
client_id = os.getenv(client_id, client_id)
|
|
3092
|
+
redirect_uri = os.getenv(redirect_uri, redirect_uri)
|
|
2929
3093
|
|
|
2930
3094
|
if not client_id:
|
|
2931
|
-
return web.json_response(
|
|
3095
|
+
return web.json_response(create_error_response("GitHub client_id not configured"), status=500)
|
|
2932
3096
|
|
|
2933
3097
|
# Generate CSRF state token
|
|
2934
3098
|
state = secrets.token_urlsafe(32)
|
|
@@ -2960,7 +3124,7 @@ def main():
|
|
|
2960
3124
|
if restrict_to.startswith("$"):
|
|
2961
3125
|
restrict_to = restrict_to[1:]
|
|
2962
3126
|
|
|
2963
|
-
restrict_to = os.
|
|
3127
|
+
restrict_to = os.getenv(restrict_to, None if restrict_to == "GITHUB_USERS" else restrict_to)
|
|
2964
3128
|
|
|
2965
3129
|
# If restrict_to is configured, validate the user
|
|
2966
3130
|
if restrict_to:
|
|
@@ -2999,7 +3163,7 @@ def main():
|
|
|
2999
3163
|
g_oauth_states.pop(state)
|
|
3000
3164
|
|
|
3001
3165
|
if "auth" not in g_config or "github" not in g_config["auth"]:
|
|
3002
|
-
return web.json_response(
|
|
3166
|
+
return web.json_response(create_error_response("GitHub OAuth not configured"), status=500)
|
|
3003
3167
|
|
|
3004
3168
|
auth_config = g_config["auth"]["github"]
|
|
3005
3169
|
client_id = auth_config.get("client_id", "")
|
|
@@ -3014,12 +3178,12 @@ def main():
|
|
|
3014
3178
|
if redirect_uri.startswith("$"):
|
|
3015
3179
|
redirect_uri = redirect_uri[1:]
|
|
3016
3180
|
|
|
3017
|
-
client_id = os.
|
|
3018
|
-
client_secret = os.
|
|
3019
|
-
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)
|
|
3020
3184
|
|
|
3021
3185
|
if not client_id or not client_secret:
|
|
3022
|
-
return web.json_response(
|
|
3186
|
+
return web.json_response(create_error_response("GitHub OAuth credentials not configured"), status=500)
|
|
3023
3187
|
|
|
3024
3188
|
# Exchange code for access token
|
|
3025
3189
|
async with aiohttp.ClientSession() as session:
|
|
@@ -3038,7 +3202,7 @@ def main():
|
|
|
3038
3202
|
|
|
3039
3203
|
if not access_token:
|
|
3040
3204
|
error = token_response.get("error_description", "Failed to get access token")
|
|
3041
|
-
return web.
|
|
3205
|
+
return web.json_response(create_error_response(f"OAuth error: {error}"), status=400)
|
|
3042
3206
|
|
|
3043
3207
|
# Fetch user info
|
|
3044
3208
|
user_url = "https://api.github.com/user"
|
|
@@ -3073,7 +3237,7 @@ def main():
|
|
|
3073
3237
|
session_token = get_session_token(request)
|
|
3074
3238
|
|
|
3075
3239
|
if not session_token or session_token not in g_sessions:
|
|
3076
|
-
return web.json_response(
|
|
3240
|
+
return web.json_response(create_error_response("Invalid or expired session"), status=401)
|
|
3077
3241
|
|
|
3078
3242
|
session_data = g_sessions[session_token]
|
|
3079
3243
|
|
|
@@ -3129,9 +3293,7 @@ def main():
|
|
|
3129
3293
|
# })
|
|
3130
3294
|
|
|
3131
3295
|
# Not authenticated - return error in expected format
|
|
3132
|
-
return web.json_response(
|
|
3133
|
-
{"responseStatus": {"errorCode": "Unauthorized", "message": "Not authenticated"}}, status=401
|
|
3134
|
-
)
|
|
3296
|
+
return web.json_response(error_auth_required, status=401)
|
|
3135
3297
|
|
|
3136
3298
|
app.router.add_get("/auth", auth_handler)
|
|
3137
3299
|
app.router.add_get("/auth/github", github_auth_handler)
|
|
@@ -3191,9 +3353,55 @@ def main():
|
|
|
3191
3353
|
|
|
3192
3354
|
# go through and register all g_app extensions
|
|
3193
3355
|
for handler in g_app.server_add_get:
|
|
3194
|
-
|
|
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])
|
|
3195
3365
|
for handler in g_app.server_add_post:
|
|
3196
|
-
|
|
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])
|
|
3197
3405
|
|
|
3198
3406
|
# Serve index.html from root
|
|
3199
3407
|
async def index_handler(request):
|
|
@@ -3236,7 +3444,7 @@ def main():
|
|
|
3236
3444
|
|
|
3237
3445
|
for provider in enable_providers:
|
|
3238
3446
|
if provider not in g_config["providers"]:
|
|
3239
|
-
print(f"Provider {provider} not found")
|
|
3447
|
+
print(f"Provider '{provider}' not found")
|
|
3240
3448
|
print(f"Available providers: {', '.join(g_config['providers'].keys())}")
|
|
3241
3449
|
exit(1)
|
|
3242
3450
|
if provider in g_config["providers"]:
|
|
@@ -3324,6 +3532,9 @@ def main():
|
|
|
3324
3532
|
|
|
3325
3533
|
if len(extra_args) > 0:
|
|
3326
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
|
+
|
|
3327
3538
|
# replace content of last message if exists, else add
|
|
3328
3539
|
last_msg = chat["messages"][-1] if "messages" in chat else None
|
|
3329
3540
|
if last_msg and last_msg["role"] == "user":
|