llms-py 3.0.0b7__py3-none-any.whl → 3.0.0b9__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 (157) hide show
  1. llms/__pycache__/main.cpython-314.pyc +0 -0
  2. llms/extensions/analytics/ui/index.mjs +51 -162
  3. llms/extensions/app/__init__.py +519 -0
  4. llms/extensions/app/__pycache__/__init__.cpython-314.pyc +0 -0
  5. llms/extensions/app/__pycache__/db.cpython-314.pyc +0 -0
  6. llms/extensions/app/__pycache__/db_manager.cpython-314.pyc +0 -0
  7. llms/extensions/app/db.py +643 -0
  8. llms/extensions/app/db_manager.py +195 -0
  9. llms/extensions/app/requests.json +9073 -0
  10. llms/extensions/app/threads.json +15290 -0
  11. llms/{ui/modules/threads → extensions/app/ui}/Recents.mjs +82 -55
  12. llms/{ui/modules/threads → extensions/app/ui}/index.mjs +78 -9
  13. llms/extensions/app/ui/threadStore.mjs +407 -0
  14. llms/extensions/core_tools/__init__.py +272 -32
  15. llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
  16. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  17. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  18. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  19. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  20. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  21. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  22. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  23. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  24. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  25. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  26. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  27. llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +344 -0
  28. llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +9884 -0
  29. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  30. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  31. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  32. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  33. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  34. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  35. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  36. llms/extensions/core_tools/ui/index.mjs +650 -0
  37. llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  38. llms/extensions/gallery/db.py +4 -4
  39. llms/extensions/gallery/ui/index.mjs +2 -1
  40. llms/extensions/katex/__init__.py +6 -0
  41. llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
  42. llms/extensions/katex/ui/README.md +125 -0
  43. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  44. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  45. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  46. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  47. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  48. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  49. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  50. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  51. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  52. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  53. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  54. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  55. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  56. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  57. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  58. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  59. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  60. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  61. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  62. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  63. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  64. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  65. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  66. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  67. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  68. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  69. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  70. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  71. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  72. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  73. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  118. llms/extensions/katex/ui/index.mjs +92 -0
  119. llms/extensions/katex/ui/katex-swap.css +1230 -0
  120. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  121. llms/extensions/katex/ui/katex.css +1230 -0
  122. llms/extensions/katex/ui/katex.js +19080 -0
  123. llms/extensions/katex/ui/katex.min.css +1 -0
  124. llms/extensions/katex/ui/katex.min.js +1 -0
  125. llms/extensions/katex/ui/katex.min.mjs +1 -0
  126. llms/extensions/katex/ui/katex.mjs +18547 -0
  127. llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  128. llms/extensions/providers/anthropic.py +44 -1
  129. llms/extensions/system_prompts/ui/index.mjs +2 -1
  130. llms/extensions/tools/__init__.py +5 -0
  131. llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  132. llms/extensions/tools/ui/index.mjs +8 -8
  133. llms/index.html +26 -38
  134. llms/llms.json +4 -1
  135. llms/main.py +492 -103
  136. llms/ui/App.mjs +2 -3
  137. llms/ui/ai.mjs +29 -13
  138. llms/ui/app.css +255 -289
  139. llms/ui/ctx.mjs +84 -6
  140. llms/ui/index.mjs +4 -6
  141. llms/ui/lib/vue.min.mjs +10 -9
  142. llms/ui/lib/vue.mjs +1796 -1635
  143. llms/ui/markdown.mjs +4 -2
  144. llms/ui/modules/chat/ChatBody.mjs +90 -86
  145. llms/ui/modules/chat/HomeTools.mjs +0 -242
  146. llms/ui/modules/chat/index.mjs +103 -170
  147. llms/ui/modules/model-selector.mjs +2 -2
  148. llms/ui/tailwind.input.css +35 -1
  149. llms/ui/utils.mjs +12 -0
  150. {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b9.dist-info}/METADATA +1 -1
  151. llms_py-3.0.0b9.dist-info/RECORD +198 -0
  152. llms/ui/modules/threads/threadStore.mjs +0 -640
  153. llms_py-3.0.0b7.dist-info/RECORD +0 -80
  154. {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b9.dist-info}/WHEEL +0 -0
  155. {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b9.dist-info}/entry_points.txt +0 -0
  156. {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b9.dist-info}/licenses/LICENSE +0 -0
  157. {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b9.dist-info}/top_level.txt +0 -0
llms/main.py CHANGED
@@ -9,6 +9,7 @@
9
9
  import argparse
10
10
  import asyncio
11
11
  import base64
12
+ import contextlib
12
13
  import hashlib
13
14
  import importlib.util
14
15
  import inspect
@@ -40,7 +41,7 @@ try:
40
41
  except ImportError:
41
42
  HAS_PIL = False
42
43
 
43
- VERSION = "3.0.0b7"
44
+ VERSION = "3.0.0b9"
44
45
  _ROOT = None
45
46
  DEBUG = os.getenv("DEBUG") == "1"
46
47
  MOCK = os.getenv("MOCK") == "1"
@@ -324,6 +325,15 @@ def convert_image_if_needed(image_bytes, mimetype="image/png"):
324
325
  return image_bytes, mimetype
325
326
 
326
327
 
328
+ def to_content(result):
329
+ if isinstance(result, (str, int, float, bool)):
330
+ return str(result)
331
+ elif isinstance(result, (list, set, tuple, dict)):
332
+ return json.dumps(result)
333
+ else:
334
+ return str(result)
335
+
336
+
327
337
  def function_to_tool_definition(func):
328
338
  type_hints = get_type_hints(func)
329
339
  signature = inspect.signature(func)
@@ -332,11 +342,11 @@ def function_to_tool_definition(func):
332
342
  for name, param in signature.parameters.items():
333
343
  param_type = type_hints.get(name, str)
334
344
  param_type_name = "string"
335
- if param_type == int:
345
+ if param_type is int:
336
346
  param_type_name = "integer"
337
- elif param_type == float:
347
+ elif param_type is float:
338
348
  param_type_name = "number"
339
- elif param_type == bool:
349
+ elif param_type is bool:
340
350
  param_type_name = "boolean"
341
351
 
342
352
  parameters["properties"][name] = {"type": param_type_name}
@@ -484,6 +494,92 @@ async def process_chat(chat, provider_id=None):
484
494
  return chat
485
495
 
486
496
 
497
+ def image_ext_from_mimetype(mimetype, default="png"):
498
+ if "/" in mimetype:
499
+ _ext = mimetypes.guess_extension(mimetype)
500
+ if _ext:
501
+ return _ext.lstrip(".")
502
+ return default
503
+
504
+
505
+ def audio_ext_from_format(format, default="mp3"):
506
+ if format == "mpeg":
507
+ return "mp3"
508
+ return format or default
509
+
510
+
511
+ def file_ext_from_mimetype(mimetype, default="pdf"):
512
+ if "/" in mimetype:
513
+ _ext = mimetypes.guess_extension(mimetype)
514
+ if _ext:
515
+ return _ext.lstrip(".")
516
+ return default
517
+
518
+
519
+ def cache_message_inline_data(m):
520
+ """
521
+ Replaces and caches any inline data URIs in the message content.
522
+ """
523
+ if "content" not in m:
524
+ return
525
+
526
+ content = m["content"]
527
+ if isinstance(content, list):
528
+ for item in content:
529
+ if item.get("type") == "image_url":
530
+ image_url = item.get("image_url", {})
531
+ url = image_url.get("url")
532
+ if url and url.startswith("data:"):
533
+ # Extract base64 and mimetype
534
+ try:
535
+ header, base64_data = url.split(";base64,")
536
+ # header is like "data:image/png"
537
+ ext = image_ext_from_mimetype(header.split(":")[1])
538
+ filename = f"image.{ext}" # Hash will handle uniqueness
539
+
540
+ cache_url, _ = save_image_to_cache(base64_data, filename, {}, ignore_info=True)
541
+ image_url["url"] = cache_url
542
+ except Exception as e:
543
+ _log(f"Error caching inline image: {e}")
544
+
545
+ elif item.get("type") == "input_audio":
546
+ input_audio = item.get("input_audio", {})
547
+ data = input_audio.get("data")
548
+ if data:
549
+ # Handle data URI or raw base64
550
+ base64_data = data
551
+ if data.startswith("data:"):
552
+ with contextlib.suppress(ValueError):
553
+ header, base64_data = data.split(";base64,")
554
+
555
+ fmt = audio_ext_from_format(input_audio.get("format"))
556
+ filename = f"audio.{fmt}"
557
+
558
+ try:
559
+ cache_url, _ = save_bytes_to_cache(base64_data, filename, {}, ignore_info=True)
560
+ input_audio["data"] = cache_url
561
+ except Exception as e:
562
+ _log(f"Error caching inline audio: {e}")
563
+
564
+ elif item.get("type") == "file":
565
+ file_info = item.get("file", {})
566
+ file_data = file_info.get("file_data")
567
+ if file_data and file_data.startswith("data:"):
568
+ try:
569
+ header, base64_data = file_data.split(";base64,")
570
+ mimetype = header.split(":")[1]
571
+ # Try to get extension from filename if available, else mimetype
572
+ filename = file_info.get("filename", "file")
573
+ if "." not in filename:
574
+ ext = file_ext_from_mimetype(mimetype)
575
+ filename = f"{filename}.{ext}"
576
+
577
+ cache_url, _ = save_bytes_to_cache(base64_data, filename, {}, ignore_info=True)
578
+ file_info["file_data"] = cache_url
579
+ except Exception as e:
580
+ _log(f"Error caching inline file: {e}")
581
+
582
+
487
583
  class HTTPError(Exception):
488
584
  def __init__(self, status, reason, body, headers=None):
489
585
  self.status = status
@@ -493,7 +589,7 @@ class HTTPError(Exception):
493
589
  super().__init__(f"HTTP {status} {reason}")
494
590
 
495
591
 
496
- def save_bytes_to_cache(base64_data, filename, file_info):
592
+ def save_bytes_to_cache(base64_data, filename, file_info, ignore_info=False):
497
593
  ext = filename.split(".")[-1]
498
594
  mimetype = get_file_mime_type(filename)
499
595
  content = base64.b64decode(base64_data) if isinstance(base64_data, str) else base64_data
@@ -507,6 +603,14 @@ def save_bytes_to_cache(base64_data, filename, file_info):
507
603
  full_path = get_cache_path(relative_path)
508
604
  url = f"/~cache/{relative_path}"
509
605
 
606
+ # if file and its .info.json already exists, return it
607
+ info_path = os.path.splitext(full_path)[0] + ".info.json"
608
+ if os.path.exists(full_path) and os.path.exists(info_path):
609
+ _dbg(f"Cached bytes exists: {relative_path}")
610
+ if ignore_info:
611
+ return url, None
612
+ return url, json.load(open(info_path))
613
+
510
614
  os.makedirs(os.path.dirname(full_path), exist_ok=True)
511
615
 
512
616
  with open(full_path, "wb") as f:
@@ -520,12 +624,19 @@ def save_bytes_to_cache(base64_data, filename, file_info):
520
624
  }
521
625
  info.update(file_info)
522
626
 
627
+ # Save metadata
628
+ info_path = os.path.splitext(full_path)[0] + ".info.json"
629
+ with open(info_path, "w") as f:
630
+ json.dump(info, f)
631
+
632
+ _dbg(f"Saved cached bytes and info: {relative_path}")
633
+
523
634
  g_app.on_cache_saved_filters({"url": url, "info": info})
524
635
 
525
636
  return url, info
526
637
 
527
638
 
528
- def save_image_to_cache(base64_data, filename, image_info):
639
+ def save_image_to_cache(base64_data, filename, image_info, ignore_info=False):
529
640
  ext = filename.split(".")[-1]
530
641
  mimetype = get_file_mime_type(filename)
531
642
  content = base64.b64decode(base64_data) if isinstance(base64_data, str) else base64_data
@@ -542,6 +653,9 @@ def save_image_to_cache(base64_data, filename, image_info):
542
653
  # if file and its .info.json already exists, return it
543
654
  info_path = os.path.splitext(full_path)[0] + ".info.json"
544
655
  if os.path.exists(full_path) and os.path.exists(info_path):
656
+ _dbg(f"Saved image exists: {relative_path}")
657
+ if ignore_info:
658
+ return url, None
545
659
  return url, json.load(open(info_path))
546
660
 
547
661
  os.makedirs(os.path.dirname(full_path), exist_ok=True)
@@ -576,6 +690,8 @@ def save_image_to_cache(base64_data, filename, image_info):
576
690
  with open(info_path, "w") as f:
577
691
  json.dump(info, f)
578
692
 
693
+ _dbg(f"Saved image and info: {relative_path}")
694
+
579
695
  g_app.on_cache_saved_filters({"url": url, "info": info})
580
696
 
581
697
  return url, info
@@ -610,6 +726,21 @@ def chat_to_prompt(chat):
610
726
  return prompt
611
727
 
612
728
 
729
+ def chat_to_system_prompt(chat):
730
+ if "messages" in chat:
731
+ for message in chat["messages"]:
732
+ if message["role"] == "system":
733
+ # if content is string
734
+ if isinstance(message["content"], str):
735
+ return message["content"]
736
+ elif isinstance(message["content"], list):
737
+ # if content is array of objects
738
+ for part in message["content"]:
739
+ if part["type"] == "text":
740
+ return part["text"]
741
+ return None
742
+
743
+
613
744
  def chat_to_username(chat):
614
745
  if "metadata" in chat and "user" in chat["metadata"]:
615
746
  return chat["metadata"]["user"]
@@ -632,6 +763,34 @@ def last_user_prompt(chat):
632
763
  return prompt
633
764
 
634
765
 
766
+ def chat_response_to_message(openai_response):
767
+ """
768
+ Returns an assistant message from the OpenAI Response.
769
+ Handles normalizing text, image, and audio responses into the message content.
770
+ """
771
+ timestamp = int(time.time() * 1000) # openai_response.get("created")
772
+ choices = openai_response
773
+ if isinstance(openai_response, dict) and "choices" in openai_response:
774
+ choices = openai_response["choices"]
775
+
776
+ choice = choices[0] if isinstance(choices, list) and choices else choices
777
+
778
+ if isinstance(choice, str):
779
+ return {"role": "assistant", "content": choice, "timestamp": timestamp}
780
+
781
+ if isinstance(choice, dict):
782
+ message = choice.get("message", choice)
783
+ else:
784
+ return {"role": "assistant", "content": str(choice), "timestamp": timestamp}
785
+
786
+ # Ensure message is a dict
787
+ if not isinstance(message, dict):
788
+ return {"role": "assistant", "content": message, "timestamp": timestamp}
789
+
790
+ message.update({"timestamp": timestamp})
791
+ return message
792
+
793
+
635
794
  def to_file_info(chat, info=None, response=None):
636
795
  prompt = last_user_prompt(chat)
637
796
  ret = info or {}
@@ -809,13 +968,17 @@ class OpenAiCompatible:
809
968
  if not self.models:
810
969
  await self.load_models()
811
970
 
812
- def model_cost(self, model):
971
+ def model_info(self, model):
813
972
  provider_model = self.provider_model(model) or model
814
973
  for model_id, model_info in self.models.items():
815
974
  if model_id.lower() == provider_model.lower():
816
- return model_info.get("cost")
975
+ return model_info
817
976
  return None
818
977
 
978
+ def model_cost(self, model):
979
+ model_info = self.model_info(model)
980
+ return model_info.get("cost") if model_info else None
981
+
819
982
  def provider_model(self, model):
820
983
  # convert model to lowercase for case-insensitive comparison
821
984
  model_lower = model.lower()
@@ -877,7 +1040,7 @@ class OpenAiCompatible:
877
1040
  chat["model"] = self.provider_model(chat["model"]) or chat["model"]
878
1041
 
879
1042
  if "modalities" in chat:
880
- for modality in chat["modalities"]:
1043
+ for modality in chat.get("modalities", []):
881
1044
  # use default implementation for text modalities
882
1045
  if modality == "text":
883
1046
  continue
@@ -1108,8 +1271,12 @@ def api_providers():
1108
1271
  return ret
1109
1272
 
1110
1273
 
1274
+ def to_error_message(e):
1275
+ return str(e)
1276
+
1277
+
1111
1278
  def to_error_response(e, stacktrace=False):
1112
- status = {"errorCode": "Error", "message": str(e)}
1279
+ status = {"errorCode": "Error", "message": to_error_message(e)}
1113
1280
  if stacktrace:
1114
1281
  status["stackTrace"] = traceback.format_exc()
1115
1282
  return {"responseStatus": status}
@@ -1122,6 +1289,14 @@ def create_error_response(message, error_code="Error", stack_trace=None):
1122
1289
  return ret
1123
1290
 
1124
1291
 
1292
+ def should_cancel_thread(context):
1293
+ ret = context.get("cancelled", False)
1294
+ if ret:
1295
+ thread_id = context.get("threadId")
1296
+ _dbg(f"Thread cancelled {thread_id}")
1297
+ return ret
1298
+
1299
+
1125
1300
  def g_chat_request(template=None, text=None, model=None, system_prompt=None):
1126
1301
  chat_template = g_config["defaults"].get(template or "text")
1127
1302
  if not chat_template:
@@ -1150,28 +1325,51 @@ def g_chat_request(template=None, text=None, model=None, system_prompt=None):
1150
1325
 
1151
1326
 
1152
1327
  async def g_chat_completion(chat, context=None):
1153
- model = chat["model"]
1154
- # get first provider that has the model
1155
- candidate_providers = [name for name, provider in g_handlers.items() if provider.provider_model(model)]
1156
- if len(candidate_providers) == 0:
1157
- raise (Exception(f"Model {model} not found"))
1328
+ try:
1329
+ model = chat.get("model")
1330
+ if not model:
1331
+ raise Exception("Model not specified")
1332
+
1333
+ if context is None:
1334
+ context = {"chat": chat, "tools": "all"}
1158
1335
 
1336
+ # get first provider that has the model
1337
+ candidate_providers = [name for name, provider in g_handlers.items() if provider.provider_model(model)]
1338
+ if len(candidate_providers) == 0:
1339
+ raise (Exception(f"Model {model} not found"))
1340
+ except Exception as e:
1341
+ await g_app.on_chat_error(e, context or {"chat": chat})
1342
+ raise e
1343
+
1344
+ started_at = time.time()
1159
1345
  first_exception = None
1346
+ provider_name = "Unknown"
1160
1347
  for name in candidate_providers:
1161
- provider = g_handlers[name]
1162
- _log(f"provider: {name} {type(provider).__name__}")
1163
1348
  try:
1164
- # Inject global tools if present
1165
- current_chat = chat.copy()
1349
+ provider_name = name
1350
+ provider = g_handlers[name]
1351
+ _log(f"provider: {name} {type(provider).__name__}")
1352
+ started_at = time.time()
1353
+ context["startedAt"] = datetime.now()
1354
+ context["provider"] = name
1355
+ model_info = provider.model_info(model)
1356
+ context["modelCost"] = model_info.get("cost", provider.model_cost(model)) or {"input": 0, "output": 0}
1357
+ context["modelInfo"] = model_info
1358
+
1359
+ # Accumulate usage across tool calls
1360
+ total_usage = {
1361
+ "prompt_tokens": 0,
1362
+ "completion_tokens": 0,
1363
+ "total_tokens": 0,
1364
+ }
1365
+ accumulated_cost = 0.0
1366
+
1166
1367
  # Inject global tools if present
1167
1368
  current_chat = chat.copy()
1168
1369
  if g_app.tool_definitions:
1169
- include_all_tools = False
1170
- only_tools = []
1171
- if "metadata" in chat:
1172
- only_tools_str = chat["metadata"].get("only_tools", "")
1173
- include_all_tools = only_tools_str == "all"
1174
- only_tools = only_tools_str.split(",")
1370
+ only_tools_str = context.get("tools", "all")
1371
+ include_all_tools = only_tools_str == "all"
1372
+ only_tools = only_tools_str.split(",")
1175
1373
 
1176
1374
  if include_all_tools or len(only_tools) > 0:
1177
1375
  if "tools" not in current_chat:
@@ -1183,12 +1381,38 @@ async def g_chat_completion(chat, context=None):
1183
1381
  if name not in existing_tools and (include_all_tools or name in only_tools):
1184
1382
  current_chat["tools"].append(tool_def)
1185
1383
 
1384
+ # Apply pre-chat filters ONCE
1385
+ context["chat"] = current_chat
1386
+ for filter_func in g_app.chat_request_filters:
1387
+ await filter_func(current_chat, context)
1388
+
1186
1389
  # Tool execution loop
1187
- max_iterations = 5
1390
+ max_iterations = 10
1188
1391
  tool_history = []
1392
+ final_response = None
1393
+
1189
1394
  for _ in range(max_iterations):
1395
+ if should_cancel_thread(context):
1396
+ return
1397
+
1190
1398
  response = await provider.chat(current_chat)
1191
1399
 
1400
+ if should_cancel_thread(context):
1401
+ return
1402
+
1403
+ # Aggregate usage
1404
+ if "usage" in response:
1405
+ usage = response["usage"]
1406
+ total_usage["prompt_tokens"] += usage.get("prompt_tokens", 0)
1407
+ total_usage["completion_tokens"] += usage.get("completion_tokens", 0)
1408
+ total_usage["total_tokens"] += usage.get("total_tokens", 0)
1409
+
1410
+ # Calculate cost for this step if available
1411
+ if "cost" in response and isinstance(response["cost"], (int, float)):
1412
+ accumulated_cost += response["cost"]
1413
+ elif "cost" in usage and isinstance(usage["cost"], (int, float)):
1414
+ accumulated_cost += usage["cost"]
1415
+
1192
1416
  # Check for tool_calls in the response
1193
1417
  choice = response.get("choices", [])[0] if response.get("choices") else {}
1194
1418
  message = choice.get("message", {})
@@ -1198,48 +1422,85 @@ async def g_chat_completion(chat, context=None):
1198
1422
  # Append the assistant's message with tool calls to history
1199
1423
  if "messages" not in current_chat:
1200
1424
  current_chat["messages"] = []
1425
+ if "timestamp" not in message:
1426
+ message["timestamp"] = int(time.time() * 1000)
1201
1427
  current_chat["messages"].append(message)
1202
1428
  tool_history.append(message)
1203
1429
 
1204
1430
  for tool_call in tool_calls:
1205
1431
  function_name = tool_call["function"]["name"]
1206
- function_args = json.loads(tool_call["function"]["arguments"])
1207
-
1208
- tool_result = f"Error: Tool {function_name} not found"
1209
- if function_name in g_app.tools:
1210
- try:
1211
- func = g_app.tools[function_name]
1212
- if inspect.iscoroutinefunction(func):
1213
- tool_result = await func(**function_args)
1214
- else:
1215
- tool_result = func(**function_args)
1216
- except Exception as e:
1217
- tool_result = f"Error executing tool {function_name}: {e}"
1432
+ try:
1433
+ function_args = json.loads(tool_call["function"]["arguments"])
1434
+ except Exception as e:
1435
+ tool_result = f"Error parsing JSON arguments for tool {function_name}: {e}"
1436
+ else:
1437
+ tool_result = f"Error: Tool {function_name} not found"
1438
+ if function_name in g_app.tools:
1439
+ try:
1440
+ func = g_app.tools[function_name]
1441
+ if inspect.iscoroutinefunction(func):
1442
+ tool_result = await func(**function_args)
1443
+ else:
1444
+ tool_result = func(**function_args)
1445
+ except Exception as e:
1446
+ tool_result = f"Error executing tool {function_name}: {e}"
1218
1447
 
1219
1448
  # Append tool result to history
1220
- tool_msg = {"role": "tool", "tool_call_id": tool_call["id"], "content": str(tool_result)}
1449
+ tool_msg = {"role": "tool", "tool_call_id": tool_call["id"], "content": to_content(tool_result)}
1221
1450
  current_chat["messages"].append(tool_msg)
1222
1451
  tool_history.append(tool_msg)
1223
1452
 
1453
+ for filter_func in g_app.chat_tool_filters:
1454
+ await filter_func(current_chat, context)
1455
+
1456
+ if should_cancel_thread(context):
1457
+ return
1458
+
1224
1459
  # Continue loop to send tool results back to LLM
1225
1460
  continue
1226
1461
 
1227
- # If no tool calls, return the response
1462
+ # If no tool calls, this is the final response
1228
1463
  if tool_history:
1229
1464
  response["tool_history"] = tool_history
1230
- return response
1465
+
1466
+ # Update final response with aggregated usage
1467
+ if "usage" not in response:
1468
+ response["usage"] = {}
1469
+ # convert to int seconds
1470
+ context["duration"] = duration = int(time.time() - started_at)
1471
+ total_usage.update({"duration": duration})
1472
+ response["usage"].update(total_usage)
1473
+ # If we accumulated cost, set it on the response
1474
+ if accumulated_cost > 0:
1475
+ response["cost"] = accumulated_cost
1476
+
1477
+ final_response = response
1478
+ break # Exit tool loop
1479
+
1480
+ if final_response:
1481
+ # Apply post-chat filters ONCE on final response
1482
+ for filter_func in g_app.chat_response_filters:
1483
+ await filter_func(final_response, context)
1484
+
1485
+ if DEBUG:
1486
+ _dbg(json.dumps(final_response, indent=2))
1487
+
1488
+ return final_response
1231
1489
 
1232
1490
  except Exception as e:
1233
1491
  if first_exception is None:
1234
1492
  first_exception = e
1235
- _log(f"Provider {name} failed: {e}")
1493
+ context["stackTrace"] = traceback.format_exc()
1494
+ _err(f"Provider {provider_name} failed", first_exception)
1495
+ await g_app.on_chat_error(e, context)
1496
+
1236
1497
  continue
1237
1498
 
1238
1499
  # If we get here, all providers failed
1239
1500
  raise first_exception
1240
1501
 
1241
1502
 
1242
- async def cli_chat(chat, image=None, audio=None, file=None, args=None, raw=False):
1503
+ async def cli_chat(chat, tools=None, image=None, audio=None, file=None, args=None, raw=False):
1243
1504
  if g_default_model:
1244
1505
  chat["model"] = g_default_model
1245
1506
 
@@ -1314,7 +1575,10 @@ async def cli_chat(chat, image=None, audio=None, file=None, args=None, raw=False
1314
1575
  printdump(chat)
1315
1576
 
1316
1577
  try:
1317
- response = await g_app.chat_completion(chat)
1578
+ context = {
1579
+ "tools": tools or "all",
1580
+ }
1581
+ response = await g_app.chat_completion(chat, context=context)
1318
1582
 
1319
1583
  if raw:
1320
1584
  print(json.dumps(response, indent=2))
@@ -1349,15 +1613,15 @@ async def cli_chat(chat, image=None, audio=None, file=None, args=None, raw=False
1349
1613
  except HTTPError as e:
1350
1614
  # HTTP error (4xx, 5xx)
1351
1615
  print(f"{e}:\n{e.body}")
1352
- exit(1)
1616
+ g_app.exit(1)
1353
1617
  except aiohttp.ClientConnectionError as e:
1354
1618
  # Connection issues
1355
1619
  print(f"Connection error: {e}")
1356
- exit(1)
1620
+ g_app.exit(1)
1357
1621
  except asyncio.TimeoutError as e:
1358
1622
  # Timeout
1359
1623
  print(f"Timeout error: {e}")
1360
- exit(1)
1624
+ g_app.exit(1)
1361
1625
 
1362
1626
 
1363
1627
  def config_str(key):
@@ -2043,17 +2307,41 @@ class AppExtensions:
2043
2307
  def __init__(self, cli_args, extra_args):
2044
2308
  self.cli_args = cli_args
2045
2309
  self.extra_args = extra_args
2310
+ self.config = None
2311
+ self.error_auth_required = create_error_response("Authentication required", "Unauthorized")
2046
2312
  self.ui_extensions = []
2047
2313
  self.chat_request_filters = []
2314
+ self.chat_tool_filters = []
2048
2315
  self.chat_response_filters = []
2316
+ self.chat_error_filters = []
2049
2317
  self.server_add_get = []
2050
2318
  self.server_add_post = []
2051
2319
  self.server_add_put = []
2052
2320
  self.server_add_delete = []
2053
2321
  self.server_add_patch = []
2054
2322
  self.cache_saved_filters = []
2323
+ self.shutdown_handlers = []
2055
2324
  self.tools = {}
2056
2325
  self.tool_definitions = []
2326
+ self.index_headers = []
2327
+ self.index_footers = []
2328
+ self.request_args = {
2329
+ "image_config": dict, # e.g. { "aspect_ratio": "1:1" }
2330
+ "temperature": float, # e.g: 0.7
2331
+ "max_completion_tokens": int, # e.g: 2048
2332
+ "seed": int, # e.g: 42
2333
+ "top_p": float, # e.g: 0.9
2334
+ "frequency_penalty": float, # e.g: 0.5
2335
+ "presence_penalty": float, # e.g: 0.5
2336
+ "stop": list, # e.g: ["Stop"]
2337
+ "reasoning_effort": str, # e.g: minimal, low, medium, high
2338
+ "verbosity": str, # e.g: low, medium, high
2339
+ "service_tier": str, # e.g: auto, default
2340
+ "top_logprobs": int,
2341
+ "safety_identifier": str,
2342
+ "store": bool,
2343
+ "enable_thinking": bool,
2344
+ }
2057
2345
  self.all_providers = [
2058
2346
  OpenAiCompatible,
2059
2347
  MistralProvider,
@@ -2075,6 +2363,43 @@ class AppExtensions:
2075
2363
  "16:9": "1344×768",
2076
2364
  "21:9": "1536×672",
2077
2365
  }
2366
+ self.import_maps = {
2367
+ "vue-prod": "/ui/lib/vue.min.mjs",
2368
+ "vue": "/ui/lib/vue.mjs",
2369
+ "vue-router": "/ui/lib/vue-router.min.mjs",
2370
+ "@servicestack/client": "/ui/lib/servicestack-client.mjs",
2371
+ "@servicestack/vue": "/ui/lib/servicestack-vue.mjs",
2372
+ "idb": "/ui/lib/idb.min.mjs",
2373
+ "marked": "/ui/lib/marked.min.mjs",
2374
+ "highlight.js": "/ui/lib/highlight.min.mjs",
2375
+ "chart.js": "/ui/lib/chart.js",
2376
+ "color.js": "/ui/lib/color.js",
2377
+ "ctx.mjs": "/ui/ctx.mjs",
2378
+ }
2379
+
2380
+ def set_config(self, config):
2381
+ self.config = config
2382
+ self.auth_enabled = self.config.get("auth", {}).get("enabled", False)
2383
+
2384
+ # Authentication middleware helper
2385
+ def check_auth(self, request):
2386
+ """Check if request is authenticated. Returns (is_authenticated, user_data)"""
2387
+ if not self.auth_enabled:
2388
+ return True, None
2389
+
2390
+ # Check for OAuth session token
2391
+ session_token = get_session_token(request)
2392
+ if session_token and session_token in g_sessions:
2393
+ return True, g_sessions[session_token]
2394
+
2395
+ # Check for API key
2396
+ auth_header = request.headers.get("Authorization", "")
2397
+ if auth_header.startswith("Bearer "):
2398
+ api_key = auth_header[7:]
2399
+ if api_key:
2400
+ return True, {"authProvider": "apikey"}
2401
+
2402
+ return False, None
2078
2403
 
2079
2404
  def get_session(self, request):
2080
2405
  session_token = get_session_token(request)
@@ -2100,25 +2425,7 @@ class AppExtensions:
2100
2425
  return g_chat_request(template=template, text=text, model=model, system_prompt=system_prompt)
2101
2426
 
2102
2427
  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
2428
  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
2429
  return response
2123
2430
 
2124
2431
  def on_cache_saved_filters(self, context):
@@ -2126,6 +2433,25 @@ class AppExtensions:
2126
2433
  for filter_func in self.cache_saved_filters:
2127
2434
  filter_func(context)
2128
2435
 
2436
+ async def on_chat_error(self, e, context):
2437
+ # Apply chat error filters
2438
+ if "stackTrace" not in context:
2439
+ context["stackTrace"] = traceback.format_exc()
2440
+ for filter_func in self.chat_error_filters:
2441
+ try:
2442
+ await filter_func(e, context)
2443
+ except Exception as e:
2444
+ _err("chat error filter failed", e)
2445
+
2446
+ def exit(self, exit_code=0):
2447
+ if len(self.shutdown_handlers) > 0:
2448
+ _dbg(f"running {len(self.shutdown_handlers)} shutdown handlers...")
2449
+ for handler in self.shutdown_handlers:
2450
+ handler()
2451
+
2452
+ _dbg(f"exit({exit_code})")
2453
+ sys.exit(exit_code)
2454
+
2129
2455
 
2130
2456
  def handler_name(handler):
2131
2457
  if hasattr(handler, "__name__"):
@@ -2136,6 +2462,9 @@ def handler_name(handler):
2136
2462
  class ExtensionContext:
2137
2463
  def __init__(self, app, path):
2138
2464
  self.app = app
2465
+ self.cli_args = app.cli_args
2466
+ self.extra_args = app.extra_args
2467
+ self.error_auth_required = app.error_auth_required
2139
2468
  self.path = path
2140
2469
  self.name = os.path.basename(path)
2141
2470
  if self.name.endswith(".py"):
@@ -2146,10 +2475,17 @@ class ExtensionContext:
2146
2475
  self.debug = DEBUG
2147
2476
  self.verbose = g_verbose
2148
2477
  self.aspect_ratios = app.aspect_ratios
2478
+ self.request_args = app.request_args
2149
2479
 
2150
2480
  def chat_to_prompt(self, chat):
2151
2481
  return chat_to_prompt(chat)
2152
2482
 
2483
+ def chat_to_system_prompt(self, chat):
2484
+ return chat_to_system_prompt(chat)
2485
+
2486
+ def chat_response_to_message(self, response):
2487
+ return chat_response_to_message(response)
2488
+
2153
2489
  def last_user_prompt(self, chat):
2154
2490
  return last_user_prompt(chat)
2155
2491
 
@@ -2184,6 +2520,12 @@ class ExtensionContext:
2184
2520
  if self.verbose:
2185
2521
  print(traceback.format_exc(), flush=True)
2186
2522
 
2523
+ def error_message(self, e):
2524
+ return to_error_message(e)
2525
+
2526
+ def error_response(self, e, stacktrace=False):
2527
+ return to_error_response(e, stacktrace=stacktrace)
2528
+
2187
2529
  def add_provider(self, provider):
2188
2530
  self.log(f"Registered provider: {provider.__name__}")
2189
2531
  self.app.all_providers.append(provider)
@@ -2197,14 +2539,26 @@ class ExtensionContext:
2197
2539
  self.log(f"Registered chat request filter: {handler_name(handler)}")
2198
2540
  self.app.chat_request_filters.append(handler)
2199
2541
 
2542
+ def register_chat_tool_filter(self, handler):
2543
+ self.log(f"Registered chat tool filter: {handler_name(handler)}")
2544
+ self.app.chat_tool_filters.append(handler)
2545
+
2200
2546
  def register_chat_response_filter(self, handler):
2201
2547
  self.log(f"Registered chat response filter: {handler_name(handler)}")
2202
2548
  self.app.chat_response_filters.append(handler)
2203
2549
 
2550
+ def register_chat_error_filter(self, handler):
2551
+ self.log(f"Registered chat error filter: {handler_name(handler)}")
2552
+ self.app.chat_error_filters.append(handler)
2553
+
2204
2554
  def register_cache_saved_filter(self, handler):
2205
2555
  self.log(f"Registered cache saved filter: {handler_name(handler)}")
2206
2556
  self.app.cache_saved_filters.append(handler)
2207
2557
 
2558
+ def register_shutdown_handler(self, handler):
2559
+ self.log(f"Registered shutdown handler: {handler_name(handler)}")
2560
+ self.app.shutdown_handlers.append(handler)
2561
+
2208
2562
  def add_static_files(self, ext_dir):
2209
2563
  self.log(f"Registered static files: {ext_dir}")
2210
2564
 
@@ -2237,6 +2591,15 @@ class ExtensionContext:
2237
2591
  self.dbg(f"Registered PATCH: {os.path.join(self.ext_prefix, path)}")
2238
2592
  self.app.server_add_patch.append((os.path.join(self.ext_prefix, path), handler, kwargs))
2239
2593
 
2594
+ def add_importmaps(self, dict):
2595
+ self.app.import_maps.update(dict)
2596
+
2597
+ def add_index_header(self, html):
2598
+ self.app.index_headers.append(html)
2599
+
2600
+ def add_index_footer(self, html):
2601
+ self.app.index_footers.append(html)
2602
+
2240
2603
  def get_config(self):
2241
2604
  return g_config
2242
2605
 
@@ -2264,6 +2627,9 @@ class ExtensionContext:
2264
2627
  self.app.tools[name] = func
2265
2628
  self.app.tool_definitions.append(tool_def)
2266
2629
 
2630
+ def check_auth(self, request):
2631
+ return self.app.check_auth(request)
2632
+
2267
2633
  def get_session(self, request):
2268
2634
  return self.app.get_session(request)
2269
2635
 
@@ -2273,6 +2639,15 @@ class ExtensionContext:
2273
2639
  def get_user_path(self, username=None):
2274
2640
  return self.app.get_user_path(username)
2275
2641
 
2642
+ def should_cancel_thread(self, context):
2643
+ return should_cancel_thread(context)
2644
+
2645
+ def cache_message_inline_data(self, message):
2646
+ return cache_message_inline_data(message)
2647
+
2648
+ def to_content(self, result):
2649
+ return to_content(result)
2650
+
2276
2651
 
2277
2652
  def get_extensions_path():
2278
2653
  return os.getenv("LLMS_EXTENSIONS_DIR", os.path.join(Path.home(), ".llms", "extensions"))
@@ -2454,6 +2829,9 @@ def main():
2454
2829
  parser.add_argument(
2455
2830
  "-s", "--system", default=None, help="System prompt to use for chat completion", metavar="PROMPT"
2456
2831
  )
2832
+ parser.add_argument(
2833
+ "--tools", default=None, help="Tools to use for chat completion (all|none|<tool>,<tool>...)", metavar="TOOLS"
2834
+ )
2457
2835
  parser.add_argument("--image", default=None, help="Image input to use in chat completion")
2458
2836
  parser.add_argument("--audio", default=None, help="Audio input to use in chat completion")
2459
2837
  parser.add_argument("--file", default=None, help="File input to use in chat completion")
@@ -2576,6 +2954,8 @@ def main():
2576
2954
  g_config_path = home_config_path
2577
2955
  g_config = load_config_json(text_from_file(g_config_path))
2578
2956
 
2957
+ g_app.set_config(g_config)
2958
+
2579
2959
  if not g_providers:
2580
2960
  g_providers = json.loads(text_from_file(home_providers_path))
2581
2961
 
@@ -2794,17 +3174,16 @@ def main():
2794
3174
  print(f"\n{model_count} models available from {provider_count} providers")
2795
3175
 
2796
3176
  print_status()
2797
- exit(0)
3177
+ g_app.exit(0)
2798
3178
 
2799
3179
  if cli_args.check is not None:
2800
3180
  # Check validity of models for a provider
2801
3181
  provider_name = cli_args.check
2802
3182
  model_names = extra_args if len(extra_args) > 0 else None
2803
3183
  asyncio.run(check_models(provider_name, model_names))
2804
- exit(0)
3184
+ g_app.exit(0)
2805
3185
 
2806
3186
  if cli_args.serve is not None:
2807
- error_auth_required = create_error_response("Authentication required", "Unauthorized")
2808
3187
  # Disable inactive providers and save to config before starting server
2809
3188
  all_providers = g_config["providers"].keys()
2810
3189
  enabled_providers = list(g_handlers.keys())
@@ -2857,35 +3236,18 @@ def main():
2857
3236
  _log(f"client_max_size set to {client_max_size} bytes ({client_max_size / 1024 / 1024:.1f}MB)")
2858
3237
  app = web.Application(client_max_size=client_max_size)
2859
3238
 
2860
- # Authentication middleware helper
2861
- def check_auth(request):
2862
- """Check if request is authenticated. Returns (is_authenticated, user_data)"""
2863
- if not auth_enabled:
2864
- return True, None
2865
-
2866
- # Check for OAuth session token
2867
- session_token = get_session_token(request)
2868
- if session_token and session_token in g_sessions:
2869
- return True, g_sessions[session_token]
2870
-
2871
- # Check for API key
2872
- auth_header = request.headers.get("Authorization", "")
2873
- if auth_header.startswith("Bearer "):
2874
- api_key = auth_header[7:]
2875
- if api_key:
2876
- return True, {"authProvider": "apikey"}
2877
-
2878
- return False, None
2879
-
2880
3239
  async def chat_handler(request):
2881
3240
  # Check authentication if enabled
2882
- is_authenticated, user_data = check_auth(request)
3241
+ is_authenticated, user_data = g_app.check_auth(request)
2883
3242
  if not is_authenticated:
2884
- return web.json_response(error_auth_required, status=401)
3243
+ return web.json_response(g_app.error_auth_required, status=401)
2885
3244
 
2886
3245
  try:
2887
3246
  chat = await request.json()
2888
- context = {"request": request, "chat": chat}
3247
+ context = {"chat": chat, "request": request, "user": g_app.get_username(request)}
3248
+ metadata = chat.get("metadata", {})
3249
+ context["threadId"] = metadata.get("threadId", None)
3250
+ context["tools"] = metadata.get("tools", "all")
2889
3251
  response = await g_app.chat_completion(chat, context)
2890
3252
  return web.json_response(response)
2891
3253
  except Exception as e:
@@ -2941,9 +3303,9 @@ def main():
2941
3303
 
2942
3304
  async def upload_handler(request):
2943
3305
  # Check authentication if enabled
2944
- is_authenticated, user_data = check_auth(request)
3306
+ is_authenticated, user_data = g_app.check_auth(request)
2945
3307
  if not is_authenticated:
2946
- return web.json_response(error_auth_required, status=401)
3308
+ return web.json_response(g_app.error_auth_required, status=401)
2947
3309
 
2948
3310
  reader = await request.multipart()
2949
3311
 
@@ -3293,7 +3655,7 @@ def main():
3293
3655
  # })
3294
3656
 
3295
3657
  # Not authenticated - return error in expected format
3296
- return web.json_response(error_auth_required, status=401)
3658
+ return web.json_response(g_app.error_auth_required, status=401)
3297
3659
 
3298
3660
  app.router.add_get("/auth", auth_handler)
3299
3661
  app.router.add_get("/auth/github", github_auth_handler)
@@ -3406,8 +3768,28 @@ def main():
3406
3768
  # Serve index.html from root
3407
3769
  async def index_handler(request):
3408
3770
  index_content = read_resource_file_bytes("index.html")
3409
- if index_content is None:
3410
- raise web.HTTPNotFound
3771
+
3772
+ importmaps = {"imports": g_app.import_maps}
3773
+ importmaps_script = '<script type="importmap">\n' + json.dumps(importmaps, indent=4) + "\n</script>"
3774
+ index_content = index_content.replace(
3775
+ b'<script type="importmap"></script>',
3776
+ importmaps_script.encode("utf-8"),
3777
+ )
3778
+
3779
+ if len(g_app.index_headers) > 0:
3780
+ html_header = ""
3781
+ for header in g_app.index_headers:
3782
+ html_header += header
3783
+ # replace </head> with html_header
3784
+ index_content = index_content.replace(b"</head>", html_header.encode("utf-8") + b"\n</head>")
3785
+
3786
+ if len(g_app.index_footers) > 0:
3787
+ html_footer = ""
3788
+ for footer in g_app.index_footers:
3789
+ html_footer += footer
3790
+ # replace </body> with html_footer
3791
+ index_content = index_content.replace(b"</body>", html_footer.encode("utf-8") + b"\n</body>")
3792
+
3411
3793
  return web.Response(body=index_content, content_type="text/html")
3412
3794
 
3413
3795
  app.router.add_get("/", index_handler)
@@ -3427,7 +3809,7 @@ def main():
3427
3809
 
3428
3810
  print(f"Starting server on port {port}...")
3429
3811
  web.run_app(app, host="0.0.0.0", port=port, print=_log)
3430
- exit(0)
3812
+ g_app.exit(0)
3431
3813
 
3432
3814
  if cli_args.enable is not None:
3433
3815
  if cli_args.enable.endswith(","):
@@ -3457,7 +3839,7 @@ def main():
3457
3839
  print_status()
3458
3840
  if len(msgs) > 0:
3459
3841
  print("\n" + "\n".join(msgs))
3460
- exit(0)
3842
+ g_app.exit(0)
3461
3843
 
3462
3844
  if cli_args.disable is not None:
3463
3845
  if cli_args.disable.endswith(","):
@@ -3480,7 +3862,7 @@ def main():
3480
3862
  print(f"\nDisabled provider {provider}")
3481
3863
 
3482
3864
  print_status()
3483
- exit(0)
3865
+ g_app.exit(0)
3484
3866
 
3485
3867
  if cli_args.default is not None:
3486
3868
  default_model = cli_args.default
@@ -3492,7 +3874,7 @@ def main():
3492
3874
  default_text["model"] = default_model
3493
3875
  save_config(g_config)
3494
3876
  print(f"\nDefault model set to: {default_model}")
3495
- exit(0)
3877
+ g_app.exit(0)
3496
3878
 
3497
3879
  if (
3498
3880
  cli_args.chat is not None
@@ -3552,21 +3934,28 @@ def main():
3552
3934
 
3553
3935
  asyncio.run(
3554
3936
  cli_chat(
3555
- chat, image=cli_args.image, audio=cli_args.audio, file=cli_args.file, args=args, raw=cli_args.raw
3937
+ chat,
3938
+ tools=cli_args.tools,
3939
+ image=cli_args.image,
3940
+ audio=cli_args.audio,
3941
+ file=cli_args.file,
3942
+ args=args,
3943
+ raw=cli_args.raw,
3556
3944
  )
3557
3945
  )
3558
- exit(0)
3946
+ g_app.exit(0)
3559
3947
  except Exception as e:
3560
3948
  print(f"{cli_args.logprefix}Error: {e}")
3561
3949
  if cli_args.verbose:
3562
3950
  traceback.print_exc()
3563
- exit(1)
3951
+ g_app.exit(1)
3564
3952
 
3565
3953
  handled = run_extension_cli()
3566
3954
 
3567
3955
  if not handled:
3568
3956
  # show usage from ArgumentParser
3569
3957
  parser.print_help()
3958
+ g_app.exit(0)
3570
3959
 
3571
3960
 
3572
3961
  if __name__ == "__main__":