llms-py 3.0.0b6__py3-none-any.whl → 3.0.0b8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llms/__pycache__/main.cpython-314.pyc +0 -0
- llms/{ui/modules/analytics.mjs → extensions/analytics/ui/index.mjs} +55 -164
- llms/extensions/app/__init__.py +519 -0
- llms/extensions/app/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/app/__pycache__/db.cpython-314.pyc +0 -0
- llms/extensions/app/__pycache__/db_manager.cpython-314.pyc +0 -0
- llms/extensions/app/db.py +641 -0
- llms/extensions/app/db_manager.py +195 -0
- llms/extensions/app/requests.json +9073 -0
- llms/extensions/app/threads.json +15290 -0
- llms/{ui/modules/threads → extensions/app/ui}/Recents.mjs +82 -55
- llms/{ui/modules/threads → extensions/app/ui}/index.mjs +83 -20
- llms/extensions/app/ui/threadStore.mjs +407 -0
- llms/extensions/core_tools/__init__.py +598 -0
- llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
- llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
- llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
- llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +344 -0
- llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +9884 -0
- llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
- llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
- llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
- llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
- llms/extensions/core_tools/ui/index.mjs +650 -0
- llms/extensions/gallery/__init__.py +61 -0
- llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
- llms/extensions/gallery/db.py +298 -0
- llms/extensions/gallery/ui/index.mjs +481 -0
- llms/extensions/katex/__init__.py +6 -0
- llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/katex/ui/README.md +125 -0
- llms/extensions/katex/ui/contrib/auto-render.js +338 -0
- llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
- llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
- llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
- llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
- llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
- llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
- llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
- llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- llms/extensions/katex/ui/index.mjs +92 -0
- llms/extensions/katex/ui/katex-swap.css +1230 -0
- llms/extensions/katex/ui/katex-swap.min.css +1 -0
- llms/extensions/katex/ui/katex.css +1230 -0
- llms/extensions/katex/ui/katex.js +19080 -0
- llms/extensions/katex/ui/katex.min.css +1 -0
- llms/extensions/katex/ui/katex.min.js +1 -0
- llms/extensions/katex/ui/katex.min.mjs +1 -0
- llms/extensions/katex/ui/katex.mjs +18547 -0
- llms/extensions/providers/__init__.py +18 -0
- llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
- llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
- llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
- llms/{providers → extensions/providers}/__pycache__/nvidia.cpython-314.pyc +0 -0
- llms/{providers → extensions/providers}/__pycache__/openai.cpython-314.pyc +0 -0
- llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
- llms/{providers → extensions/providers}/anthropic.py +45 -5
- llms/{providers → extensions/providers}/chutes.py +21 -18
- llms/{providers → extensions/providers}/google.py +99 -27
- llms/{providers → extensions/providers}/nvidia.py +6 -8
- llms/{providers → extensions/providers}/openai.py +3 -6
- llms/{providers → extensions/providers}/openrouter.py +12 -10
- llms/extensions/system_prompts/__init__.py +45 -0
- llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/system_prompts/ui/index.mjs +285 -0
- llms/extensions/system_prompts/ui/prompts.json +1067 -0
- llms/extensions/tools/__init__.py +5 -0
- llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/{ui/modules/tools.mjs → extensions/tools/ui/index.mjs} +12 -10
- llms/index.html +26 -38
- llms/llms.json +20 -1
- llms/main.py +845 -245
- llms/providers-extra.json +0 -32
- llms/ui/App.mjs +18 -20
- llms/ui/ai.mjs +38 -15
- llms/ui/app.css +1440 -59
- llms/ui/ctx.mjs +154 -18
- llms/ui/index.mjs +17 -14
- llms/ui/lib/vue.min.mjs +10 -9
- llms/ui/lib/vue.mjs +1796 -1635
- llms/ui/markdown.mjs +4 -2
- llms/ui/modules/chat/ChatBody.mjs +101 -334
- llms/ui/modules/chat/HomeTools.mjs +12 -0
- llms/ui/modules/chat/SettingsDialog.mjs +1 -1
- llms/ui/modules/chat/index.mjs +351 -314
- llms/ui/modules/layout.mjs +2 -26
- llms/ui/modules/model-selector.mjs +3 -3
- llms/ui/tailwind.input.css +35 -1
- llms/ui/utils.mjs +33 -3
- {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b8.dist-info}/METADATA +1 -1
- llms_py-3.0.0b8.dist-info/RECORD +198 -0
- llms/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
- llms/providers/__pycache__/chutes.cpython-314.pyc +0 -0
- llms/providers/__pycache__/google.cpython-314.pyc +0 -0
- llms/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
- llms/ui/modules/threads/threadStore.mjs +0 -586
- llms_py-3.0.0b6.dist-info/RECORD +0 -66
- {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b8.dist-info}/WHEEL +0 -0
- {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b8.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b8.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b8.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,12 +41,12 @@ try:
|
|
|
40
41
|
except ImportError:
|
|
41
42
|
HAS_PIL = False
|
|
42
43
|
|
|
43
|
-
VERSION = "3.0.
|
|
44
|
+
VERSION = "3.0.0b8"
|
|
44
45
|
_ROOT = None
|
|
45
|
-
DEBUG =
|
|
46
|
-
MOCK = False
|
|
47
|
-
MOCK_DIR = os.getenv("MOCK_DIR")
|
|
46
|
+
DEBUG = os.getenv("DEBUG") == "1"
|
|
48
47
|
MOCK = os.getenv("MOCK") == "1"
|
|
48
|
+
MOCK_DIR = os.getenv("MOCK_DIR")
|
|
49
|
+
DISABLE_EXTENSIONS = (os.getenv("LLMS_DISABLE") or "").split(",")
|
|
49
50
|
g_config_path = None
|
|
50
51
|
g_config = None
|
|
51
52
|
g_providers = None
|
|
@@ -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
|
|
345
|
+
if param_type is int:
|
|
336
346
|
param_type_name = "integer"
|
|
337
|
-
elif param_type
|
|
347
|
+
elif param_type is float:
|
|
338
348
|
param_type_name = "number"
|
|
339
|
-
elif param_type
|
|
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
|
|
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
|
|
@@ -505,12 +601,61 @@ def save_image_to_cache(base64_data, filename, image_info):
|
|
|
505
601
|
subdir = sha256_hash[:2]
|
|
506
602
|
relative_path = f"{subdir}/{save_filename}"
|
|
507
603
|
full_path = get_cache_path(relative_path)
|
|
604
|
+
url = f"/~cache/{relative_path}"
|
|
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))
|
|
508
613
|
|
|
509
|
-
|
|
614
|
+
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
615
|
+
|
|
616
|
+
with open(full_path, "wb") as f:
|
|
617
|
+
f.write(content)
|
|
618
|
+
info = {
|
|
619
|
+
"date": int(time.time()),
|
|
620
|
+
"url": url,
|
|
621
|
+
"size": len(content),
|
|
622
|
+
"type": mimetype,
|
|
623
|
+
"name": filename,
|
|
624
|
+
}
|
|
625
|
+
info.update(file_info)
|
|
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
|
+
|
|
634
|
+
g_app.on_cache_saved_filters({"url": url, "info": info})
|
|
635
|
+
|
|
636
|
+
return url, info
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def save_image_to_cache(base64_data, filename, image_info, ignore_info=False):
|
|
640
|
+
ext = filename.split(".")[-1]
|
|
641
|
+
mimetype = get_file_mime_type(filename)
|
|
642
|
+
content = base64.b64decode(base64_data) if isinstance(base64_data, str) else base64_data
|
|
643
|
+
sha256_hash = hashlib.sha256(content).hexdigest()
|
|
644
|
+
|
|
645
|
+
save_filename = f"{sha256_hash}.{ext}" if ext else sha256_hash
|
|
646
|
+
|
|
647
|
+
# Use first 2 chars for subdir to avoid too many files in one dir
|
|
648
|
+
subdir = sha256_hash[:2]
|
|
649
|
+
relative_path = f"{subdir}/{save_filename}"
|
|
650
|
+
full_path = get_cache_path(relative_path)
|
|
651
|
+
url = f"/~cache/{relative_path}"
|
|
510
652
|
|
|
511
653
|
# if file and its .info.json already exists, return it
|
|
512
654
|
info_path = os.path.splitext(full_path)[0] + ".info.json"
|
|
513
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
|
|
514
659
|
return url, json.load(open(info_path))
|
|
515
660
|
|
|
516
661
|
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
@@ -545,6 +690,10 @@ def save_image_to_cache(base64_data, filename, image_info):
|
|
|
545
690
|
with open(info_path, "w") as f:
|
|
546
691
|
json.dump(info, f)
|
|
547
692
|
|
|
693
|
+
_dbg(f"Saved image and info: {relative_path}")
|
|
694
|
+
|
|
695
|
+
g_app.on_cache_saved_filters({"url": url, "info": info})
|
|
696
|
+
|
|
548
697
|
return url, info
|
|
549
698
|
|
|
550
699
|
|
|
@@ -577,6 +726,27 @@ def chat_to_prompt(chat):
|
|
|
577
726
|
return prompt
|
|
578
727
|
|
|
579
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
|
+
|
|
744
|
+
def chat_to_username(chat):
|
|
745
|
+
if "metadata" in chat and "user" in chat["metadata"]:
|
|
746
|
+
return chat["metadata"]["user"]
|
|
747
|
+
return None
|
|
748
|
+
|
|
749
|
+
|
|
580
750
|
def last_user_prompt(chat):
|
|
581
751
|
prompt = ""
|
|
582
752
|
if "messages" in chat:
|
|
@@ -593,6 +763,49 @@ def last_user_prompt(chat):
|
|
|
593
763
|
return prompt
|
|
594
764
|
|
|
595
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
|
+
|
|
794
|
+
def to_file_info(chat, info=None, response=None):
|
|
795
|
+
prompt = last_user_prompt(chat)
|
|
796
|
+
ret = info or {}
|
|
797
|
+
if chat["model"] and "model" not in ret:
|
|
798
|
+
ret["model"] = chat["model"]
|
|
799
|
+
if prompt and "prompt" not in ret:
|
|
800
|
+
ret["prompt"] = prompt
|
|
801
|
+
if "image_config" in chat:
|
|
802
|
+
ret.update(chat["image_config"])
|
|
803
|
+
user = chat_to_username(chat)
|
|
804
|
+
if user:
|
|
805
|
+
ret["user"] = user
|
|
806
|
+
return ret
|
|
807
|
+
|
|
808
|
+
|
|
596
809
|
# Image Generator Providers
|
|
597
810
|
class GeneratorBase:
|
|
598
811
|
def __init__(self, **kwargs):
|
|
@@ -755,13 +968,17 @@ class OpenAiCompatible:
|
|
|
755
968
|
if not self.models:
|
|
756
969
|
await self.load_models()
|
|
757
970
|
|
|
758
|
-
def
|
|
971
|
+
def model_info(self, model):
|
|
759
972
|
provider_model = self.provider_model(model) or model
|
|
760
973
|
for model_id, model_info in self.models.items():
|
|
761
974
|
if model_id.lower() == provider_model.lower():
|
|
762
|
-
return model_info
|
|
975
|
+
return model_info
|
|
763
976
|
return None
|
|
764
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
|
+
|
|
765
982
|
def provider_model(self, model):
|
|
766
983
|
# convert model to lowercase for case-insensitive comparison
|
|
767
984
|
model_lower = model.lower()
|
|
@@ -823,7 +1040,7 @@ class OpenAiCompatible:
|
|
|
823
1040
|
chat["model"] = self.provider_model(chat["model"]) or chat["model"]
|
|
824
1041
|
|
|
825
1042
|
if "modalities" in chat:
|
|
826
|
-
for modality in chat
|
|
1043
|
+
for modality in chat.get("modalities", []):
|
|
827
1044
|
# use default implementation for text modalities
|
|
828
1045
|
if modality == "text":
|
|
829
1046
|
continue
|
|
@@ -875,13 +1092,14 @@ class OpenAiCompatible:
|
|
|
875
1092
|
_log(f"POST {self.chat_url}")
|
|
876
1093
|
_log(chat_summary(chat))
|
|
877
1094
|
# remove metadata if any (conflicts with some providers, e.g. Z.ai)
|
|
878
|
-
chat.pop("metadata", None)
|
|
1095
|
+
metadata = chat.pop("metadata", None)
|
|
879
1096
|
|
|
880
1097
|
async with aiohttp.ClientSession() as session:
|
|
881
1098
|
started_at = time.time()
|
|
882
1099
|
async with session.post(
|
|
883
1100
|
self.chat_url, headers=self.headers, data=json.dumps(chat), timeout=aiohttp.ClientTimeout(total=120)
|
|
884
1101
|
) as response:
|
|
1102
|
+
chat["metadata"] = metadata
|
|
885
1103
|
return self.to_response(await response_json(response), chat, started_at)
|
|
886
1104
|
|
|
887
1105
|
|
|
@@ -1053,29 +1271,105 @@ def api_providers():
|
|
|
1053
1271
|
return ret
|
|
1054
1272
|
|
|
1055
1273
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1274
|
+
def to_error_message(e):
|
|
1275
|
+
return str(e)
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
def to_error_response(e, stacktrace=False):
|
|
1279
|
+
status = {"errorCode": "Error", "message": to_error_message(e)}
|
|
1280
|
+
if stacktrace:
|
|
1281
|
+
status["stackTrace"] = traceback.format_exc()
|
|
1282
|
+
return {"responseStatus": status}
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
def create_error_response(message, error_code="Error", stack_trace=None):
|
|
1286
|
+
ret = {"responseStatus": {"errorCode": error_code, "message": message}}
|
|
1287
|
+
if stack_trace:
|
|
1288
|
+
ret["responseStatus"]["stackTrace"] = stack_trace
|
|
1289
|
+
return ret
|
|
1290
|
+
|
|
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
|
+
|
|
1300
|
+
def g_chat_request(template=None, text=None, model=None, system_prompt=None):
|
|
1301
|
+
chat_template = g_config["defaults"].get(template or "text")
|
|
1302
|
+
if not chat_template:
|
|
1303
|
+
raise Exception(f"Chat template '{template}' not found")
|
|
1304
|
+
|
|
1305
|
+
chat = chat_template.copy()
|
|
1306
|
+
if model:
|
|
1307
|
+
chat["model"] = model
|
|
1308
|
+
if system_prompt is not None:
|
|
1309
|
+
chat["messages"].insert(0, {"role": "system", "content": system_prompt})
|
|
1310
|
+
if text is not None:
|
|
1311
|
+
if not chat["messages"] or len(chat["messages"]) == 0:
|
|
1312
|
+
chat["messages"] = [{"role": "user", "content": [{"type": "text", "text": ""}]}]
|
|
1313
|
+
|
|
1314
|
+
# replace content of last message if exists, else add
|
|
1315
|
+
last_msg = chat["messages"][-1] if "messages" in chat else None
|
|
1316
|
+
if last_msg and last_msg["role"] == "user":
|
|
1317
|
+
if isinstance(last_msg["content"], list):
|
|
1318
|
+
last_msg["content"][-1]["text"] = text
|
|
1319
|
+
else:
|
|
1320
|
+
last_msg["content"] = text
|
|
1321
|
+
else:
|
|
1322
|
+
chat["messages"].append({"role": "user", "content": text})
|
|
1323
|
+
|
|
1324
|
+
return chat
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
async def g_chat_completion(chat, context=None):
|
|
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"}
|
|
1062
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()
|
|
1063
1345
|
first_exception = None
|
|
1346
|
+
provider_name = "Unknown"
|
|
1064
1347
|
for name in candidate_providers:
|
|
1065
|
-
provider = g_handlers[name]
|
|
1066
|
-
_log(f"provider: {name} {type(provider).__name__}")
|
|
1067
1348
|
try:
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
+
|
|
1070
1367
|
# Inject global tools if present
|
|
1071
1368
|
current_chat = chat.copy()
|
|
1072
1369
|
if g_app.tool_definitions:
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
only_tools_str = chat["metadata"].get("only_tools", "")
|
|
1077
|
-
include_all_tools = only_tools_str == "all"
|
|
1078
|
-
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(",")
|
|
1079
1373
|
|
|
1080
1374
|
if include_all_tools or len(only_tools) > 0:
|
|
1081
1375
|
if "tools" not in current_chat:
|
|
@@ -1087,12 +1381,38 @@ async def chat_completion(chat):
|
|
|
1087
1381
|
if name not in existing_tools and (include_all_tools or name in only_tools):
|
|
1088
1382
|
current_chat["tools"].append(tool_def)
|
|
1089
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
|
+
|
|
1090
1389
|
# Tool execution loop
|
|
1091
|
-
max_iterations =
|
|
1390
|
+
max_iterations = 10
|
|
1092
1391
|
tool_history = []
|
|
1392
|
+
final_response = None
|
|
1393
|
+
|
|
1093
1394
|
for _ in range(max_iterations):
|
|
1395
|
+
if should_cancel_thread(context):
|
|
1396
|
+
return
|
|
1397
|
+
|
|
1094
1398
|
response = await provider.chat(current_chat)
|
|
1095
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
|
+
|
|
1096
1416
|
# Check for tool_calls in the response
|
|
1097
1417
|
choice = response.get("choices", [])[0] if response.get("choices") else {}
|
|
1098
1418
|
message = choice.get("message", {})
|
|
@@ -1102,48 +1422,85 @@ async def chat_completion(chat):
|
|
|
1102
1422
|
# Append the assistant's message with tool calls to history
|
|
1103
1423
|
if "messages" not in current_chat:
|
|
1104
1424
|
current_chat["messages"] = []
|
|
1425
|
+
if "timestamp" not in message:
|
|
1426
|
+
message["timestamp"] = int(time.time() * 1000)
|
|
1105
1427
|
current_chat["messages"].append(message)
|
|
1106
1428
|
tool_history.append(message)
|
|
1107
1429
|
|
|
1108
1430
|
for tool_call in tool_calls:
|
|
1109
1431
|
function_name = tool_call["function"]["name"]
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
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}"
|
|
1122
1447
|
|
|
1123
1448
|
# Append tool result to history
|
|
1124
|
-
tool_msg = {"role": "tool", "tool_call_id": tool_call["id"], "content":
|
|
1449
|
+
tool_msg = {"role": "tool", "tool_call_id": tool_call["id"], "content": to_content(tool_result)}
|
|
1125
1450
|
current_chat["messages"].append(tool_msg)
|
|
1126
1451
|
tool_history.append(tool_msg)
|
|
1127
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
|
+
|
|
1128
1459
|
# Continue loop to send tool results back to LLM
|
|
1129
1460
|
continue
|
|
1130
1461
|
|
|
1131
|
-
# If no tool calls,
|
|
1462
|
+
# If no tool calls, this is the final response
|
|
1132
1463
|
if tool_history:
|
|
1133
1464
|
response["tool_history"] = tool_history
|
|
1134
|
-
|
|
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
|
|
1135
1489
|
|
|
1136
1490
|
except Exception as e:
|
|
1137
1491
|
if first_exception is None:
|
|
1138
1492
|
first_exception = e
|
|
1139
|
-
|
|
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
|
+
|
|
1140
1497
|
continue
|
|
1141
1498
|
|
|
1142
1499
|
# If we get here, all providers failed
|
|
1143
1500
|
raise first_exception
|
|
1144
1501
|
|
|
1145
1502
|
|
|
1146
|
-
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):
|
|
1147
1504
|
if g_default_model:
|
|
1148
1505
|
chat["model"] = g_default_model
|
|
1149
1506
|
|
|
@@ -1218,16 +1575,11 @@ async def cli_chat(chat, image=None, audio=None, file=None, args=None, raw=False
|
|
|
1218
1575
|
printdump(chat)
|
|
1219
1576
|
|
|
1220
1577
|
try:
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
response = await chat_completion(chat)
|
|
1578
|
+
context = {
|
|
1579
|
+
"tools": tools or "all",
|
|
1580
|
+
}
|
|
1581
|
+
response = await g_app.chat_completion(chat, context=context)
|
|
1227
1582
|
|
|
1228
|
-
# Apply post-chat filters
|
|
1229
|
-
for filter_func in g_app.chat_response_filters:
|
|
1230
|
-
response = await filter_func(response, context)
|
|
1231
1583
|
if raw:
|
|
1232
1584
|
print(json.dumps(response, indent=2))
|
|
1233
1585
|
exit(0)
|
|
@@ -1244,28 +1596,32 @@ async def cli_chat(chat, image=None, audio=None, file=None, args=None, raw=False
|
|
|
1244
1596
|
for image in msg["images"]:
|
|
1245
1597
|
image_url = image["image_url"]["url"]
|
|
1246
1598
|
generated_files.append(image_url)
|
|
1599
|
+
if "audios" in msg:
|
|
1600
|
+
for audio in msg["audios"]:
|
|
1601
|
+
audio_url = audio["audio_url"]["url"]
|
|
1602
|
+
generated_files.append(audio_url)
|
|
1247
1603
|
|
|
1248
1604
|
if len(generated_files) > 0:
|
|
1249
1605
|
print("\nSaved files:")
|
|
1250
1606
|
for file in generated_files:
|
|
1251
|
-
if file.startswith("
|
|
1252
|
-
print(get_cache_path(file[
|
|
1253
|
-
|
|
1607
|
+
if file.startswith("/~cache"):
|
|
1608
|
+
print(get_cache_path(file[8:]))
|
|
1609
|
+
print(f"http://localhost:8000/{file}")
|
|
1254
1610
|
else:
|
|
1255
1611
|
print(file)
|
|
1256
1612
|
|
|
1257
1613
|
except HTTPError as e:
|
|
1258
1614
|
# HTTP error (4xx, 5xx)
|
|
1259
1615
|
print(f"{e}:\n{e.body}")
|
|
1260
|
-
exit(1)
|
|
1616
|
+
g_app.exit(1)
|
|
1261
1617
|
except aiohttp.ClientConnectionError as e:
|
|
1262
1618
|
# Connection issues
|
|
1263
1619
|
print(f"Connection error: {e}")
|
|
1264
|
-
exit(1)
|
|
1620
|
+
g_app.exit(1)
|
|
1265
1621
|
except asyncio.TimeoutError as e:
|
|
1266
1622
|
# Timeout
|
|
1267
1623
|
print(f"Timeout error: {e}")
|
|
1268
|
-
exit(1)
|
|
1624
|
+
g_app.exit(1)
|
|
1269
1625
|
|
|
1270
1626
|
|
|
1271
1627
|
def config_str(key):
|
|
@@ -1288,7 +1644,7 @@ def init_llms(config, providers):
|
|
|
1288
1644
|
# iterate over config and replace $ENV with env value
|
|
1289
1645
|
for key, value in g_config.items():
|
|
1290
1646
|
if isinstance(value, str) and value.startswith("$"):
|
|
1291
|
-
g_config[key] = os.
|
|
1647
|
+
g_config[key] = os.getenv(value[1:], "")
|
|
1292
1648
|
|
|
1293
1649
|
# if g_verbose:
|
|
1294
1650
|
# printdump(g_config)
|
|
@@ -1326,11 +1682,11 @@ def create_provider_kwargs(definition, provider=None):
|
|
|
1326
1682
|
if "api_key" in provider:
|
|
1327
1683
|
value = provider["api_key"]
|
|
1328
1684
|
if isinstance(value, str) and value.startswith("$"):
|
|
1329
|
-
provider["api_key"] = os.
|
|
1685
|
+
provider["api_key"] = os.getenv(value[1:], "")
|
|
1330
1686
|
|
|
1331
1687
|
if "api_key" not in provider and "env" in provider:
|
|
1332
1688
|
for env_var in provider["env"]:
|
|
1333
|
-
val = os.
|
|
1689
|
+
val = os.getenv(env_var)
|
|
1334
1690
|
if val:
|
|
1335
1691
|
provider["api_key"] = val
|
|
1336
1692
|
break
|
|
@@ -1467,11 +1823,11 @@ def print_status():
|
|
|
1467
1823
|
|
|
1468
1824
|
|
|
1469
1825
|
def home_llms_path(filename):
|
|
1470
|
-
return f"{os.
|
|
1826
|
+
return f"{os.getenv('HOME')}/.llms/{filename}"
|
|
1471
1827
|
|
|
1472
1828
|
|
|
1473
|
-
def get_cache_path(
|
|
1474
|
-
return home_llms_path(f"cache/{
|
|
1829
|
+
def get_cache_path(path=""):
|
|
1830
|
+
return home_llms_path(f"cache/{path}") if path else home_llms_path("cache")
|
|
1475
1831
|
|
|
1476
1832
|
|
|
1477
1833
|
def get_config_path():
|
|
@@ -1480,8 +1836,8 @@ def get_config_path():
|
|
|
1480
1836
|
"./llms.json",
|
|
1481
1837
|
home_config_path,
|
|
1482
1838
|
]
|
|
1483
|
-
if os.
|
|
1484
|
-
check_paths.insert(0, os.
|
|
1839
|
+
if os.getenv("LLMS_CONFIG_PATH"):
|
|
1840
|
+
check_paths.insert(0, os.getenv("LLMS_CONFIG_PATH"))
|
|
1485
1841
|
|
|
1486
1842
|
for check_path in check_paths:
|
|
1487
1843
|
g_config_path = os.path.normpath(os.path.join(os.path.dirname(__file__), check_path))
|
|
@@ -1951,14 +2307,41 @@ class AppExtensions:
|
|
|
1951
2307
|
def __init__(self, cli_args, extra_args):
|
|
1952
2308
|
self.cli_args = cli_args
|
|
1953
2309
|
self.extra_args = extra_args
|
|
2310
|
+
self.config = None
|
|
2311
|
+
self.error_auth_required = create_error_response("Authentication required", "Unauthorized")
|
|
1954
2312
|
self.ui_extensions = []
|
|
1955
2313
|
self.chat_request_filters = []
|
|
2314
|
+
self.chat_tool_filters = []
|
|
1956
2315
|
self.chat_response_filters = []
|
|
2316
|
+
self.chat_error_filters = []
|
|
1957
2317
|
self.server_add_get = []
|
|
1958
2318
|
self.server_add_post = []
|
|
1959
|
-
self.
|
|
2319
|
+
self.server_add_put = []
|
|
2320
|
+
self.server_add_delete = []
|
|
2321
|
+
self.server_add_patch = []
|
|
2322
|
+
self.cache_saved_filters = []
|
|
2323
|
+
self.shutdown_handlers = []
|
|
1960
2324
|
self.tools = {}
|
|
1961
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
|
+
}
|
|
1962
2345
|
self.all_providers = [
|
|
1963
2346
|
OpenAiCompatible,
|
|
1964
2347
|
MistralProvider,
|
|
@@ -1980,11 +2363,108 @@ class AppExtensions:
|
|
|
1980
2363
|
"16:9": "1344×768",
|
|
1981
2364
|
"21:9": "1536×672",
|
|
1982
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
|
|
2403
|
+
|
|
2404
|
+
def get_session(self, request):
|
|
2405
|
+
session_token = get_session_token(request)
|
|
2406
|
+
|
|
2407
|
+
if not session_token or session_token not in g_sessions:
|
|
2408
|
+
return None
|
|
2409
|
+
|
|
2410
|
+
session_data = g_sessions[session_token]
|
|
2411
|
+
return session_data
|
|
2412
|
+
|
|
2413
|
+
def get_username(self, request):
|
|
2414
|
+
session = self.get_session(request)
|
|
2415
|
+
if session:
|
|
2416
|
+
return session.get("userName")
|
|
2417
|
+
return None
|
|
2418
|
+
|
|
2419
|
+
def get_user_path(self, username=None):
|
|
2420
|
+
if username:
|
|
2421
|
+
return home_llms_path(os.path.join("user", username))
|
|
2422
|
+
return home_llms_path(os.path.join("user", "default"))
|
|
2423
|
+
|
|
2424
|
+
def chat_request(self, template=None, text=None, model=None, system_prompt=None):
|
|
2425
|
+
return g_chat_request(template=template, text=text, model=model, system_prompt=system_prompt)
|
|
2426
|
+
|
|
2427
|
+
async def chat_completion(self, chat, context=None):
|
|
2428
|
+
response = await g_chat_completion(chat, context)
|
|
2429
|
+
return response
|
|
2430
|
+
|
|
2431
|
+
def on_cache_saved_filters(self, context):
|
|
2432
|
+
# _log(f"on_cache_saved_filters {len(self.cache_saved_filters)}: {context['url']}")
|
|
2433
|
+
for filter_func in self.cache_saved_filters:
|
|
2434
|
+
filter_func(context)
|
|
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
|
+
|
|
2455
|
+
|
|
2456
|
+
def handler_name(handler):
|
|
2457
|
+
if hasattr(handler, "__name__"):
|
|
2458
|
+
return handler.__name__
|
|
2459
|
+
return "unknown"
|
|
1983
2460
|
|
|
1984
2461
|
|
|
1985
2462
|
class ExtensionContext:
|
|
1986
2463
|
def __init__(self, app, path):
|
|
1987
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
|
|
1988
2468
|
self.path = path
|
|
1989
2469
|
self.name = os.path.basename(path)
|
|
1990
2470
|
if self.name.endswith(".py"):
|
|
@@ -1994,16 +2474,30 @@ class ExtensionContext:
|
|
|
1994
2474
|
self.MOCK_DIR = MOCK_DIR
|
|
1995
2475
|
self.debug = DEBUG
|
|
1996
2476
|
self.verbose = g_verbose
|
|
2477
|
+
self.aspect_ratios = app.aspect_ratios
|
|
2478
|
+
self.request_args = app.request_args
|
|
1997
2479
|
|
|
1998
2480
|
def chat_to_prompt(self, chat):
|
|
1999
2481
|
return chat_to_prompt(chat)
|
|
2000
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
|
+
|
|
2001
2489
|
def last_user_prompt(self, chat):
|
|
2002
2490
|
return last_user_prompt(chat)
|
|
2003
2491
|
|
|
2492
|
+
def to_file_info(self, chat, info=None, response=None):
|
|
2493
|
+
return to_file_info(chat, info=info, response=response)
|
|
2494
|
+
|
|
2004
2495
|
def save_image_to_cache(self, base64_data, filename, image_info):
|
|
2005
2496
|
return save_image_to_cache(base64_data, filename, image_info)
|
|
2006
2497
|
|
|
2498
|
+
def save_bytes_to_cache(self, bytes_data, filename, file_info):
|
|
2499
|
+
return save_bytes_to_cache(bytes_data, filename, file_info)
|
|
2500
|
+
|
|
2007
2501
|
def text_from_file(self, path):
|
|
2008
2502
|
return text_from_file(path)
|
|
2009
2503
|
|
|
@@ -2026,8 +2520,14 @@ class ExtensionContext:
|
|
|
2026
2520
|
if self.verbose:
|
|
2027
2521
|
print(traceback.format_exc(), flush=True)
|
|
2028
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
|
+
|
|
2029
2529
|
def add_provider(self, provider):
|
|
2030
|
-
self.log(f"Registered provider: {provider}")
|
|
2530
|
+
self.log(f"Registered provider: {provider.__name__}")
|
|
2031
2531
|
self.app.all_providers.append(provider)
|
|
2032
2532
|
|
|
2033
2533
|
def register_ui_extension(self, index):
|
|
@@ -2036,13 +2536,29 @@ class ExtensionContext:
|
|
|
2036
2536
|
self.app.ui_extensions.append({"id": self.name, "path": path})
|
|
2037
2537
|
|
|
2038
2538
|
def register_chat_request_filter(self, handler):
|
|
2039
|
-
self.log(f"Registered chat request filter: {handler}")
|
|
2539
|
+
self.log(f"Registered chat request filter: {handler_name(handler)}")
|
|
2040
2540
|
self.app.chat_request_filters.append(handler)
|
|
2041
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
|
+
|
|
2042
2546
|
def register_chat_response_filter(self, handler):
|
|
2043
|
-
self.log(f"Registered chat response filter: {handler}")
|
|
2547
|
+
self.log(f"Registered chat response filter: {handler_name(handler)}")
|
|
2044
2548
|
self.app.chat_response_filters.append(handler)
|
|
2045
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
|
+
|
|
2554
|
+
def register_cache_saved_filter(self, handler):
|
|
2555
|
+
self.log(f"Registered cache saved filter: {handler_name(handler)}")
|
|
2556
|
+
self.app.cache_saved_filters.append(handler)
|
|
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
|
+
|
|
2046
2562
|
def add_static_files(self, ext_dir):
|
|
2047
2563
|
self.log(f"Registered static files: {ext_dir}")
|
|
2048
2564
|
|
|
@@ -2063,11 +2579,38 @@ class ExtensionContext:
|
|
|
2063
2579
|
self.dbg(f"Registered POST: {os.path.join(self.ext_prefix, path)}")
|
|
2064
2580
|
self.app.server_add_post.append((os.path.join(self.ext_prefix, path), handler, kwargs))
|
|
2065
2581
|
|
|
2582
|
+
def add_put(self, path, handler, **kwargs):
|
|
2583
|
+
self.dbg(f"Registered PUT: {os.path.join(self.ext_prefix, path)}")
|
|
2584
|
+
self.app.server_add_put.append((os.path.join(self.ext_prefix, path), handler, kwargs))
|
|
2585
|
+
|
|
2586
|
+
def add_delete(self, path, handler, **kwargs):
|
|
2587
|
+
self.dbg(f"Registered DELETE: {os.path.join(self.ext_prefix, path)}")
|
|
2588
|
+
self.app.server_add_delete.append((os.path.join(self.ext_prefix, path), handler, kwargs))
|
|
2589
|
+
|
|
2590
|
+
def add_patch(self, path, handler, **kwargs):
|
|
2591
|
+
self.dbg(f"Registered PATCH: {os.path.join(self.ext_prefix, path)}")
|
|
2592
|
+
self.app.server_add_patch.append((os.path.join(self.ext_prefix, path), handler, kwargs))
|
|
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
|
+
|
|
2066
2603
|
def get_config(self):
|
|
2067
2604
|
return g_config
|
|
2068
2605
|
|
|
2069
|
-
def
|
|
2070
|
-
return
|
|
2606
|
+
def get_cache_path(self, path=""):
|
|
2607
|
+
return get_cache_path(path)
|
|
2608
|
+
|
|
2609
|
+
def chat_request(self, template=None, text=None, model=None, system_prompt=None):
|
|
2610
|
+
return self.app.chat_request(template=template, text=text, model=model, system_prompt=system_prompt)
|
|
2611
|
+
|
|
2612
|
+
def chat_completion(self, chat, context=None):
|
|
2613
|
+
return self.app.chat_completion(chat, context=context)
|
|
2071
2614
|
|
|
2072
2615
|
def get_providers(self):
|
|
2073
2616
|
return g_handlers
|
|
@@ -2075,21 +2618,6 @@ class ExtensionContext:
|
|
|
2075
2618
|
def get_provider(self, name):
|
|
2076
2619
|
return g_handlers.get(name)
|
|
2077
2620
|
|
|
2078
|
-
def get_session(self, request):
|
|
2079
|
-
session_token = get_session_token(request)
|
|
2080
|
-
|
|
2081
|
-
if not session_token or session_token not in g_sessions:
|
|
2082
|
-
return None
|
|
2083
|
-
|
|
2084
|
-
session_data = g_sessions[session_token]
|
|
2085
|
-
return session_data
|
|
2086
|
-
|
|
2087
|
-
def get_username(self, request):
|
|
2088
|
-
session = self.get_session(request)
|
|
2089
|
-
if session:
|
|
2090
|
-
return session.get("userName")
|
|
2091
|
-
return None
|
|
2092
|
-
|
|
2093
2621
|
def register_tool(self, func, tool_def=None):
|
|
2094
2622
|
if tool_def is None:
|
|
2095
2623
|
tool_def = function_to_tool_definition(func)
|
|
@@ -2099,44 +2627,83 @@ class ExtensionContext:
|
|
|
2099
2627
|
self.app.tools[name] = func
|
|
2100
2628
|
self.app.tool_definitions.append(tool_def)
|
|
2101
2629
|
|
|
2630
|
+
def check_auth(self, request):
|
|
2631
|
+
return self.app.check_auth(request)
|
|
2102
2632
|
|
|
2103
|
-
def
|
|
2104
|
-
|
|
2105
|
-
if not providers_path.exists():
|
|
2106
|
-
return
|
|
2633
|
+
def get_session(self, request):
|
|
2634
|
+
return self.app.get_session(request)
|
|
2107
2635
|
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
continue
|
|
2636
|
+
def get_username(self, request):
|
|
2637
|
+
return self.app.get_username(request)
|
|
2111
2638
|
|
|
2112
|
-
|
|
2113
|
-
|
|
2639
|
+
def get_user_path(self, username=None):
|
|
2640
|
+
return self.app.get_user_path(username)
|
|
2114
2641
|
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
if callable(install_func):
|
|
2124
|
-
install_func(ExtensionContext(g_app, item_path))
|
|
2125
|
-
_log(f"Loaded builtin extension: {module_name}")
|
|
2126
|
-
except Exception as e:
|
|
2127
|
-
_err(f"Failed to load builtin extension {module_name}", e)
|
|
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)
|
|
2128
2650
|
|
|
2129
2651
|
|
|
2130
2652
|
def get_extensions_path():
|
|
2131
|
-
return os.
|
|
2653
|
+
return os.getenv("LLMS_EXTENSIONS_DIR", os.path.join(Path.home(), ".llms", "extensions"))
|
|
2132
2654
|
|
|
2133
2655
|
|
|
2134
|
-
def
|
|
2656
|
+
def get_disabled_extensions():
|
|
2657
|
+
ret = DISABLE_EXTENSIONS.copy()
|
|
2658
|
+
if g_config:
|
|
2659
|
+
for ext in g_config.get("disable_extensions", []):
|
|
2660
|
+
if ext not in ret:
|
|
2661
|
+
ret.append(ext)
|
|
2662
|
+
return ret
|
|
2663
|
+
|
|
2664
|
+
|
|
2665
|
+
def get_extensions_dirs():
|
|
2666
|
+
"""
|
|
2667
|
+
Returns a list of extension directories.
|
|
2668
|
+
"""
|
|
2135
2669
|
extensions_path = get_extensions_path()
|
|
2136
2670
|
os.makedirs(extensions_path, exist_ok=True)
|
|
2137
2671
|
|
|
2138
|
-
|
|
2139
|
-
|
|
2672
|
+
# allow overriding builtin extensions
|
|
2673
|
+
override_extensions = []
|
|
2674
|
+
if os.path.exists(extensions_path):
|
|
2675
|
+
override_extensions = os.listdir(extensions_path)
|
|
2676
|
+
|
|
2677
|
+
ret = []
|
|
2678
|
+
disabled_extensions = get_disabled_extensions()
|
|
2679
|
+
|
|
2680
|
+
builtin_extensions_dir = _ROOT / "extensions"
|
|
2681
|
+
if os.path.exists(builtin_extensions_dir):
|
|
2682
|
+
for item in os.listdir(builtin_extensions_dir):
|
|
2683
|
+
if os.path.isdir(os.path.join(builtin_extensions_dir, item)):
|
|
2684
|
+
if item in override_extensions:
|
|
2685
|
+
continue
|
|
2686
|
+
if item in disabled_extensions:
|
|
2687
|
+
continue
|
|
2688
|
+
ret.append(os.path.join(builtin_extensions_dir, item))
|
|
2689
|
+
|
|
2690
|
+
if os.path.exists(extensions_path):
|
|
2691
|
+
for item in os.listdir(extensions_path):
|
|
2692
|
+
if os.path.isdir(os.path.join(extensions_path, item)):
|
|
2693
|
+
if item in disabled_extensions:
|
|
2694
|
+
continue
|
|
2695
|
+
ret.append(os.path.join(extensions_path, item))
|
|
2696
|
+
|
|
2697
|
+
return ret
|
|
2698
|
+
|
|
2699
|
+
|
|
2700
|
+
def init_extensions(parser):
|
|
2701
|
+
"""
|
|
2702
|
+
Initializes extensions by loading their __init__.py files and calling the __parser__ function if it exists.
|
|
2703
|
+
"""
|
|
2704
|
+
for item_path in get_extensions_dirs():
|
|
2705
|
+
item = os.path.basename(item_path)
|
|
2706
|
+
|
|
2140
2707
|
if os.path.isdir(item_path):
|
|
2141
2708
|
try:
|
|
2142
2709
|
# check for __parser__ function if exists in __init.__.py and call it with parser
|
|
@@ -2161,25 +2728,28 @@ def install_extensions():
|
|
|
2161
2728
|
Scans ensure ~/.llms/extensions/ for directories with __init__.py and loads them as extensions.
|
|
2162
2729
|
Calls the `__install__(ctx)` function in the extension module.
|
|
2163
2730
|
"""
|
|
2164
|
-
extensions_path = get_extensions_path()
|
|
2165
|
-
os.makedirs(extensions_path, exist_ok=True)
|
|
2166
2731
|
|
|
2167
|
-
|
|
2732
|
+
extension_dirs = get_extensions_dirs()
|
|
2733
|
+
ext_count = len(list(extension_dirs))
|
|
2168
2734
|
if ext_count == 0:
|
|
2169
2735
|
_log("No extensions found")
|
|
2170
2736
|
return
|
|
2171
2737
|
|
|
2738
|
+
disabled_extensions = get_disabled_extensions()
|
|
2739
|
+
if len(disabled_extensions) > 0:
|
|
2740
|
+
_log(f"Disabled extensions: {', '.join(disabled_extensions)}")
|
|
2741
|
+
|
|
2172
2742
|
_log(f"Installing {ext_count} extension{'' if ext_count == 1 else 's'}...")
|
|
2173
2743
|
|
|
2174
|
-
|
|
2744
|
+
for item_path in extension_dirs:
|
|
2745
|
+
item = os.path.basename(item_path)
|
|
2175
2746
|
|
|
2176
|
-
for item in os.listdir(extensions_path):
|
|
2177
|
-
item_path = os.path.join(extensions_path, item)
|
|
2178
2747
|
if os.path.isdir(item_path):
|
|
2179
|
-
|
|
2180
|
-
|
|
2748
|
+
sys.path.append(item_path)
|
|
2749
|
+
try:
|
|
2181
2750
|
ctx = ExtensionContext(g_app, item_path)
|
|
2182
|
-
|
|
2751
|
+
init_file = os.path.join(item_path, "__init__.py")
|
|
2752
|
+
if os.path.exists(init_file):
|
|
2183
2753
|
spec = importlib.util.spec_from_file_location(item, init_file)
|
|
2184
2754
|
if spec and spec.loader:
|
|
2185
2755
|
module = importlib.util.module_from_spec(spec)
|
|
@@ -2194,20 +2764,20 @@ def install_extensions():
|
|
|
2194
2764
|
_dbg(f"Extension {item} has no __install__ function")
|
|
2195
2765
|
else:
|
|
2196
2766
|
_dbg(f"Extension {item} has no __init__.py")
|
|
2767
|
+
else:
|
|
2768
|
+
_dbg(f"Extension {init_file} not found")
|
|
2197
2769
|
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2770
|
+
# if ui folder exists, serve as static files at /ext/{item}/
|
|
2771
|
+
ui_path = os.path.join(item_path, "ui")
|
|
2772
|
+
if os.path.exists(ui_path):
|
|
2773
|
+
ctx.add_static_files(ui_path)
|
|
2202
2774
|
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2775
|
+
# Register UI extension if index.mjs exists (/ext/{item}/index.mjs)
|
|
2776
|
+
if os.path.exists(os.path.join(ui_path, "index.mjs")):
|
|
2777
|
+
ctx.register_ui_extension("index.mjs")
|
|
2206
2778
|
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
else:
|
|
2210
|
-
_dbg(f"Extension {init_file} not found")
|
|
2779
|
+
except Exception as e:
|
|
2780
|
+
_err(f"Failed to install extension {item}", e)
|
|
2211
2781
|
else:
|
|
2212
2782
|
_dbg(f"Extension {item} not found: {item_path} is not a directory {os.path.exists(item_path)}")
|
|
2213
2783
|
|
|
@@ -2216,11 +2786,9 @@ def run_extension_cli():
|
|
|
2216
2786
|
"""
|
|
2217
2787
|
Run the CLI for an extension.
|
|
2218
2788
|
"""
|
|
2219
|
-
|
|
2220
|
-
|
|
2789
|
+
for item_path in get_extensions_dirs():
|
|
2790
|
+
item = os.path.basename(item_path)
|
|
2221
2791
|
|
|
2222
|
-
for item in os.listdir(extensions_path):
|
|
2223
|
-
item_path = os.path.join(extensions_path, item)
|
|
2224
2792
|
if os.path.isdir(item_path):
|
|
2225
2793
|
init_file = os.path.join(item_path, "__init__.py")
|
|
2226
2794
|
if os.path.exists(init_file):
|
|
@@ -2235,8 +2803,8 @@ def run_extension_cli():
|
|
|
2235
2803
|
# Check for __run__ function if exists in __init__.py and call it with ctx
|
|
2236
2804
|
run_func = getattr(module, "__run__", None)
|
|
2237
2805
|
if callable(run_func):
|
|
2806
|
+
_log(f"Running extension {item}...")
|
|
2238
2807
|
handled = run_func(ctx)
|
|
2239
|
-
_log(f"Extension {item} was run")
|
|
2240
2808
|
return handled
|
|
2241
2809
|
|
|
2242
2810
|
except Exception as e:
|
|
@@ -2247,6 +2815,11 @@ def run_extension_cli():
|
|
|
2247
2815
|
def main():
|
|
2248
2816
|
global _ROOT, g_verbose, g_default_model, g_logprefix, g_providers, g_config, g_config_path, g_app
|
|
2249
2817
|
|
|
2818
|
+
_ROOT = os.getenv("LLMS_ROOT", resolve_root())
|
|
2819
|
+
if not _ROOT:
|
|
2820
|
+
print("Resource root not found")
|
|
2821
|
+
exit(1)
|
|
2822
|
+
|
|
2250
2823
|
parser = argparse.ArgumentParser(description=f"llms v{VERSION}")
|
|
2251
2824
|
parser.add_argument("--config", default=None, help="Path to config file", metavar="FILE")
|
|
2252
2825
|
parser.add_argument("--providers", default=None, help="Path to models.dev providers file", metavar="FILE")
|
|
@@ -2256,6 +2829,9 @@ def main():
|
|
|
2256
2829
|
parser.add_argument(
|
|
2257
2830
|
"-s", "--system", default=None, help="System prompt to use for chat completion", metavar="PROMPT"
|
|
2258
2831
|
)
|
|
2832
|
+
parser.add_argument(
|
|
2833
|
+
"--tools", default=None, help="Tools to use for chat completion (all|none|<tool>,<tool>...)", metavar="TOOLS"
|
|
2834
|
+
)
|
|
2259
2835
|
parser.add_argument("--image", default=None, help="Image input to use in chat completion")
|
|
2260
2836
|
parser.add_argument("--audio", default=None, help="Audio input to use in chat completion")
|
|
2261
2837
|
parser.add_argument("--file", default=None, help="File input to use in chat completion")
|
|
@@ -2283,9 +2859,7 @@ def main():
|
|
|
2283
2859
|
|
|
2284
2860
|
parser.add_argument("--init", action="store_true", help="Create a default llms.json")
|
|
2285
2861
|
parser.add_argument("--update-providers", action="store_true", help="Update local models.dev providers.json")
|
|
2286
|
-
parser.add_argument("--update-extensions", action="store_true", help="Update installed extensions")
|
|
2287
2862
|
|
|
2288
|
-
parser.add_argument("--root", default=None, help="Change root directory for UI files", metavar="PATH")
|
|
2289
2863
|
parser.add_argument("--logprefix", default="", help="Prefix used in log messages", metavar="PREFIX")
|
|
2290
2864
|
parser.add_argument("--verbose", action="store_true", help="Verbose output")
|
|
2291
2865
|
|
|
@@ -2323,7 +2897,7 @@ def main():
|
|
|
2323
2897
|
g_app = AppExtensions(cli_args, extra_args)
|
|
2324
2898
|
|
|
2325
2899
|
# Check for verbose mode from CLI argument or environment variables
|
|
2326
|
-
verbose_env = os.
|
|
2900
|
+
verbose_env = os.getenv("VERBOSE", "").lower()
|
|
2327
2901
|
if cli_args.verbose or verbose_env in ("1", "true"):
|
|
2328
2902
|
g_verbose = True
|
|
2329
2903
|
# printdump(cli_args)
|
|
@@ -2332,11 +2906,6 @@ def main():
|
|
|
2332
2906
|
if cli_args.logprefix:
|
|
2333
2907
|
g_logprefix = cli_args.logprefix
|
|
2334
2908
|
|
|
2335
|
-
_ROOT = Path(cli_args.root) if cli_args.root else resolve_root()
|
|
2336
|
-
if not _ROOT:
|
|
2337
|
-
print("Resource root not found")
|
|
2338
|
-
exit(1)
|
|
2339
|
-
|
|
2340
2909
|
home_config_path = home_llms_path("llms.json")
|
|
2341
2910
|
home_providers_path = home_llms_path("providers.json")
|
|
2342
2911
|
home_providers_extra_path = home_llms_path("providers-extra.json")
|
|
@@ -2385,6 +2954,8 @@ def main():
|
|
|
2385
2954
|
g_config_path = home_config_path
|
|
2386
2955
|
g_config = load_config_json(text_from_file(g_config_path))
|
|
2387
2956
|
|
|
2957
|
+
g_app.set_config(g_config)
|
|
2958
|
+
|
|
2388
2959
|
if not g_providers:
|
|
2389
2960
|
g_providers = json.loads(text_from_file(home_providers_path))
|
|
2390
2961
|
|
|
@@ -2397,7 +2968,7 @@ def main():
|
|
|
2397
2968
|
if (
|
|
2398
2969
|
os.path.exists(home_providers_path)
|
|
2399
2970
|
and (time.time() - os.path.getmtime(home_providers_path)) > 86400
|
|
2400
|
-
and os.
|
|
2971
|
+
and os.getenv("LLMS_DISABLE_UPDATE", "") != "1"
|
|
2401
2972
|
):
|
|
2402
2973
|
try:
|
|
2403
2974
|
asyncio.run(update_providers(home_providers_path))
|
|
@@ -2554,12 +3125,10 @@ def main():
|
|
|
2554
3125
|
asyncio.run(update_extensions(cli_args.update))
|
|
2555
3126
|
exit(0)
|
|
2556
3127
|
|
|
2557
|
-
|
|
3128
|
+
install_extensions()
|
|
2558
3129
|
|
|
2559
3130
|
asyncio.run(reload_providers())
|
|
2560
3131
|
|
|
2561
|
-
install_extensions()
|
|
2562
|
-
|
|
2563
3132
|
# print names
|
|
2564
3133
|
_log(f"enabled providers: {', '.join(g_handlers.keys())}")
|
|
2565
3134
|
|
|
@@ -2605,14 +3174,14 @@ def main():
|
|
|
2605
3174
|
print(f"\n{model_count} models available from {provider_count} providers")
|
|
2606
3175
|
|
|
2607
3176
|
print_status()
|
|
2608
|
-
exit(0)
|
|
3177
|
+
g_app.exit(0)
|
|
2609
3178
|
|
|
2610
3179
|
if cli_args.check is not None:
|
|
2611
3180
|
# Check validity of models for a provider
|
|
2612
3181
|
provider_name = cli_args.check
|
|
2613
3182
|
model_names = extra_args if len(extra_args) > 0 else None
|
|
2614
3183
|
asyncio.run(check_models(provider_name, model_names))
|
|
2615
|
-
exit(0)
|
|
3184
|
+
g_app.exit(0)
|
|
2616
3185
|
|
|
2617
3186
|
if cli_args.serve is not None:
|
|
2618
3187
|
# Disable inactive providers and save to config before starting server
|
|
@@ -2645,8 +3214,8 @@ def main():
|
|
|
2645
3214
|
if client_secret.startswith("$"):
|
|
2646
3215
|
client_secret = client_secret[1:]
|
|
2647
3216
|
|
|
2648
|
-
client_id = os.
|
|
2649
|
-
client_secret = os.
|
|
3217
|
+
client_id = os.getenv(client_id, client_id)
|
|
3218
|
+
client_secret = os.getenv(client_secret, client_secret)
|
|
2650
3219
|
|
|
2651
3220
|
if (
|
|
2652
3221
|
not client_id
|
|
@@ -2667,59 +3236,22 @@ def main():
|
|
|
2667
3236
|
_log(f"client_max_size set to {client_max_size} bytes ({client_max_size / 1024 / 1024:.1f}MB)")
|
|
2668
3237
|
app = web.Application(client_max_size=client_max_size)
|
|
2669
3238
|
|
|
2670
|
-
# Authentication middleware helper
|
|
2671
|
-
def check_auth(request):
|
|
2672
|
-
"""Check if request is authenticated. Returns (is_authenticated, user_data)"""
|
|
2673
|
-
if not auth_enabled:
|
|
2674
|
-
return True, None
|
|
2675
|
-
|
|
2676
|
-
# Check for OAuth session token
|
|
2677
|
-
session_token = get_session_token(request)
|
|
2678
|
-
if session_token and session_token in g_sessions:
|
|
2679
|
-
return True, g_sessions[session_token]
|
|
2680
|
-
|
|
2681
|
-
# Check for API key
|
|
2682
|
-
auth_header = request.headers.get("Authorization", "")
|
|
2683
|
-
if auth_header.startswith("Bearer "):
|
|
2684
|
-
api_key = auth_header[7:]
|
|
2685
|
-
if api_key:
|
|
2686
|
-
return True, {"authProvider": "apikey"}
|
|
2687
|
-
|
|
2688
|
-
return False, None
|
|
2689
|
-
|
|
2690
3239
|
async def chat_handler(request):
|
|
2691
3240
|
# Check authentication if enabled
|
|
2692
|
-
is_authenticated, user_data = check_auth(request)
|
|
3241
|
+
is_authenticated, user_data = g_app.check_auth(request)
|
|
2693
3242
|
if not is_authenticated:
|
|
2694
|
-
return web.json_response(
|
|
2695
|
-
{
|
|
2696
|
-
"error": {
|
|
2697
|
-
"message": "Authentication required",
|
|
2698
|
-
"type": "authentication_error",
|
|
2699
|
-
"code": "unauthorized",
|
|
2700
|
-
}
|
|
2701
|
-
},
|
|
2702
|
-
status=401,
|
|
2703
|
-
)
|
|
3243
|
+
return web.json_response(g_app.error_auth_required, status=401)
|
|
2704
3244
|
|
|
2705
3245
|
try:
|
|
2706
3246
|
chat = await request.json()
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
context =
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
response = await chat_completion(chat)
|
|
2714
|
-
|
|
2715
|
-
# Apply post-chat filters
|
|
2716
|
-
# Apply post-chat filters
|
|
2717
|
-
for filter_func in g_app.chat_response_filters:
|
|
2718
|
-
response = await filter_func(response, context)
|
|
2719
|
-
|
|
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")
|
|
3251
|
+
response = await g_app.chat_completion(chat, context)
|
|
2720
3252
|
return web.json_response(response)
|
|
2721
3253
|
except Exception as e:
|
|
2722
|
-
return web.json_response(
|
|
3254
|
+
return web.json_response(to_error_response(e), status=500)
|
|
2723
3255
|
|
|
2724
3256
|
app.router.add_post("/v1/chat/completions", chat_handler)
|
|
2725
3257
|
|
|
@@ -2771,18 +3303,9 @@ def main():
|
|
|
2771
3303
|
|
|
2772
3304
|
async def upload_handler(request):
|
|
2773
3305
|
# Check authentication if enabled
|
|
2774
|
-
is_authenticated, user_data = check_auth(request)
|
|
3306
|
+
is_authenticated, user_data = g_app.check_auth(request)
|
|
2775
3307
|
if not is_authenticated:
|
|
2776
|
-
return web.json_response(
|
|
2777
|
-
{
|
|
2778
|
-
"error": {
|
|
2779
|
-
"message": "Authentication required",
|
|
2780
|
-
"type": "authentication_error",
|
|
2781
|
-
"code": "unauthorized",
|
|
2782
|
-
}
|
|
2783
|
-
},
|
|
2784
|
-
status=401,
|
|
2785
|
-
)
|
|
3308
|
+
return web.json_response(g_app.error_auth_required, status=401)
|
|
2786
3309
|
|
|
2787
3310
|
reader = await request.multipart()
|
|
2788
3311
|
|
|
@@ -2792,7 +3315,7 @@ def main():
|
|
|
2792
3315
|
field = await reader.next()
|
|
2793
3316
|
|
|
2794
3317
|
if not field:
|
|
2795
|
-
return web.json_response(
|
|
3318
|
+
return web.json_response(create_error_response("No file provided"), status=400)
|
|
2796
3319
|
|
|
2797
3320
|
filename = field.filename or "file"
|
|
2798
3321
|
content = await field.read()
|
|
@@ -2830,9 +3353,10 @@ def main():
|
|
|
2830
3353
|
with open(full_path, "wb") as f:
|
|
2831
3354
|
f.write(content)
|
|
2832
3355
|
|
|
3356
|
+
url = f"/~cache/{relative_path}"
|
|
2833
3357
|
response_data = {
|
|
2834
3358
|
"date": int(time.time()),
|
|
2835
|
-
"url":
|
|
3359
|
+
"url": url,
|
|
2836
3360
|
"size": len(content),
|
|
2837
3361
|
"type": mimetype,
|
|
2838
3362
|
"name": filename,
|
|
@@ -2852,6 +3376,8 @@ def main():
|
|
|
2852
3376
|
with open(info_path, "w") as f:
|
|
2853
3377
|
json.dump(response_data, f)
|
|
2854
3378
|
|
|
3379
|
+
g_app.on_cache_saved_filters({"url": url, "info": response_data})
|
|
3380
|
+
|
|
2855
3381
|
return web.json_response(response_data)
|
|
2856
3382
|
|
|
2857
3383
|
app.router.add_post("/upload", upload_handler)
|
|
@@ -2877,7 +3403,7 @@ def main():
|
|
|
2877
3403
|
|
|
2878
3404
|
# Check for directory traversal for info path
|
|
2879
3405
|
try:
|
|
2880
|
-
cache_root = Path(get_cache_path(
|
|
3406
|
+
cache_root = Path(get_cache_path())
|
|
2881
3407
|
requested_path = Path(info_path).resolve()
|
|
2882
3408
|
if not str(requested_path).startswith(str(cache_root)):
|
|
2883
3409
|
return web.Response(text="403: Forbidden", status=403)
|
|
@@ -2893,7 +3419,7 @@ def main():
|
|
|
2893
3419
|
|
|
2894
3420
|
# Check for directory traversal
|
|
2895
3421
|
try:
|
|
2896
|
-
cache_root = Path(get_cache_path(
|
|
3422
|
+
cache_root = Path(get_cache_path())
|
|
2897
3423
|
requested_path = Path(full_path).resolve()
|
|
2898
3424
|
if not str(requested_path).startswith(str(cache_root)):
|
|
2899
3425
|
return web.Response(text="403: Forbidden", status=403)
|
|
@@ -2912,7 +3438,7 @@ def main():
|
|
|
2912
3438
|
async def github_auth_handler(request):
|
|
2913
3439
|
"""Initiate GitHub OAuth flow"""
|
|
2914
3440
|
if "auth" not in g_config or "github" not in g_config["auth"]:
|
|
2915
|
-
return web.json_response(
|
|
3441
|
+
return web.json_response(create_error_response("GitHub OAuth not configured"), status=500)
|
|
2916
3442
|
|
|
2917
3443
|
auth_config = g_config["auth"]["github"]
|
|
2918
3444
|
client_id = auth_config.get("client_id", "")
|
|
@@ -2924,11 +3450,11 @@ def main():
|
|
|
2924
3450
|
if redirect_uri.startswith("$"):
|
|
2925
3451
|
redirect_uri = redirect_uri[1:]
|
|
2926
3452
|
|
|
2927
|
-
client_id = os.
|
|
2928
|
-
redirect_uri = os.
|
|
3453
|
+
client_id = os.getenv(client_id, client_id)
|
|
3454
|
+
redirect_uri = os.getenv(redirect_uri, redirect_uri)
|
|
2929
3455
|
|
|
2930
3456
|
if not client_id:
|
|
2931
|
-
return web.json_response(
|
|
3457
|
+
return web.json_response(create_error_response("GitHub client_id not configured"), status=500)
|
|
2932
3458
|
|
|
2933
3459
|
# Generate CSRF state token
|
|
2934
3460
|
state = secrets.token_urlsafe(32)
|
|
@@ -2960,7 +3486,7 @@ def main():
|
|
|
2960
3486
|
if restrict_to.startswith("$"):
|
|
2961
3487
|
restrict_to = restrict_to[1:]
|
|
2962
3488
|
|
|
2963
|
-
restrict_to = os.
|
|
3489
|
+
restrict_to = os.getenv(restrict_to, None if restrict_to == "GITHUB_USERS" else restrict_to)
|
|
2964
3490
|
|
|
2965
3491
|
# If restrict_to is configured, validate the user
|
|
2966
3492
|
if restrict_to:
|
|
@@ -2999,7 +3525,7 @@ def main():
|
|
|
2999
3525
|
g_oauth_states.pop(state)
|
|
3000
3526
|
|
|
3001
3527
|
if "auth" not in g_config or "github" not in g_config["auth"]:
|
|
3002
|
-
return web.json_response(
|
|
3528
|
+
return web.json_response(create_error_response("GitHub OAuth not configured"), status=500)
|
|
3003
3529
|
|
|
3004
3530
|
auth_config = g_config["auth"]["github"]
|
|
3005
3531
|
client_id = auth_config.get("client_id", "")
|
|
@@ -3014,12 +3540,12 @@ def main():
|
|
|
3014
3540
|
if redirect_uri.startswith("$"):
|
|
3015
3541
|
redirect_uri = redirect_uri[1:]
|
|
3016
3542
|
|
|
3017
|
-
client_id = os.
|
|
3018
|
-
client_secret = os.
|
|
3019
|
-
redirect_uri = os.
|
|
3543
|
+
client_id = os.getenv(client_id, client_id)
|
|
3544
|
+
client_secret = os.getenv(client_secret, client_secret)
|
|
3545
|
+
redirect_uri = os.getenv(redirect_uri, redirect_uri)
|
|
3020
3546
|
|
|
3021
3547
|
if not client_id or not client_secret:
|
|
3022
|
-
return web.json_response(
|
|
3548
|
+
return web.json_response(create_error_response("GitHub OAuth credentials not configured"), status=500)
|
|
3023
3549
|
|
|
3024
3550
|
# Exchange code for access token
|
|
3025
3551
|
async with aiohttp.ClientSession() as session:
|
|
@@ -3038,7 +3564,7 @@ def main():
|
|
|
3038
3564
|
|
|
3039
3565
|
if not access_token:
|
|
3040
3566
|
error = token_response.get("error_description", "Failed to get access token")
|
|
3041
|
-
return web.
|
|
3567
|
+
return web.json_response(create_error_response(f"OAuth error: {error}"), status=400)
|
|
3042
3568
|
|
|
3043
3569
|
# Fetch user info
|
|
3044
3570
|
user_url = "https://api.github.com/user"
|
|
@@ -3073,7 +3599,7 @@ def main():
|
|
|
3073
3599
|
session_token = get_session_token(request)
|
|
3074
3600
|
|
|
3075
3601
|
if not session_token or session_token not in g_sessions:
|
|
3076
|
-
return web.json_response(
|
|
3602
|
+
return web.json_response(create_error_response("Invalid or expired session"), status=401)
|
|
3077
3603
|
|
|
3078
3604
|
session_data = g_sessions[session_token]
|
|
3079
3605
|
|
|
@@ -3129,9 +3655,7 @@ def main():
|
|
|
3129
3655
|
# })
|
|
3130
3656
|
|
|
3131
3657
|
# Not authenticated - return error in expected format
|
|
3132
|
-
return web.json_response(
|
|
3133
|
-
{"responseStatus": {"errorCode": "Unauthorized", "message": "Not authenticated"}}, status=401
|
|
3134
|
-
)
|
|
3658
|
+
return web.json_response(g_app.error_auth_required, status=401)
|
|
3135
3659
|
|
|
3136
3660
|
app.router.add_get("/auth", auth_handler)
|
|
3137
3661
|
app.router.add_get("/auth/github", github_auth_handler)
|
|
@@ -3191,15 +3715,81 @@ def main():
|
|
|
3191
3715
|
|
|
3192
3716
|
# go through and register all g_app extensions
|
|
3193
3717
|
for handler in g_app.server_add_get:
|
|
3194
|
-
|
|
3718
|
+
handler_fn = handler[1]
|
|
3719
|
+
|
|
3720
|
+
async def managed_handler(request, handler_fn=handler_fn):
|
|
3721
|
+
try:
|
|
3722
|
+
return await handler_fn(request)
|
|
3723
|
+
except Exception as e:
|
|
3724
|
+
return web.json_response(to_error_response(e, stacktrace=g_verbose), status=500)
|
|
3725
|
+
|
|
3726
|
+
app.router.add_get(handler[0], managed_handler, **handler[2])
|
|
3195
3727
|
for handler in g_app.server_add_post:
|
|
3196
|
-
|
|
3728
|
+
handler_fn = handler[1]
|
|
3729
|
+
|
|
3730
|
+
async def managed_handler(request, handler_fn=handler_fn):
|
|
3731
|
+
try:
|
|
3732
|
+
return await handler_fn(request)
|
|
3733
|
+
except Exception as e:
|
|
3734
|
+
return web.json_response(to_error_response(e, stacktrace=g_verbose), status=500)
|
|
3735
|
+
|
|
3736
|
+
app.router.add_post(handler[0], managed_handler, **handler[2])
|
|
3737
|
+
for handler in g_app.server_add_put:
|
|
3738
|
+
handler_fn = handler[1]
|
|
3739
|
+
|
|
3740
|
+
async def managed_handler(request, handler_fn=handler_fn):
|
|
3741
|
+
try:
|
|
3742
|
+
return await handler_fn(request)
|
|
3743
|
+
except Exception as e:
|
|
3744
|
+
return web.json_response(to_error_response(e, stacktrace=g_verbose), status=500)
|
|
3745
|
+
|
|
3746
|
+
app.router.add_put(handler[0], managed_handler, **handler[2])
|
|
3747
|
+
for handler in g_app.server_add_delete:
|
|
3748
|
+
handler_fn = handler[1]
|
|
3749
|
+
|
|
3750
|
+
async def managed_handler(request, handler_fn=handler_fn):
|
|
3751
|
+
try:
|
|
3752
|
+
return await handler_fn(request)
|
|
3753
|
+
except Exception as e:
|
|
3754
|
+
return web.json_response(to_error_response(e, stacktrace=g_verbose), status=500)
|
|
3755
|
+
|
|
3756
|
+
app.router.add_delete(handler[0], managed_handler, **handler[2])
|
|
3757
|
+
for handler in g_app.server_add_patch:
|
|
3758
|
+
handler_fn = handler[1]
|
|
3759
|
+
|
|
3760
|
+
async def managed_handler(request, handler_fn=handler_fn):
|
|
3761
|
+
try:
|
|
3762
|
+
return await handler_fn(request)
|
|
3763
|
+
except Exception as e:
|
|
3764
|
+
return web.json_response(to_error_response(e, stacktrace=g_verbose), status=500)
|
|
3765
|
+
|
|
3766
|
+
app.router.add_patch(handler[0], managed_handler, **handler[2])
|
|
3197
3767
|
|
|
3198
3768
|
# Serve index.html from root
|
|
3199
3769
|
async def index_handler(request):
|
|
3200
3770
|
index_content = read_resource_file_bytes("index.html")
|
|
3201
|
-
|
|
3202
|
-
|
|
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
|
+
|
|
3203
3793
|
return web.Response(body=index_content, content_type="text/html")
|
|
3204
3794
|
|
|
3205
3795
|
app.router.add_get("/", index_handler)
|
|
@@ -3219,7 +3809,7 @@ def main():
|
|
|
3219
3809
|
|
|
3220
3810
|
print(f"Starting server on port {port}...")
|
|
3221
3811
|
web.run_app(app, host="0.0.0.0", port=port, print=_log)
|
|
3222
|
-
exit(0)
|
|
3812
|
+
g_app.exit(0)
|
|
3223
3813
|
|
|
3224
3814
|
if cli_args.enable is not None:
|
|
3225
3815
|
if cli_args.enable.endswith(","):
|
|
@@ -3236,7 +3826,7 @@ def main():
|
|
|
3236
3826
|
|
|
3237
3827
|
for provider in enable_providers:
|
|
3238
3828
|
if provider not in g_config["providers"]:
|
|
3239
|
-
print(f"Provider {provider} not found")
|
|
3829
|
+
print(f"Provider '{provider}' not found")
|
|
3240
3830
|
print(f"Available providers: {', '.join(g_config['providers'].keys())}")
|
|
3241
3831
|
exit(1)
|
|
3242
3832
|
if provider in g_config["providers"]:
|
|
@@ -3249,7 +3839,7 @@ def main():
|
|
|
3249
3839
|
print_status()
|
|
3250
3840
|
if len(msgs) > 0:
|
|
3251
3841
|
print("\n" + "\n".join(msgs))
|
|
3252
|
-
exit(0)
|
|
3842
|
+
g_app.exit(0)
|
|
3253
3843
|
|
|
3254
3844
|
if cli_args.disable is not None:
|
|
3255
3845
|
if cli_args.disable.endswith(","):
|
|
@@ -3272,7 +3862,7 @@ def main():
|
|
|
3272
3862
|
print(f"\nDisabled provider {provider}")
|
|
3273
3863
|
|
|
3274
3864
|
print_status()
|
|
3275
|
-
exit(0)
|
|
3865
|
+
g_app.exit(0)
|
|
3276
3866
|
|
|
3277
3867
|
if cli_args.default is not None:
|
|
3278
3868
|
default_model = cli_args.default
|
|
@@ -3284,7 +3874,7 @@ def main():
|
|
|
3284
3874
|
default_text["model"] = default_model
|
|
3285
3875
|
save_config(g_config)
|
|
3286
3876
|
print(f"\nDefault model set to: {default_model}")
|
|
3287
|
-
exit(0)
|
|
3877
|
+
g_app.exit(0)
|
|
3288
3878
|
|
|
3289
3879
|
if (
|
|
3290
3880
|
cli_args.chat is not None
|
|
@@ -3324,6 +3914,9 @@ def main():
|
|
|
3324
3914
|
|
|
3325
3915
|
if len(extra_args) > 0:
|
|
3326
3916
|
prompt = " ".join(extra_args)
|
|
3917
|
+
if not chat["messages"] or len(chat["messages"]) == 0:
|
|
3918
|
+
chat["messages"] = [{"role": "user", "content": [{"type": "text", "text": ""}]}]
|
|
3919
|
+
|
|
3327
3920
|
# replace content of last message if exists, else add
|
|
3328
3921
|
last_msg = chat["messages"][-1] if "messages" in chat else None
|
|
3329
3922
|
if last_msg and last_msg["role"] == "user":
|
|
@@ -3341,21 +3934,28 @@ def main():
|
|
|
3341
3934
|
|
|
3342
3935
|
asyncio.run(
|
|
3343
3936
|
cli_chat(
|
|
3344
|
-
chat,
|
|
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,
|
|
3345
3944
|
)
|
|
3346
3945
|
)
|
|
3347
|
-
exit(0)
|
|
3946
|
+
g_app.exit(0)
|
|
3348
3947
|
except Exception as e:
|
|
3349
3948
|
print(f"{cli_args.logprefix}Error: {e}")
|
|
3350
3949
|
if cli_args.verbose:
|
|
3351
3950
|
traceback.print_exc()
|
|
3352
|
-
exit(1)
|
|
3951
|
+
g_app.exit(1)
|
|
3353
3952
|
|
|
3354
3953
|
handled = run_extension_cli()
|
|
3355
3954
|
|
|
3356
3955
|
if not handled:
|
|
3357
3956
|
# show usage from ArgumentParser
|
|
3358
3957
|
parser.print_help()
|
|
3958
|
+
g_app.exit(0)
|
|
3359
3959
|
|
|
3360
3960
|
|
|
3361
3961
|
if __name__ == "__main__":
|