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.
Files changed (55) hide show
  1. llms/__pycache__/main.cpython-314.pyc +0 -0
  2. llms/{ui/modules/analytics.mjs → extensions/analytics/ui/index.mjs} +4 -2
  3. llms/extensions/core_tools/__init__.py +358 -0
  4. llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
  5. llms/extensions/gallery/__init__.py +61 -0
  6. llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
  7. llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  8. llms/extensions/gallery/db.py +298 -0
  9. llms/extensions/gallery/ui/index.mjs +480 -0
  10. llms/extensions/providers/__init__.py +18 -0
  11. llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  12. llms/{providers → extensions/providers}/__pycache__/anthropic.cpython-314.pyc +0 -0
  13. llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  14. llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
  15. llms/{providers → extensions/providers}/__pycache__/nvidia.cpython-314.pyc +0 -0
  16. llms/{providers → extensions/providers}/__pycache__/openai.cpython-314.pyc +0 -0
  17. llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  18. llms/{providers → extensions/providers}/anthropic.py +1 -4
  19. llms/{providers → extensions/providers}/chutes.py +21 -18
  20. llms/{providers → extensions/providers}/google.py +99 -27
  21. llms/{providers → extensions/providers}/nvidia.py +6 -8
  22. llms/{providers → extensions/providers}/openai.py +3 -6
  23. llms/{providers → extensions/providers}/openrouter.py +12 -10
  24. llms/extensions/system_prompts/__init__.py +45 -0
  25. llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
  26. llms/extensions/system_prompts/ui/index.mjs +284 -0
  27. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  28. llms/{ui/modules/tools.mjs → extensions/tools/ui/index.mjs} +4 -2
  29. llms/llms.json +17 -1
  30. llms/main.py +407 -175
  31. llms/providers-extra.json +0 -32
  32. llms/ui/App.mjs +17 -18
  33. llms/ui/ai.mjs +10 -3
  34. llms/ui/app.css +1553 -24
  35. llms/ui/ctx.mjs +70 -12
  36. llms/ui/index.mjs +13 -8
  37. llms/ui/modules/chat/ChatBody.mjs +11 -248
  38. llms/ui/modules/chat/HomeTools.mjs +254 -0
  39. llms/ui/modules/chat/SettingsDialog.mjs +1 -1
  40. llms/ui/modules/chat/index.mjs +278 -174
  41. llms/ui/modules/layout.mjs +2 -26
  42. llms/ui/modules/model-selector.mjs +1 -1
  43. llms/ui/modules/threads/index.mjs +5 -11
  44. llms/ui/modules/threads/threadStore.mjs +56 -2
  45. llms/ui/utils.mjs +21 -3
  46. {llms_py-3.0.0b5.dist-info → llms_py-3.0.0b7.dist-info}/METADATA +1 -1
  47. llms_py-3.0.0b7.dist-info/RECORD +80 -0
  48. llms/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  49. llms/providers/__pycache__/google.cpython-314.pyc +0 -0
  50. llms/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  51. llms_py-3.0.0b5.dist-info/RECORD +0 -66
  52. {llms_py-3.0.0b5.dist-info → llms_py-3.0.0b7.dist-info}/WHEEL +0 -0
  53. {llms_py-3.0.0b5.dist-info → llms_py-3.0.0b7.dist-info}/entry_points.txt +0 -0
  54. {llms_py-3.0.0b5.dist-info → llms_py-3.0.0b7.dist-info}/licenses/LICENSE +0 -0
  55. {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.0b5"
43
+ VERSION = "3.0.0b7"
44
44
  _ROOT = None
45
- DEBUG = True # os.getenv("PYPI_SERVICESTACK") is not None
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 save_image_to_cache(base64_data, filename, image_info):
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
- url = f"~cache/{relative_path}"
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
- async def chat_completion(chat):
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
- only_tools = chat["metadata"].get("only_tools", "").split(",")
1077
- include_all_tools = only_tools == "all"
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
- # Apply pre-chat filters
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("~cache"):
1251
- print(get_cache_path(file[7:]))
1252
- _log(f"http://localhost:8000/{file}")
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.environ.get(value[1:], "")
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.environ.get(value[1:], "")
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.environ.get(env_var)
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.environ.get('HOME')}/.llms/{filename}"
1562
+ return f"{os.getenv('HOME')}/.llms/{filename}"
1470
1563
 
1471
1564
 
1472
- def get_cache_path(filename):
1473
- return home_llms_path(f"cache/{filename}")
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.environ.get("LLMS_CONFIG_PATH"):
1483
- check_paths.insert(0, os.environ.get("LLMS_CONFIG_PATH"))
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.server_add_post = []
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 chat_completion(self, chat):
2069
- return chat_completion(chat)
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 load_builtin_extensions():
2103
- providers_path = _ROOT / "providers"
2104
- if not providers_path.exists():
2105
- return
2270
+ def get_username(self, request):
2271
+ return self.app.get_username(request)
2106
2272
 
2107
- for item in os.listdir(providers_path):
2108
- if not item.endswith(".py") or item == "__init__.py":
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
- try:
2115
- spec = importlib.util.spec_from_file_location(module_name, item_path)
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 get_extensions_path():
2130
- return os.environ.get("LLMS_EXTENSIONS_DIR", os.path.join(Path.home(), ".llms", "extensions"))
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 init_extensions(parser):
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
- for item in os.listdir(extensions_path):
2138
- item_path = os.path.join(extensions_path, item)
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
- ext_count = len(os.listdir(extensions_path))
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
- sys.path.append(extensions_path)
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
- init_file = os.path.join(item_path, "__init__.py")
2179
- if os.path.exists(init_file):
2373
+ sys.path.append(item_path)
2374
+ try:
2180
2375
  ctx = ExtensionContext(g_app, item_path)
2181
- try:
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
- # if ui folder exists, serve as static files at /ext/{item}/
2198
- ui_path = os.path.join(item_path, "ui")
2199
- if os.path.exists(ui_path):
2200
- ctx.add_static_files(ui_path)
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
- # Register UI extension if index.mjs exists (/ext/{item}/index.mjs)
2203
- if os.path.exists(os.path.join(ui_path, "index.mjs")):
2204
- ctx.register_ui_extension("index.mjs")
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
- except Exception as e:
2207
- _err(f"Failed to install extension {item}", e)
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
- extensions_path = get_extensions_path()
2219
- os.makedirs(extensions_path, exist_ok=True)
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.environ.get("VERBOSE", "").lower()
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.environ.get("LLMS_DISABLE_UPDATE", "") != "1"
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
- subprocess.run(
2456
- [sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], cwd=target_path, check=True
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
- load_builtin_extensions()
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.environ.get(client_id, client_id)
2628
- client_secret = os.environ.get(client_secret, client_secret)
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
- for filter_func in g_app.chat_request_filters:
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({"error": str(e)}, status=500)
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({"error": "No file provided"}, status=400)
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": f"/~cache/{relative_path}",
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({"error": "GitHub OAuth not configured"}, status=500)
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.environ.get(client_id, client_id)
2907
- redirect_uri = os.environ.get(redirect_uri, redirect_uri)
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({"error": "GitHub client_id not configured"}, status=500)
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.environ.get(restrict_to, None if restrict_to == "GITHUB_USERS" else restrict_to)
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({"error": "GitHub OAuth not configured"}, status=500)
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.environ.get(client_id, client_id)
2997
- client_secret = os.environ.get(client_secret, client_secret)
2998
- redirect_uri = os.environ.get(redirect_uri, redirect_uri)
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({"error": "GitHub OAuth credentials not configured"}, status=500)
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.Response(text=f"OAuth error: {error}", status=400)
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({"error": "Invalid or expired session"}, status=401)
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
- app.router.add_get(handler[0], handler[1], **handler[2])
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
- app.router.add_post(handler[0], handler[1], **handler[2])
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":