llms-py 3.0.0__py3-none-any.whl → 3.0.0b2__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/index.html +37 -26
- llms/llms.json +21 -70
- llms/main.py +731 -1426
- llms/providers.json +1 -1
- llms/{extensions/analytics/ui/index.mjs → ui/Analytics.mjs} +238 -154
- llms/ui/App.mjs +63 -133
- llms/ui/Avatar.mjs +86 -0
- llms/ui/Brand.mjs +52 -0
- llms/ui/ChatPrompt.mjs +597 -0
- llms/ui/Main.mjs +862 -0
- llms/ui/OAuthSignIn.mjs +61 -0
- llms/ui/ProviderIcon.mjs +36 -0
- llms/ui/ProviderStatus.mjs +104 -0
- llms/{extensions/app/ui → ui}/Recents.mjs +57 -82
- llms/ui/{modules/chat/SettingsDialog.mjs → SettingsDialog.mjs} +9 -9
- llms/{extensions/app/ui/index.mjs → ui/Sidebar.mjs} +57 -122
- llms/ui/SignIn.mjs +65 -0
- llms/ui/Welcome.mjs +8 -0
- llms/ui/ai.mjs +13 -117
- llms/ui/app.css +49 -1776
- llms/ui/index.mjs +171 -87
- llms/ui/lib/charts.mjs +13 -9
- llms/ui/lib/servicestack-vue.mjs +3 -3
- llms/ui/lib/vue.min.mjs +9 -10
- llms/ui/lib/vue.mjs +1602 -1763
- llms/ui/markdown.mjs +2 -10
- llms/ui/model-selector.mjs +686 -0
- llms/ui/tailwind.input.css +1 -55
- llms/ui/threadStore.mjs +583 -0
- llms/ui/utils.mjs +118 -113
- llms/ui.json +1069 -0
- {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/METADATA +1 -1
- llms_py-3.0.0b2.dist-info/RECORD +58 -0
- llms/extensions/app/README.md +0 -20
- llms/extensions/app/__init__.py +0 -530
- 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 +0 -644
- llms/extensions/app/db_manager.py +0 -195
- llms/extensions/app/requests.json +0 -9073
- llms/extensions/app/threads.json +0 -15290
- llms/extensions/app/ui/threadStore.mjs +0 -411
- llms/extensions/core_tools/CALCULATOR.md +0 -32
- llms/extensions/core_tools/__init__.py +0 -598
- llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +0 -201
- llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +0 -185
- llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +0 -101
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +0 -160
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +0 -66
- llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +0 -27
- llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +0 -72
- llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +0 -119
- llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +0 -98
- llms/extensions/core_tools/ui/codemirror/doc/docs.css +0 -225
- llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
- llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +0 -344
- llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +0 -9884
- llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +0 -942
- llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +0 -118
- llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +0 -962
- llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +0 -62
- llms/extensions/core_tools/ui/codemirror/mode/python/python.js +0 -402
- llms/extensions/core_tools/ui/codemirror/theme/dracula.css +0 -40
- llms/extensions/core_tools/ui/codemirror/theme/mocha.css +0 -135
- llms/extensions/core_tools/ui/index.mjs +0 -650
- llms/extensions/gallery/README.md +0 -61
- llms/extensions/gallery/__init__.py +0 -61
- 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 +0 -298
- llms/extensions/gallery/ui/index.mjs +0 -482
- llms/extensions/katex/README.md +0 -39
- llms/extensions/katex/__init__.py +0 -6
- llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/katex/ui/README.md +0 -125
- llms/extensions/katex/ui/contrib/auto-render.js +0 -338
- llms/extensions/katex/ui/contrib/auto-render.min.js +0 -1
- llms/extensions/katex/ui/contrib/auto-render.mjs +0 -244
- llms/extensions/katex/ui/contrib/copy-tex.js +0 -127
- llms/extensions/katex/ui/contrib/copy-tex.min.js +0 -1
- llms/extensions/katex/ui/contrib/copy-tex.mjs +0 -105
- llms/extensions/katex/ui/contrib/mathtex-script-type.js +0 -109
- llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +0 -1
- llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +0 -24
- llms/extensions/katex/ui/contrib/mhchem.js +0 -3213
- llms/extensions/katex/ui/contrib/mhchem.min.js +0 -1
- llms/extensions/katex/ui/contrib/mhchem.mjs +0 -3109
- llms/extensions/katex/ui/contrib/render-a11y-string.js +0 -887
- llms/extensions/katex/ui/contrib/render-a11y-string.min.js +0 -1
- llms/extensions/katex/ui/contrib/render-a11y-string.mjs +0 -800
- 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 +0 -92
- llms/extensions/katex/ui/katex-swap.css +0 -1230
- llms/extensions/katex/ui/katex-swap.min.css +0 -1
- llms/extensions/katex/ui/katex.css +0 -1230
- llms/extensions/katex/ui/katex.js +0 -19080
- llms/extensions/katex/ui/katex.min.css +0 -1
- llms/extensions/katex/ui/katex.min.js +0 -1
- llms/extensions/katex/ui/katex.min.mjs +0 -1
- llms/extensions/katex/ui/katex.mjs +0 -18547
- llms/extensions/providers/__init__.py +0 -18
- 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/extensions/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
- llms/extensions/providers/__pycache__/openai.cpython-314.pyc +0 -0
- llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
- llms/extensions/providers/anthropic.py +0 -229
- llms/extensions/providers/chutes.py +0 -155
- llms/extensions/providers/google.py +0 -378
- llms/extensions/providers/nvidia.py +0 -105
- llms/extensions/providers/openai.py +0 -156
- llms/extensions/providers/openrouter.py +0 -72
- llms/extensions/system_prompts/README.md +0 -22
- llms/extensions/system_prompts/__init__.py +0 -45
- llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/system_prompts/ui/index.mjs +0 -280
- llms/extensions/system_prompts/ui/prompts.json +0 -1067
- llms/extensions/tools/__init__.py +0 -5
- llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/tools/ui/index.mjs +0 -204
- llms/providers-extra.json +0 -356
- llms/ui/ctx.mjs +0 -365
- llms/ui/modules/chat/ChatBody.mjs +0 -691
- llms/ui/modules/chat/index.mjs +0 -828
- llms/ui/modules/layout.mjs +0 -243
- llms/ui/modules/model-selector.mjs +0 -851
- llms_py-3.0.0.dist-info/RECORD +0 -202
- {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/WHEEL +0 -0
- {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/top_level.txt +0 -0
llms/main.py
CHANGED
|
@@ -9,10 +9,8 @@
|
|
|
9
9
|
import argparse
|
|
10
10
|
import asyncio
|
|
11
11
|
import base64
|
|
12
|
-
import contextlib
|
|
13
12
|
import hashlib
|
|
14
13
|
import importlib.util
|
|
15
|
-
import inspect
|
|
16
14
|
import json
|
|
17
15
|
import mimetypes
|
|
18
16
|
import os
|
|
@@ -28,8 +26,7 @@ from datetime import datetime
|
|
|
28
26
|
from importlib import resources # Py≥3.9 (pip install importlib_resources for 3.7/3.8)
|
|
29
27
|
from io import BytesIO
|
|
30
28
|
from pathlib import Path
|
|
31
|
-
from
|
|
32
|
-
from urllib.parse import parse_qs, urlencode, urljoin
|
|
29
|
+
from urllib.parse import parse_qs, urlencode
|
|
33
30
|
|
|
34
31
|
import aiohttp
|
|
35
32
|
from aiohttp import web
|
|
@@ -41,13 +38,11 @@ try:
|
|
|
41
38
|
except ImportError:
|
|
42
39
|
HAS_PIL = False
|
|
43
40
|
|
|
44
|
-
VERSION = "3.0.
|
|
41
|
+
VERSION = "3.0.0b2"
|
|
45
42
|
_ROOT = None
|
|
46
|
-
DEBUG = os.getenv("
|
|
47
|
-
MOCK = os.getenv("MOCK") == "1"
|
|
48
|
-
MOCK_DIR = os.getenv("MOCK_DIR")
|
|
49
|
-
DISABLE_EXTENSIONS = (os.getenv("LLMS_DISABLE") or "").split(",")
|
|
43
|
+
DEBUG = True # os.getenv("PYPI_SERVICESTACK") is not None
|
|
50
44
|
g_config_path = None
|
|
45
|
+
g_ui_path = None
|
|
51
46
|
g_config = None
|
|
52
47
|
g_providers = None
|
|
53
48
|
g_handlers = {}
|
|
@@ -107,6 +102,17 @@ def chat_summary(chat):
|
|
|
107
102
|
return json.dumps(clone, indent=2)
|
|
108
103
|
|
|
109
104
|
|
|
105
|
+
def gemini_chat_summary(gemini_chat):
|
|
106
|
+
"""Summarize Gemini chat completion request for logging. Replace inline_data with size of content only"""
|
|
107
|
+
clone = json.loads(json.dumps(gemini_chat))
|
|
108
|
+
for content in clone["contents"]:
|
|
109
|
+
for part in content["parts"]:
|
|
110
|
+
if "inline_data" in part:
|
|
111
|
+
data = part["inline_data"]["data"]
|
|
112
|
+
part["inline_data"]["data"] = f"({len(data)})"
|
|
113
|
+
return json.dumps(clone, indent=2)
|
|
114
|
+
|
|
115
|
+
|
|
110
116
|
image_exts = ["png", "webp", "jpg", "jpeg", "gif", "bmp", "svg", "tiff", "ico"]
|
|
111
117
|
audio_exts = ["mp3", "wav", "ogg", "flac", "m4a", "opus", "webm"]
|
|
112
118
|
|
|
@@ -200,16 +206,6 @@ def is_base_64(data):
|
|
|
200
206
|
return False
|
|
201
207
|
|
|
202
208
|
|
|
203
|
-
def id_to_name(id):
|
|
204
|
-
return id.replace("-", " ").title()
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def pluralize(word, count):
|
|
208
|
-
if count == 1:
|
|
209
|
-
return word
|
|
210
|
-
return word + "s"
|
|
211
|
-
|
|
212
|
-
|
|
213
209
|
def get_file_mime_type(filename):
|
|
214
210
|
mime_type, _ = mimetypes.guess_type(filename)
|
|
215
211
|
return mime_type or "application/octet-stream"
|
|
@@ -331,52 +327,11 @@ def convert_image_if_needed(image_bytes, mimetype="image/png"):
|
|
|
331
327
|
return image_bytes, mimetype
|
|
332
328
|
|
|
333
329
|
|
|
334
|
-
def to_content(result):
|
|
335
|
-
if isinstance(result, (str, int, float, bool)):
|
|
336
|
-
return str(result)
|
|
337
|
-
elif isinstance(result, (list, set, tuple, dict)):
|
|
338
|
-
return json.dumps(result)
|
|
339
|
-
else:
|
|
340
|
-
return str(result)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
def function_to_tool_definition(func):
|
|
344
|
-
type_hints = get_type_hints(func)
|
|
345
|
-
signature = inspect.signature(func)
|
|
346
|
-
parameters = {"type": "object", "properties": {}, "required": []}
|
|
347
|
-
|
|
348
|
-
for name, param in signature.parameters.items():
|
|
349
|
-
param_type = type_hints.get(name, str)
|
|
350
|
-
param_type_name = "string"
|
|
351
|
-
if param_type is int:
|
|
352
|
-
param_type_name = "integer"
|
|
353
|
-
elif param_type is float:
|
|
354
|
-
param_type_name = "number"
|
|
355
|
-
elif param_type is bool:
|
|
356
|
-
param_type_name = "boolean"
|
|
357
|
-
|
|
358
|
-
parameters["properties"][name] = {"type": param_type_name}
|
|
359
|
-
if param.default == inspect.Parameter.empty:
|
|
360
|
-
parameters["required"].append(name)
|
|
361
|
-
|
|
362
|
-
return {
|
|
363
|
-
"type": "function",
|
|
364
|
-
"function": {
|
|
365
|
-
"name": func.__name__,
|
|
366
|
-
"description": func.__doc__ or "",
|
|
367
|
-
"parameters": parameters,
|
|
368
|
-
},
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
|
|
372
330
|
async def process_chat(chat, provider_id=None):
|
|
373
331
|
if not chat:
|
|
374
332
|
raise Exception("No chat provided")
|
|
375
333
|
if "stream" not in chat:
|
|
376
334
|
chat["stream"] = False
|
|
377
|
-
# Some providers don't support empty tools
|
|
378
|
-
if "tools" in chat and len(chat["tools"]) == 0:
|
|
379
|
-
del chat["tools"]
|
|
380
335
|
if "messages" not in chat:
|
|
381
336
|
return chat
|
|
382
337
|
|
|
@@ -503,92 +458,6 @@ async def process_chat(chat, provider_id=None):
|
|
|
503
458
|
return chat
|
|
504
459
|
|
|
505
460
|
|
|
506
|
-
def image_ext_from_mimetype(mimetype, default="png"):
|
|
507
|
-
if "/" in mimetype:
|
|
508
|
-
_ext = mimetypes.guess_extension(mimetype)
|
|
509
|
-
if _ext:
|
|
510
|
-
return _ext.lstrip(".")
|
|
511
|
-
return default
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
def audio_ext_from_format(format, default="mp3"):
|
|
515
|
-
if format == "mpeg":
|
|
516
|
-
return "mp3"
|
|
517
|
-
return format or default
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
def file_ext_from_mimetype(mimetype, default="pdf"):
|
|
521
|
-
if "/" in mimetype:
|
|
522
|
-
_ext = mimetypes.guess_extension(mimetype)
|
|
523
|
-
if _ext:
|
|
524
|
-
return _ext.lstrip(".")
|
|
525
|
-
return default
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
def cache_message_inline_data(m):
|
|
529
|
-
"""
|
|
530
|
-
Replaces and caches any inline data URIs in the message content.
|
|
531
|
-
"""
|
|
532
|
-
if "content" not in m:
|
|
533
|
-
return
|
|
534
|
-
|
|
535
|
-
content = m["content"]
|
|
536
|
-
if isinstance(content, list):
|
|
537
|
-
for item in content:
|
|
538
|
-
if item.get("type") == "image_url":
|
|
539
|
-
image_url = item.get("image_url", {})
|
|
540
|
-
url = image_url.get("url")
|
|
541
|
-
if url and url.startswith("data:"):
|
|
542
|
-
# Extract base64 and mimetype
|
|
543
|
-
try:
|
|
544
|
-
header, base64_data = url.split(";base64,")
|
|
545
|
-
# header is like "data:image/png"
|
|
546
|
-
ext = image_ext_from_mimetype(header.split(":")[1])
|
|
547
|
-
filename = f"image.{ext}" # Hash will handle uniqueness
|
|
548
|
-
|
|
549
|
-
cache_url, _ = save_image_to_cache(base64_data, filename, {}, ignore_info=True)
|
|
550
|
-
image_url["url"] = cache_url
|
|
551
|
-
except Exception as e:
|
|
552
|
-
_log(f"Error caching inline image: {e}")
|
|
553
|
-
|
|
554
|
-
elif item.get("type") == "input_audio":
|
|
555
|
-
input_audio = item.get("input_audio", {})
|
|
556
|
-
data = input_audio.get("data")
|
|
557
|
-
if data:
|
|
558
|
-
# Handle data URI or raw base64
|
|
559
|
-
base64_data = data
|
|
560
|
-
if data.startswith("data:"):
|
|
561
|
-
with contextlib.suppress(ValueError):
|
|
562
|
-
header, base64_data = data.split(";base64,")
|
|
563
|
-
|
|
564
|
-
fmt = audio_ext_from_format(input_audio.get("format"))
|
|
565
|
-
filename = f"audio.{fmt}"
|
|
566
|
-
|
|
567
|
-
try:
|
|
568
|
-
cache_url, _ = save_bytes_to_cache(base64_data, filename, {}, ignore_info=True)
|
|
569
|
-
input_audio["data"] = cache_url
|
|
570
|
-
except Exception as e:
|
|
571
|
-
_log(f"Error caching inline audio: {e}")
|
|
572
|
-
|
|
573
|
-
elif item.get("type") == "file":
|
|
574
|
-
file_info = item.get("file", {})
|
|
575
|
-
file_data = file_info.get("file_data")
|
|
576
|
-
if file_data and file_data.startswith("data:"):
|
|
577
|
-
try:
|
|
578
|
-
header, base64_data = file_data.split(";base64,")
|
|
579
|
-
mimetype = header.split(":")[1]
|
|
580
|
-
# Try to get extension from filename if available, else mimetype
|
|
581
|
-
filename = file_info.get("filename", "file")
|
|
582
|
-
if "." not in filename:
|
|
583
|
-
ext = file_ext_from_mimetype(mimetype)
|
|
584
|
-
filename = f"{filename}.{ext}"
|
|
585
|
-
|
|
586
|
-
cache_url, _ = save_bytes_to_cache(base64_data, filename, {}, ignore_info=True)
|
|
587
|
-
file_info["file_data"] = cache_url
|
|
588
|
-
except Exception as e:
|
|
589
|
-
_log(f"Error caching inline file: {e}")
|
|
590
|
-
|
|
591
|
-
|
|
592
461
|
class HTTPError(Exception):
|
|
593
462
|
def __init__(self, status, reason, body, headers=None):
|
|
594
463
|
self.status = status
|
|
@@ -598,302 +467,15 @@ class HTTPError(Exception):
|
|
|
598
467
|
super().__init__(f"HTTP {status} {reason}")
|
|
599
468
|
|
|
600
469
|
|
|
601
|
-
def save_bytes_to_cache(base64_data, filename, file_info, ignore_info=False):
|
|
602
|
-
ext = filename.split(".")[-1]
|
|
603
|
-
mimetype = get_file_mime_type(filename)
|
|
604
|
-
content = base64.b64decode(base64_data) if isinstance(base64_data, str) else base64_data
|
|
605
|
-
sha256_hash = hashlib.sha256(content).hexdigest()
|
|
606
|
-
|
|
607
|
-
save_filename = f"{sha256_hash}.{ext}" if ext else sha256_hash
|
|
608
|
-
|
|
609
|
-
# Use first 2 chars for subdir to avoid too many files in one dir
|
|
610
|
-
subdir = sha256_hash[:2]
|
|
611
|
-
relative_path = f"{subdir}/{save_filename}"
|
|
612
|
-
full_path = get_cache_path(relative_path)
|
|
613
|
-
url = f"/~cache/{relative_path}"
|
|
614
|
-
|
|
615
|
-
# if file and its .info.json already exists, return it
|
|
616
|
-
info_path = os.path.splitext(full_path)[0] + ".info.json"
|
|
617
|
-
if os.path.exists(full_path) and os.path.exists(info_path):
|
|
618
|
-
_dbg(f"Cached bytes exists: {relative_path}")
|
|
619
|
-
if ignore_info:
|
|
620
|
-
return url, None
|
|
621
|
-
return url, json.load(open(info_path))
|
|
622
|
-
|
|
623
|
-
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
624
|
-
|
|
625
|
-
with open(full_path, "wb") as f:
|
|
626
|
-
f.write(content)
|
|
627
|
-
info = {
|
|
628
|
-
"date": int(time.time()),
|
|
629
|
-
"url": url,
|
|
630
|
-
"size": len(content),
|
|
631
|
-
"type": mimetype,
|
|
632
|
-
"name": filename,
|
|
633
|
-
}
|
|
634
|
-
info.update(file_info)
|
|
635
|
-
|
|
636
|
-
# Save metadata
|
|
637
|
-
info_path = os.path.splitext(full_path)[0] + ".info.json"
|
|
638
|
-
with open(info_path, "w") as f:
|
|
639
|
-
json.dump(info, f)
|
|
640
|
-
|
|
641
|
-
_dbg(f"Saved cached bytes and info: {relative_path}")
|
|
642
|
-
|
|
643
|
-
g_app.on_cache_saved_filters({"url": url, "info": info})
|
|
644
|
-
|
|
645
|
-
return url, info
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
def save_image_to_cache(base64_data, filename, image_info, ignore_info=False):
|
|
649
|
-
ext = filename.split(".")[-1]
|
|
650
|
-
mimetype = get_file_mime_type(filename)
|
|
651
|
-
content = base64.b64decode(base64_data) if isinstance(base64_data, str) else base64_data
|
|
652
|
-
sha256_hash = hashlib.sha256(content).hexdigest()
|
|
653
|
-
|
|
654
|
-
save_filename = f"{sha256_hash}.{ext}" if ext else sha256_hash
|
|
655
|
-
|
|
656
|
-
# Use first 2 chars for subdir to avoid too many files in one dir
|
|
657
|
-
subdir = sha256_hash[:2]
|
|
658
|
-
relative_path = f"{subdir}/{save_filename}"
|
|
659
|
-
full_path = get_cache_path(relative_path)
|
|
660
|
-
url = f"/~cache/{relative_path}"
|
|
661
|
-
|
|
662
|
-
# if file and its .info.json already exists, return it
|
|
663
|
-
info_path = os.path.splitext(full_path)[0] + ".info.json"
|
|
664
|
-
if os.path.exists(full_path) and os.path.exists(info_path):
|
|
665
|
-
_dbg(f"Saved image exists: {relative_path}")
|
|
666
|
-
if ignore_info:
|
|
667
|
-
return url, None
|
|
668
|
-
return url, json.load(open(info_path))
|
|
669
|
-
|
|
670
|
-
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
671
|
-
|
|
672
|
-
with open(full_path, "wb") as f:
|
|
673
|
-
f.write(content)
|
|
674
|
-
info = {
|
|
675
|
-
"date": int(time.time()),
|
|
676
|
-
"url": url,
|
|
677
|
-
"size": len(content),
|
|
678
|
-
"type": mimetype,
|
|
679
|
-
"name": filename,
|
|
680
|
-
}
|
|
681
|
-
info.update(image_info)
|
|
682
|
-
|
|
683
|
-
# If image, get dimensions
|
|
684
|
-
if HAS_PIL and mimetype.startswith("image/"):
|
|
685
|
-
try:
|
|
686
|
-
with Image.open(BytesIO(content)) as img:
|
|
687
|
-
info["width"] = img.width
|
|
688
|
-
info["height"] = img.height
|
|
689
|
-
except Exception:
|
|
690
|
-
pass
|
|
691
|
-
|
|
692
|
-
if "width" in info and "height" in info:
|
|
693
|
-
_log(f"Saved image to cache: {full_path} ({len(content)} bytes) {info['width']}x{info['height']}")
|
|
694
|
-
else:
|
|
695
|
-
_log(f"Saved image to cache: {full_path} ({len(content)} bytes)")
|
|
696
|
-
|
|
697
|
-
# Save metadata
|
|
698
|
-
info_path = os.path.splitext(full_path)[0] + ".info.json"
|
|
699
|
-
with open(info_path, "w") as f:
|
|
700
|
-
json.dump(info, f)
|
|
701
|
-
|
|
702
|
-
_dbg(f"Saved image and info: {relative_path}")
|
|
703
|
-
|
|
704
|
-
g_app.on_cache_saved_filters({"url": url, "info": info})
|
|
705
|
-
|
|
706
|
-
return url, info
|
|
707
|
-
|
|
708
|
-
|
|
709
470
|
async def response_json(response):
|
|
710
471
|
text = await response.text()
|
|
711
472
|
if response.status >= 400:
|
|
712
|
-
_dbg(f"HTTP {response.status} {response.reason}: {text}")
|
|
713
473
|
raise HTTPError(response.status, reason=response.reason, body=text, headers=dict(response.headers))
|
|
714
474
|
response.raise_for_status()
|
|
715
475
|
body = json.loads(text)
|
|
716
476
|
return body
|
|
717
477
|
|
|
718
478
|
|
|
719
|
-
def chat_to_prompt(chat):
|
|
720
|
-
prompt = ""
|
|
721
|
-
if "messages" in chat:
|
|
722
|
-
for message in chat["messages"]:
|
|
723
|
-
if message["role"] == "user":
|
|
724
|
-
# if content is string
|
|
725
|
-
if isinstance(message["content"], str):
|
|
726
|
-
if prompt:
|
|
727
|
-
prompt += "\n"
|
|
728
|
-
prompt += message["content"]
|
|
729
|
-
elif isinstance(message["content"], list):
|
|
730
|
-
# if content is array of objects
|
|
731
|
-
for part in message["content"]:
|
|
732
|
-
if part["type"] == "text":
|
|
733
|
-
if prompt:
|
|
734
|
-
prompt += "\n"
|
|
735
|
-
prompt += part["text"]
|
|
736
|
-
return prompt
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
def chat_to_system_prompt(chat):
|
|
740
|
-
if "messages" in chat:
|
|
741
|
-
for message in chat["messages"]:
|
|
742
|
-
if message["role"] == "system":
|
|
743
|
-
# if content is string
|
|
744
|
-
if isinstance(message["content"], str):
|
|
745
|
-
return message["content"]
|
|
746
|
-
elif isinstance(message["content"], list):
|
|
747
|
-
# if content is array of objects
|
|
748
|
-
for part in message["content"]:
|
|
749
|
-
if part["type"] == "text":
|
|
750
|
-
return part["text"]
|
|
751
|
-
return None
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
def chat_to_username(chat):
|
|
755
|
-
if "metadata" in chat and "user" in chat["metadata"]:
|
|
756
|
-
return chat["metadata"]["user"]
|
|
757
|
-
return None
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
def last_user_prompt(chat):
|
|
761
|
-
prompt = ""
|
|
762
|
-
if "messages" in chat:
|
|
763
|
-
for message in chat["messages"]:
|
|
764
|
-
if message["role"] == "user":
|
|
765
|
-
# if content is string
|
|
766
|
-
if isinstance(message["content"], str):
|
|
767
|
-
prompt = message["content"]
|
|
768
|
-
elif isinstance(message["content"], list):
|
|
769
|
-
# if content is array of objects
|
|
770
|
-
for part in message["content"]:
|
|
771
|
-
if part["type"] == "text":
|
|
772
|
-
prompt = part["text"]
|
|
773
|
-
return prompt
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
def chat_response_to_message(openai_response):
|
|
777
|
-
"""
|
|
778
|
-
Returns an assistant message from the OpenAI Response.
|
|
779
|
-
Handles normalizing text, image, and audio responses into the message content.
|
|
780
|
-
"""
|
|
781
|
-
timestamp = int(time.time() * 1000) # openai_response.get("created")
|
|
782
|
-
choices = openai_response
|
|
783
|
-
if isinstance(openai_response, dict) and "choices" in openai_response:
|
|
784
|
-
choices = openai_response["choices"]
|
|
785
|
-
|
|
786
|
-
choice = choices[0] if isinstance(choices, list) and choices else choices
|
|
787
|
-
|
|
788
|
-
if isinstance(choice, str):
|
|
789
|
-
return {"role": "assistant", "content": choice, "timestamp": timestamp}
|
|
790
|
-
|
|
791
|
-
if isinstance(choice, dict):
|
|
792
|
-
message = choice.get("message", choice)
|
|
793
|
-
else:
|
|
794
|
-
return {"role": "assistant", "content": str(choice), "timestamp": timestamp}
|
|
795
|
-
|
|
796
|
-
# Ensure message is a dict
|
|
797
|
-
if not isinstance(message, dict):
|
|
798
|
-
return {"role": "assistant", "content": message, "timestamp": timestamp}
|
|
799
|
-
|
|
800
|
-
message.update({"timestamp": timestamp})
|
|
801
|
-
return message
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
def to_file_info(chat, info=None, response=None):
|
|
805
|
-
prompt = last_user_prompt(chat)
|
|
806
|
-
ret = info or {}
|
|
807
|
-
if chat["model"] and "model" not in ret:
|
|
808
|
-
ret["model"] = chat["model"]
|
|
809
|
-
if prompt and "prompt" not in ret:
|
|
810
|
-
ret["prompt"] = prompt
|
|
811
|
-
if "image_config" in chat:
|
|
812
|
-
ret.update(chat["image_config"])
|
|
813
|
-
user = chat_to_username(chat)
|
|
814
|
-
if user:
|
|
815
|
-
ret["user"] = user
|
|
816
|
-
return ret
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
# Image Generator Providers
|
|
820
|
-
class GeneratorBase:
|
|
821
|
-
def __init__(self, **kwargs):
|
|
822
|
-
self.id = kwargs.get("id")
|
|
823
|
-
self.api = kwargs.get("api")
|
|
824
|
-
self.api_key = kwargs.get("api_key")
|
|
825
|
-
self.headers = {
|
|
826
|
-
"Accept": "application/json",
|
|
827
|
-
"Content-Type": "application/json",
|
|
828
|
-
}
|
|
829
|
-
self.chat_url = f"{self.api}/chat/completions"
|
|
830
|
-
self.default_content = "I've generated the image for you."
|
|
831
|
-
|
|
832
|
-
def validate(self, **kwargs):
|
|
833
|
-
if not self.api_key:
|
|
834
|
-
api_keys = ", ".join(self.env)
|
|
835
|
-
return f"Provider '{self.name}' requires API Key {api_keys}"
|
|
836
|
-
return None
|
|
837
|
-
|
|
838
|
-
def test(self, **kwargs):
|
|
839
|
-
error_msg = self.validate(**kwargs)
|
|
840
|
-
if error_msg:
|
|
841
|
-
_log(error_msg)
|
|
842
|
-
return False
|
|
843
|
-
return True
|
|
844
|
-
|
|
845
|
-
async def load(self):
|
|
846
|
-
pass
|
|
847
|
-
|
|
848
|
-
def gen_summary(self, gen):
|
|
849
|
-
"""Summarize gen response for logging."""
|
|
850
|
-
clone = json.loads(json.dumps(gen))
|
|
851
|
-
return json.dumps(clone, indent=2)
|
|
852
|
-
|
|
853
|
-
def chat_summary(self, chat):
|
|
854
|
-
return chat_summary(chat)
|
|
855
|
-
|
|
856
|
-
def process_chat(self, chat, provider_id=None):
|
|
857
|
-
return process_chat(chat, provider_id)
|
|
858
|
-
|
|
859
|
-
async def response_json(self, response):
|
|
860
|
-
return await response_json(response)
|
|
861
|
-
|
|
862
|
-
def get_headers(self, provider, chat):
|
|
863
|
-
headers = self.headers.copy()
|
|
864
|
-
if provider is not None:
|
|
865
|
-
headers["Authorization"] = f"Bearer {provider.api_key}"
|
|
866
|
-
elif self.api_key:
|
|
867
|
-
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
868
|
-
return headers
|
|
869
|
-
|
|
870
|
-
def to_response(self, response, chat, started_at):
|
|
871
|
-
raise NotImplementedError
|
|
872
|
-
|
|
873
|
-
async def chat(self, chat, provider=None):
|
|
874
|
-
return {
|
|
875
|
-
"choices": [
|
|
876
|
-
{
|
|
877
|
-
"message": {
|
|
878
|
-
"role": "assistant",
|
|
879
|
-
"content": "Not Implemented",
|
|
880
|
-
"images": [
|
|
881
|
-
{
|
|
882
|
-
"type": "image_url",
|
|
883
|
-
"image_url": {
|
|
884
|
-
"url": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik0xMiAyMGE4IDggMCAxIDAgMC0xNmE4IDggMCAwIDAgMCAxNm0wIDJDNi40NzcgMjIgMiAxNy41MjMgMiAxMlM2LjQ3NyAyIDEyIDJzMTAgNC40NzcgMTAgMTBzLTQuNDc3IDEwLTEwIDEwbS0xLTZoMnYyaC0yem0wLTEwaDJ2OGgtMnoiLz48L3N2Zz4=",
|
|
885
|
-
},
|
|
886
|
-
}
|
|
887
|
-
],
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
]
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
# OpenAI Providers
|
|
895
|
-
|
|
896
|
-
|
|
897
479
|
class OpenAiCompatible:
|
|
898
480
|
sdk = "@ai-sdk/openai-compatible"
|
|
899
481
|
|
|
@@ -905,9 +487,8 @@ class OpenAiCompatible:
|
|
|
905
487
|
|
|
906
488
|
self.id = kwargs.get("id")
|
|
907
489
|
self.api = kwargs.get("api").strip("/")
|
|
908
|
-
self.env = kwargs.get("env", [])
|
|
909
490
|
self.api_key = kwargs.get("api_key")
|
|
910
|
-
self.name = kwargs.get("name",
|
|
491
|
+
self.name = kwargs.get("name", self.id.replace("-", " ").title().replace(" ", ""))
|
|
911
492
|
self.set_models(**kwargs)
|
|
912
493
|
|
|
913
494
|
self.chat_url = f"{self.api}/chat/completions"
|
|
@@ -935,7 +516,6 @@ class OpenAiCompatible:
|
|
|
935
516
|
self.stream = bool(kwargs["stream"]) if "stream" in kwargs else None
|
|
936
517
|
self.enable_thinking = bool(kwargs["enable_thinking"]) if "enable_thinking" in kwargs else None
|
|
937
518
|
self.check = kwargs.get("check")
|
|
938
|
-
self.modalities = kwargs.get("modalities", {})
|
|
939
519
|
|
|
940
520
|
def set_models(self, **kwargs):
|
|
941
521
|
models = kwargs.get("models", {})
|
|
@@ -961,34 +541,23 @@ class OpenAiCompatible:
|
|
|
961
541
|
_log(f"Filtering {len(self.models)} models, excluding models that match regex: {exclude_models}")
|
|
962
542
|
self.models = {k: v for k, v in self.models.items() if not re.search(exclude_models, k)}
|
|
963
543
|
|
|
964
|
-
def validate(self, **kwargs):
|
|
965
|
-
if not self.api_key:
|
|
966
|
-
api_keys = ", ".join(self.env)
|
|
967
|
-
return f"Provider '{self.name}' requires API Key {api_keys}"
|
|
968
|
-
return None
|
|
969
|
-
|
|
970
544
|
def test(self, **kwargs):
|
|
971
|
-
|
|
972
|
-
if
|
|
973
|
-
_log(
|
|
974
|
-
|
|
975
|
-
return True
|
|
545
|
+
ret = self.api and self.api_key and (len(self.models) > 0)
|
|
546
|
+
if not ret:
|
|
547
|
+
_log(f"Provider {self.name} Missing: {self.api}, {self.api_key}, {len(self.models)}")
|
|
548
|
+
return ret
|
|
976
549
|
|
|
977
550
|
async def load(self):
|
|
978
551
|
if not self.models:
|
|
979
552
|
await self.load_models()
|
|
980
553
|
|
|
981
|
-
def
|
|
554
|
+
def model_cost(self, model):
|
|
982
555
|
provider_model = self.provider_model(model) or model
|
|
983
556
|
for model_id, model_info in self.models.items():
|
|
984
557
|
if model_id.lower() == provider_model.lower():
|
|
985
|
-
return model_info
|
|
558
|
+
return model_info.get("cost")
|
|
986
559
|
return None
|
|
987
560
|
|
|
988
|
-
def model_cost(self, model):
|
|
989
|
-
model_info = self.model_info(model)
|
|
990
|
-
return model_info.get("cost") if model_info else None
|
|
991
|
-
|
|
992
561
|
def provider_model(self, model):
|
|
993
562
|
# convert model to lowercase for case-insensitive comparison
|
|
994
563
|
model_lower = model.lower()
|
|
@@ -1024,12 +593,8 @@ class OpenAiCompatible:
|
|
|
1024
593
|
if "/" in model:
|
|
1025
594
|
last_part = model.split("/")[-1]
|
|
1026
595
|
return self.provider_model(last_part)
|
|
1027
|
-
|
|
1028
596
|
return None
|
|
1029
597
|
|
|
1030
|
-
def response_json(self, response):
|
|
1031
|
-
return response_json(response)
|
|
1032
|
-
|
|
1033
598
|
def to_response(self, response, chat, started_at):
|
|
1034
599
|
if "metadata" not in response:
|
|
1035
600
|
response["metadata"] = {}
|
|
@@ -1038,28 +603,12 @@ class OpenAiCompatible:
|
|
|
1038
603
|
pricing = self.model_cost(chat["model"])
|
|
1039
604
|
if pricing and "input" in pricing and "output" in pricing:
|
|
1040
605
|
response["metadata"]["pricing"] = f"{pricing['input']}/{pricing['output']}"
|
|
606
|
+
_log(json.dumps(response, indent=2))
|
|
1041
607
|
return response
|
|
1042
608
|
|
|
1043
|
-
def chat_summary(self, chat):
|
|
1044
|
-
return chat_summary(chat)
|
|
1045
|
-
|
|
1046
|
-
def process_chat(self, chat, provider_id=None):
|
|
1047
|
-
return process_chat(chat, provider_id)
|
|
1048
|
-
|
|
1049
609
|
async def chat(self, chat):
|
|
1050
610
|
chat["model"] = self.provider_model(chat["model"]) or chat["model"]
|
|
1051
611
|
|
|
1052
|
-
if "modalities" in chat:
|
|
1053
|
-
for modality in chat.get("modalities", []):
|
|
1054
|
-
# use default implementation for text modalities
|
|
1055
|
-
if modality == "text":
|
|
1056
|
-
continue
|
|
1057
|
-
modality_provider = self.modalities.get(modality)
|
|
1058
|
-
if modality_provider:
|
|
1059
|
-
return await modality_provider.chat(chat, self)
|
|
1060
|
-
else:
|
|
1061
|
-
raise Exception(f"Provider {self.name} does not support '{modality}' modality")
|
|
1062
|
-
|
|
1063
612
|
# with open(os.path.join(os.path.dirname(__file__), 'chat.wip.json'), "w") as f:
|
|
1064
613
|
# f.write(json.dumps(chat, indent=2))
|
|
1065
614
|
|
|
@@ -1102,17 +651,203 @@ class OpenAiCompatible:
|
|
|
1102
651
|
_log(f"POST {self.chat_url}")
|
|
1103
652
|
_log(chat_summary(chat))
|
|
1104
653
|
# remove metadata if any (conflicts with some providers, e.g. Z.ai)
|
|
1105
|
-
|
|
654
|
+
chat.pop("metadata", None)
|
|
1106
655
|
|
|
1107
656
|
async with aiohttp.ClientSession() as session:
|
|
1108
657
|
started_at = time.time()
|
|
1109
658
|
async with session.post(
|
|
1110
659
|
self.chat_url, headers=self.headers, data=json.dumps(chat), timeout=aiohttp.ClientTimeout(total=120)
|
|
1111
660
|
) as response:
|
|
1112
|
-
chat["metadata"] = metadata
|
|
1113
661
|
return self.to_response(await response_json(response), chat, started_at)
|
|
1114
662
|
|
|
1115
663
|
|
|
664
|
+
class OpenAiProvider(OpenAiCompatible):
|
|
665
|
+
sdk = "@ai-sdk/openai"
|
|
666
|
+
|
|
667
|
+
def __init__(self, **kwargs):
|
|
668
|
+
if "api" not in kwargs:
|
|
669
|
+
kwargs["api"] = "https://api.openai.com/v1"
|
|
670
|
+
super().__init__(**kwargs)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
class AnthropicProvider(OpenAiCompatible):
|
|
674
|
+
sdk = "@ai-sdk/anthropic"
|
|
675
|
+
|
|
676
|
+
def __init__(self, **kwargs):
|
|
677
|
+
if "api" not in kwargs:
|
|
678
|
+
kwargs["api"] = "https://api.anthropic.com/v1"
|
|
679
|
+
super().__init__(**kwargs)
|
|
680
|
+
|
|
681
|
+
# Anthropic uses x-api-key header instead of Authorization
|
|
682
|
+
if self.api_key:
|
|
683
|
+
self.headers = self.headers.copy()
|
|
684
|
+
if "Authorization" in self.headers:
|
|
685
|
+
del self.headers["Authorization"]
|
|
686
|
+
self.headers["x-api-key"] = self.api_key
|
|
687
|
+
|
|
688
|
+
if "anthropic-version" not in self.headers:
|
|
689
|
+
self.headers = self.headers.copy()
|
|
690
|
+
self.headers["anthropic-version"] = "2023-06-01"
|
|
691
|
+
self.chat_url = f"{self.api}/messages"
|
|
692
|
+
|
|
693
|
+
async def chat(self, chat):
|
|
694
|
+
chat["model"] = self.provider_model(chat["model"]) or chat["model"]
|
|
695
|
+
|
|
696
|
+
chat = await process_chat(chat, provider_id=self.id)
|
|
697
|
+
|
|
698
|
+
# Transform OpenAI format to Anthropic format
|
|
699
|
+
anthropic_request = {
|
|
700
|
+
"model": chat["model"],
|
|
701
|
+
"messages": [],
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
# Extract system message (Anthropic uses top-level 'system' parameter)
|
|
705
|
+
system_messages = []
|
|
706
|
+
for message in chat.get("messages", []):
|
|
707
|
+
if message.get("role") == "system":
|
|
708
|
+
content = message.get("content", "")
|
|
709
|
+
if isinstance(content, str):
|
|
710
|
+
system_messages.append(content)
|
|
711
|
+
elif isinstance(content, list):
|
|
712
|
+
for item in content:
|
|
713
|
+
if item.get("type") == "text":
|
|
714
|
+
system_messages.append(item.get("text", ""))
|
|
715
|
+
|
|
716
|
+
if system_messages:
|
|
717
|
+
anthropic_request["system"] = "\n".join(system_messages)
|
|
718
|
+
|
|
719
|
+
# Transform messages (exclude system messages)
|
|
720
|
+
for message in chat.get("messages", []):
|
|
721
|
+
if message.get("role") == "system":
|
|
722
|
+
continue
|
|
723
|
+
|
|
724
|
+
anthropic_message = {"role": message.get("role"), "content": []}
|
|
725
|
+
|
|
726
|
+
content = message.get("content", "")
|
|
727
|
+
if isinstance(content, str):
|
|
728
|
+
anthropic_message["content"] = content
|
|
729
|
+
elif isinstance(content, list):
|
|
730
|
+
for item in content:
|
|
731
|
+
if item.get("type") == "text":
|
|
732
|
+
anthropic_message["content"].append({"type": "text", "text": item.get("text", "")})
|
|
733
|
+
elif item.get("type") == "image_url" and "image_url" in item:
|
|
734
|
+
# Transform OpenAI image_url format to Anthropic format
|
|
735
|
+
image_url = item["image_url"].get("url", "")
|
|
736
|
+
if image_url.startswith("data:"):
|
|
737
|
+
# Extract media type and base64 data
|
|
738
|
+
parts = image_url.split(";base64,", 1)
|
|
739
|
+
if len(parts) == 2:
|
|
740
|
+
media_type = parts[0].replace("data:", "")
|
|
741
|
+
base64_data = parts[1]
|
|
742
|
+
anthropic_message["content"].append(
|
|
743
|
+
{
|
|
744
|
+
"type": "image",
|
|
745
|
+
"source": {"type": "base64", "media_type": media_type, "data": base64_data},
|
|
746
|
+
}
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
anthropic_request["messages"].append(anthropic_message)
|
|
750
|
+
|
|
751
|
+
# Handle max_tokens (required by Anthropic, uses max_tokens not max_completion_tokens)
|
|
752
|
+
if "max_completion_tokens" in chat:
|
|
753
|
+
anthropic_request["max_tokens"] = chat["max_completion_tokens"]
|
|
754
|
+
elif "max_tokens" in chat:
|
|
755
|
+
anthropic_request["max_tokens"] = chat["max_tokens"]
|
|
756
|
+
else:
|
|
757
|
+
# Anthropic requires max_tokens, set a default
|
|
758
|
+
anthropic_request["max_tokens"] = 4096
|
|
759
|
+
|
|
760
|
+
# Copy other supported parameters
|
|
761
|
+
if "temperature" in chat:
|
|
762
|
+
anthropic_request["temperature"] = chat["temperature"]
|
|
763
|
+
if "top_p" in chat:
|
|
764
|
+
anthropic_request["top_p"] = chat["top_p"]
|
|
765
|
+
if "top_k" in chat:
|
|
766
|
+
anthropic_request["top_k"] = chat["top_k"]
|
|
767
|
+
if "stop" in chat:
|
|
768
|
+
anthropic_request["stop_sequences"] = chat["stop"] if isinstance(chat["stop"], list) else [chat["stop"]]
|
|
769
|
+
if "stream" in chat:
|
|
770
|
+
anthropic_request["stream"] = chat["stream"]
|
|
771
|
+
if "tools" in chat:
|
|
772
|
+
anthropic_request["tools"] = chat["tools"]
|
|
773
|
+
if "tool_choice" in chat:
|
|
774
|
+
anthropic_request["tool_choice"] = chat["tool_choice"]
|
|
775
|
+
|
|
776
|
+
_log(f"POST {self.chat_url}")
|
|
777
|
+
_log(f"Anthropic Request: {json.dumps(anthropic_request, indent=2)}")
|
|
778
|
+
|
|
779
|
+
async with aiohttp.ClientSession() as session:
|
|
780
|
+
started_at = time.time()
|
|
781
|
+
async with session.post(
|
|
782
|
+
self.chat_url,
|
|
783
|
+
headers=self.headers,
|
|
784
|
+
data=json.dumps(anthropic_request),
|
|
785
|
+
timeout=aiohttp.ClientTimeout(total=120),
|
|
786
|
+
) as response:
|
|
787
|
+
return self.to_response(await response_json(response), chat, started_at)
|
|
788
|
+
|
|
789
|
+
def to_response(self, response, chat, started_at):
|
|
790
|
+
"""Convert Anthropic response format to OpenAI-compatible format."""
|
|
791
|
+
# Transform Anthropic response to OpenAI format
|
|
792
|
+
openai_response = {
|
|
793
|
+
"id": response.get("id", ""),
|
|
794
|
+
"object": "chat.completion",
|
|
795
|
+
"created": int(started_at),
|
|
796
|
+
"model": response.get("model", ""),
|
|
797
|
+
"choices": [],
|
|
798
|
+
"usage": {},
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
# Transform content blocks to message content
|
|
802
|
+
content_parts = []
|
|
803
|
+
thinking_parts = []
|
|
804
|
+
|
|
805
|
+
for block in response.get("content", []):
|
|
806
|
+
if block.get("type") == "text":
|
|
807
|
+
content_parts.append(block.get("text", ""))
|
|
808
|
+
elif block.get("type") == "thinking":
|
|
809
|
+
# Store thinking blocks separately (some models include reasoning)
|
|
810
|
+
thinking_parts.append(block.get("thinking", ""))
|
|
811
|
+
|
|
812
|
+
# Combine all text content
|
|
813
|
+
message_content = "\n".join(content_parts) if content_parts else ""
|
|
814
|
+
|
|
815
|
+
# Create the choice object
|
|
816
|
+
choice = {
|
|
817
|
+
"index": 0,
|
|
818
|
+
"message": {"role": "assistant", "content": message_content},
|
|
819
|
+
"finish_reason": response.get("stop_reason", "stop"),
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
# Add thinking as metadata if present
|
|
823
|
+
if thinking_parts:
|
|
824
|
+
choice["message"]["thinking"] = "\n".join(thinking_parts)
|
|
825
|
+
|
|
826
|
+
openai_response["choices"].append(choice)
|
|
827
|
+
|
|
828
|
+
# Transform usage
|
|
829
|
+
if "usage" in response:
|
|
830
|
+
usage = response["usage"]
|
|
831
|
+
openai_response["usage"] = {
|
|
832
|
+
"prompt_tokens": usage.get("input_tokens", 0),
|
|
833
|
+
"completion_tokens": usage.get("output_tokens", 0),
|
|
834
|
+
"total_tokens": usage.get("input_tokens", 0) + usage.get("output_tokens", 0),
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
# Add metadata
|
|
838
|
+
if "metadata" not in openai_response:
|
|
839
|
+
openai_response["metadata"] = {}
|
|
840
|
+
openai_response["metadata"]["duration"] = int((time.time() - started_at) * 1000)
|
|
841
|
+
|
|
842
|
+
if chat is not None and "model" in chat:
|
|
843
|
+
cost = self.model_cost(chat["model"])
|
|
844
|
+
if cost and "input" in cost and "output" in cost:
|
|
845
|
+
openai_response["metadata"]["pricing"] = f"{cost['input']}/{cost['output']}"
|
|
846
|
+
|
|
847
|
+
_log(json.dumps(openai_response, indent=2))
|
|
848
|
+
return openai_response
|
|
849
|
+
|
|
850
|
+
|
|
1116
851
|
class MistralProvider(OpenAiCompatible):
|
|
1117
852
|
sdk = "@ai-sdk/mistral"
|
|
1118
853
|
|
|
@@ -1208,8 +943,8 @@ class OllamaProvider(OpenAiCompatible):
|
|
|
1208
943
|
}
|
|
1209
944
|
self.models = models
|
|
1210
945
|
|
|
1211
|
-
def
|
|
1212
|
-
return
|
|
946
|
+
def test(self, **kwargs):
|
|
947
|
+
return True
|
|
1213
948
|
|
|
1214
949
|
|
|
1215
950
|
class LMStudioProvider(OllamaProvider):
|
|
@@ -1238,6 +973,223 @@ class LMStudioProvider(OllamaProvider):
|
|
|
1238
973
|
return ret
|
|
1239
974
|
|
|
1240
975
|
|
|
976
|
+
# class GoogleOpenAiProvider(OpenAiCompatible):
|
|
977
|
+
# sdk = "google-openai-compatible"
|
|
978
|
+
|
|
979
|
+
# def __init__(self, api_key, **kwargs):
|
|
980
|
+
# super().__init__(api="https://generativelanguage.googleapis.com", api_key=api_key, **kwargs)
|
|
981
|
+
# self.chat_url = "https://generativelanguage.googleapis.com/v1beta/chat/completions"
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
class GoogleProvider(OpenAiCompatible):
|
|
985
|
+
sdk = "@ai-sdk/google"
|
|
986
|
+
|
|
987
|
+
def __init__(self, **kwargs):
|
|
988
|
+
new_kwargs = {"api": "https://generativelanguage.googleapis.com", **kwargs}
|
|
989
|
+
super().__init__(**new_kwargs)
|
|
990
|
+
self.safety_settings = kwargs.get("safety_settings")
|
|
991
|
+
self.thinking_config = kwargs.get("thinking_config")
|
|
992
|
+
self.curl = kwargs.get("curl")
|
|
993
|
+
self.headers = kwargs.get("headers", {"Content-Type": "application/json"})
|
|
994
|
+
# Google fails when using Authorization header, use query string param instead
|
|
995
|
+
if "Authorization" in self.headers:
|
|
996
|
+
del self.headers["Authorization"]
|
|
997
|
+
|
|
998
|
+
async def chat(self, chat):
|
|
999
|
+
chat["model"] = self.provider_model(chat["model"]) or chat["model"]
|
|
1000
|
+
|
|
1001
|
+
chat = await process_chat(chat)
|
|
1002
|
+
generation_config = {}
|
|
1003
|
+
|
|
1004
|
+
# Filter out system messages and convert to proper Gemini format
|
|
1005
|
+
contents = []
|
|
1006
|
+
system_prompt = None
|
|
1007
|
+
|
|
1008
|
+
async with aiohttp.ClientSession() as session:
|
|
1009
|
+
for message in chat["messages"]:
|
|
1010
|
+
if message["role"] == "system":
|
|
1011
|
+
content = message["content"]
|
|
1012
|
+
if isinstance(content, list):
|
|
1013
|
+
for item in content:
|
|
1014
|
+
if "text" in item:
|
|
1015
|
+
system_prompt = item["text"]
|
|
1016
|
+
break
|
|
1017
|
+
elif isinstance(content, str):
|
|
1018
|
+
system_prompt = content
|
|
1019
|
+
elif "content" in message:
|
|
1020
|
+
if isinstance(message["content"], list):
|
|
1021
|
+
parts = []
|
|
1022
|
+
for item in message["content"]:
|
|
1023
|
+
if "type" in item:
|
|
1024
|
+
if item["type"] == "image_url" and "image_url" in item:
|
|
1025
|
+
image_url = item["image_url"]
|
|
1026
|
+
if "url" not in image_url:
|
|
1027
|
+
continue
|
|
1028
|
+
url = image_url["url"]
|
|
1029
|
+
if not url.startswith("data:"):
|
|
1030
|
+
raise (Exception("Image was not downloaded: " + url))
|
|
1031
|
+
# Extract mime type from data uri
|
|
1032
|
+
mimetype = url.split(";", 1)[0].split(":", 1)[1] if ";" in url else "image/png"
|
|
1033
|
+
base64_data = url.split(",", 1)[1]
|
|
1034
|
+
parts.append({"inline_data": {"mime_type": mimetype, "data": base64_data}})
|
|
1035
|
+
elif item["type"] == "input_audio" and "input_audio" in item:
|
|
1036
|
+
input_audio = item["input_audio"]
|
|
1037
|
+
if "data" not in input_audio:
|
|
1038
|
+
continue
|
|
1039
|
+
data = input_audio["data"]
|
|
1040
|
+
format = input_audio["format"]
|
|
1041
|
+
mimetype = f"audio/{format}"
|
|
1042
|
+
parts.append({"inline_data": {"mime_type": mimetype, "data": data}})
|
|
1043
|
+
elif item["type"] == "file" and "file" in item:
|
|
1044
|
+
file = item["file"]
|
|
1045
|
+
if "file_data" not in file:
|
|
1046
|
+
continue
|
|
1047
|
+
data = file["file_data"]
|
|
1048
|
+
if not data.startswith("data:"):
|
|
1049
|
+
raise (Exception("File was not downloaded: " + data))
|
|
1050
|
+
# Extract mime type from data uri
|
|
1051
|
+
mimetype = (
|
|
1052
|
+
data.split(";", 1)[0].split(":", 1)[1]
|
|
1053
|
+
if ";" in data
|
|
1054
|
+
else "application/octet-stream"
|
|
1055
|
+
)
|
|
1056
|
+
base64_data = data.split(",", 1)[1]
|
|
1057
|
+
parts.append({"inline_data": {"mime_type": mimetype, "data": base64_data}})
|
|
1058
|
+
if "text" in item:
|
|
1059
|
+
text = item["text"]
|
|
1060
|
+
parts.append({"text": text})
|
|
1061
|
+
if len(parts) > 0:
|
|
1062
|
+
contents.append(
|
|
1063
|
+
{
|
|
1064
|
+
"role": message["role"]
|
|
1065
|
+
if "role" in message and message["role"] == "user"
|
|
1066
|
+
else "model",
|
|
1067
|
+
"parts": parts,
|
|
1068
|
+
}
|
|
1069
|
+
)
|
|
1070
|
+
else:
|
|
1071
|
+
content = message["content"]
|
|
1072
|
+
contents.append(
|
|
1073
|
+
{
|
|
1074
|
+
"role": message["role"] if "role" in message and message["role"] == "user" else "model",
|
|
1075
|
+
"parts": [{"text": content}],
|
|
1076
|
+
}
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
gemini_chat = {
|
|
1080
|
+
"contents": contents,
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if self.safety_settings:
|
|
1084
|
+
gemini_chat["safetySettings"] = self.safety_settings
|
|
1085
|
+
|
|
1086
|
+
# Add system instruction if present
|
|
1087
|
+
if system_prompt is not None:
|
|
1088
|
+
gemini_chat["systemInstruction"] = {"parts": [{"text": system_prompt}]}
|
|
1089
|
+
|
|
1090
|
+
if "max_completion_tokens" in chat:
|
|
1091
|
+
generation_config["maxOutputTokens"] = chat["max_completion_tokens"]
|
|
1092
|
+
if "stop" in chat:
|
|
1093
|
+
generation_config["stopSequences"] = [chat["stop"]]
|
|
1094
|
+
if "temperature" in chat:
|
|
1095
|
+
generation_config["temperature"] = chat["temperature"]
|
|
1096
|
+
if "top_p" in chat:
|
|
1097
|
+
generation_config["topP"] = chat["top_p"]
|
|
1098
|
+
if "top_logprobs" in chat:
|
|
1099
|
+
generation_config["topK"] = chat["top_logprobs"]
|
|
1100
|
+
|
|
1101
|
+
if "thinkingConfig" in chat:
|
|
1102
|
+
generation_config["thinkingConfig"] = chat["thinkingConfig"]
|
|
1103
|
+
elif self.thinking_config:
|
|
1104
|
+
generation_config["thinkingConfig"] = self.thinking_config
|
|
1105
|
+
|
|
1106
|
+
if len(generation_config) > 0:
|
|
1107
|
+
gemini_chat["generationConfig"] = generation_config
|
|
1108
|
+
|
|
1109
|
+
started_at = int(time.time() * 1000)
|
|
1110
|
+
gemini_chat_url = f"https://generativelanguage.googleapis.com/v1beta/models/{chat['model']}:generateContent?key={self.api_key}"
|
|
1111
|
+
|
|
1112
|
+
_log(f"POST {gemini_chat_url}")
|
|
1113
|
+
_log(gemini_chat_summary(gemini_chat))
|
|
1114
|
+
started_at = time.time()
|
|
1115
|
+
|
|
1116
|
+
if self.curl:
|
|
1117
|
+
curl_args = [
|
|
1118
|
+
"curl",
|
|
1119
|
+
"-X",
|
|
1120
|
+
"POST",
|
|
1121
|
+
"-H",
|
|
1122
|
+
"Content-Type: application/json",
|
|
1123
|
+
"-d",
|
|
1124
|
+
json.dumps(gemini_chat),
|
|
1125
|
+
gemini_chat_url,
|
|
1126
|
+
]
|
|
1127
|
+
try:
|
|
1128
|
+
o = subprocess.run(curl_args, check=True, capture_output=True, text=True, timeout=120)
|
|
1129
|
+
obj = json.loads(o.stdout)
|
|
1130
|
+
except Exception as e:
|
|
1131
|
+
raise Exception(f"Error executing curl: {e}") from e
|
|
1132
|
+
else:
|
|
1133
|
+
async with session.post(
|
|
1134
|
+
gemini_chat_url,
|
|
1135
|
+
headers=self.headers,
|
|
1136
|
+
data=json.dumps(gemini_chat),
|
|
1137
|
+
timeout=aiohttp.ClientTimeout(total=120),
|
|
1138
|
+
) as res:
|
|
1139
|
+
obj = await response_json(res)
|
|
1140
|
+
_log(f"google response:\n{json.dumps(obj, indent=2)}")
|
|
1141
|
+
|
|
1142
|
+
response = {
|
|
1143
|
+
"id": f"chatcmpl-{started_at}",
|
|
1144
|
+
"created": started_at,
|
|
1145
|
+
"model": obj.get("modelVersion", chat["model"]),
|
|
1146
|
+
}
|
|
1147
|
+
choices = []
|
|
1148
|
+
if "error" in obj:
|
|
1149
|
+
_log(f"Error: {obj['error']}")
|
|
1150
|
+
raise Exception(obj["error"]["message"])
|
|
1151
|
+
for i, candidate in enumerate(obj["candidates"]):
|
|
1152
|
+
role = "assistant"
|
|
1153
|
+
if "content" in candidate and "role" in candidate["content"]:
|
|
1154
|
+
role = "assistant" if candidate["content"]["role"] == "model" else candidate["content"]["role"]
|
|
1155
|
+
|
|
1156
|
+
# Safely extract content from all text parts
|
|
1157
|
+
content = ""
|
|
1158
|
+
reasoning = ""
|
|
1159
|
+
if "content" in candidate and "parts" in candidate["content"]:
|
|
1160
|
+
text_parts = []
|
|
1161
|
+
reasoning_parts = []
|
|
1162
|
+
for part in candidate["content"]["parts"]:
|
|
1163
|
+
if "text" in part:
|
|
1164
|
+
if "thought" in part and part["thought"]:
|
|
1165
|
+
reasoning_parts.append(part["text"])
|
|
1166
|
+
else:
|
|
1167
|
+
text_parts.append(part["text"])
|
|
1168
|
+
content = " ".join(text_parts)
|
|
1169
|
+
reasoning = " ".join(reasoning_parts)
|
|
1170
|
+
|
|
1171
|
+
choice = {
|
|
1172
|
+
"index": i,
|
|
1173
|
+
"finish_reason": candidate.get("finishReason", "stop"),
|
|
1174
|
+
"message": {
|
|
1175
|
+
"role": role,
|
|
1176
|
+
"content": content,
|
|
1177
|
+
},
|
|
1178
|
+
}
|
|
1179
|
+
if reasoning:
|
|
1180
|
+
choice["message"]["reasoning"] = reasoning
|
|
1181
|
+
choices.append(choice)
|
|
1182
|
+
response["choices"] = choices
|
|
1183
|
+
if "usageMetadata" in obj:
|
|
1184
|
+
usage = obj["usageMetadata"]
|
|
1185
|
+
response["usage"] = {
|
|
1186
|
+
"completion_tokens": usage["candidatesTokenCount"],
|
|
1187
|
+
"total_tokens": usage["totalTokenCount"],
|
|
1188
|
+
"prompt_tokens": usage["promptTokenCount"],
|
|
1189
|
+
}
|
|
1190
|
+
return self.to_response(response, chat, started_at)
|
|
1191
|
+
|
|
1192
|
+
|
|
1241
1193
|
def get_provider_model(model_name):
|
|
1242
1194
|
for provider in g_handlers.values():
|
|
1243
1195
|
provider_model = provider.provider_model(model_name)
|
|
@@ -1281,237 +1233,31 @@ def api_providers():
|
|
|
1281
1233
|
return ret
|
|
1282
1234
|
|
|
1283
1235
|
|
|
1284
|
-
def
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
if stacktrace:
|
|
1291
|
-
status["stackTrace"] = traceback.format_exc()
|
|
1292
|
-
return {"responseStatus": status}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
def create_error_response(message, error_code="Error", stack_trace=None):
|
|
1296
|
-
ret = {"responseStatus": {"errorCode": error_code, "message": message}}
|
|
1297
|
-
if stack_trace:
|
|
1298
|
-
ret["responseStatus"]["stackTrace"] = stack_trace
|
|
1299
|
-
return ret
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
def should_cancel_thread(context):
|
|
1303
|
-
ret = context.get("cancelled", False)
|
|
1304
|
-
if ret:
|
|
1305
|
-
thread_id = context.get("threadId")
|
|
1306
|
-
_dbg(f"Thread cancelled {thread_id}")
|
|
1307
|
-
return ret
|
|
1308
|
-
|
|
1236
|
+
async def chat_completion(chat):
|
|
1237
|
+
model = chat["model"]
|
|
1238
|
+
# get first provider that has the model
|
|
1239
|
+
candidate_providers = [name for name, provider in g_handlers.items() if provider.provider_model(model)]
|
|
1240
|
+
if len(candidate_providers) == 0:
|
|
1241
|
+
raise (Exception(f"Model {model} not found"))
|
|
1309
1242
|
|
|
1310
|
-
def g_chat_request(template=None, text=None, model=None, system_prompt=None):
|
|
1311
|
-
chat_template = g_config["defaults"].get(template or "text")
|
|
1312
|
-
if not chat_template:
|
|
1313
|
-
raise Exception(f"Chat template '{template}' not found")
|
|
1314
|
-
|
|
1315
|
-
chat = chat_template.copy()
|
|
1316
|
-
if model:
|
|
1317
|
-
chat["model"] = model
|
|
1318
|
-
if system_prompt is not None:
|
|
1319
|
-
chat["messages"].insert(0, {"role": "system", "content": system_prompt})
|
|
1320
|
-
if text is not None:
|
|
1321
|
-
if not chat["messages"] or len(chat["messages"]) == 0:
|
|
1322
|
-
chat["messages"] = [{"role": "user", "content": [{"type": "text", "text": ""}]}]
|
|
1323
|
-
|
|
1324
|
-
# replace content of last message if exists, else add
|
|
1325
|
-
last_msg = chat["messages"][-1] if "messages" in chat else None
|
|
1326
|
-
if last_msg and last_msg["role"] == "user":
|
|
1327
|
-
if isinstance(last_msg["content"], list):
|
|
1328
|
-
last_msg["content"][-1]["text"] = text
|
|
1329
|
-
else:
|
|
1330
|
-
last_msg["content"] = text
|
|
1331
|
-
else:
|
|
1332
|
-
chat["messages"].append({"role": "user", "content": text})
|
|
1333
|
-
|
|
1334
|
-
return chat
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
async def g_chat_completion(chat, context=None):
|
|
1338
|
-
try:
|
|
1339
|
-
model = chat.get("model")
|
|
1340
|
-
if not model:
|
|
1341
|
-
raise Exception("Model not specified")
|
|
1342
|
-
|
|
1343
|
-
if context is None:
|
|
1344
|
-
context = {"chat": chat, "tools": "all"}
|
|
1345
|
-
|
|
1346
|
-
# get first provider that has the model
|
|
1347
|
-
candidate_providers = [name for name, provider in g_handlers.items() if provider.provider_model(model)]
|
|
1348
|
-
if len(candidate_providers) == 0:
|
|
1349
|
-
raise (Exception(f"Model {model} not found"))
|
|
1350
|
-
except Exception as e:
|
|
1351
|
-
await g_app.on_chat_error(e, context or {"chat": chat})
|
|
1352
|
-
raise e
|
|
1353
|
-
|
|
1354
|
-
started_at = time.time()
|
|
1355
1243
|
first_exception = None
|
|
1356
|
-
provider_name = "Unknown"
|
|
1357
1244
|
for name in candidate_providers:
|
|
1245
|
+
provider = g_handlers[name]
|
|
1246
|
+
_log(f"provider: {name} {type(provider).__name__}")
|
|
1358
1247
|
try:
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
_log(f"provider: {name} {type(provider).__name__}")
|
|
1362
|
-
started_at = time.time()
|
|
1363
|
-
context["startedAt"] = datetime.now()
|
|
1364
|
-
context["provider"] = name
|
|
1365
|
-
model_info = provider.model_info(model)
|
|
1366
|
-
context["modelCost"] = model_info.get("cost", provider.model_cost(model)) or {"input": 0, "output": 0}
|
|
1367
|
-
context["modelInfo"] = model_info
|
|
1368
|
-
|
|
1369
|
-
# Accumulate usage across tool calls
|
|
1370
|
-
total_usage = {
|
|
1371
|
-
"prompt_tokens": 0,
|
|
1372
|
-
"completion_tokens": 0,
|
|
1373
|
-
"total_tokens": 0,
|
|
1374
|
-
}
|
|
1375
|
-
accumulated_cost = 0.0
|
|
1376
|
-
|
|
1377
|
-
# Inject global tools if present
|
|
1378
|
-
current_chat = chat.copy()
|
|
1379
|
-
if g_app.tool_definitions:
|
|
1380
|
-
only_tools_str = context.get("tools", "all")
|
|
1381
|
-
include_all_tools = only_tools_str == "all"
|
|
1382
|
-
only_tools = only_tools_str.split(",")
|
|
1383
|
-
|
|
1384
|
-
if include_all_tools or len(only_tools) > 0:
|
|
1385
|
-
if "tools" not in current_chat:
|
|
1386
|
-
current_chat["tools"] = []
|
|
1387
|
-
|
|
1388
|
-
existing_tools = {t["function"]["name"] for t in current_chat["tools"]}
|
|
1389
|
-
for tool_def in g_app.tool_definitions:
|
|
1390
|
-
name = tool_def["function"]["name"]
|
|
1391
|
-
if name not in existing_tools and (include_all_tools or name in only_tools):
|
|
1392
|
-
current_chat["tools"].append(tool_def)
|
|
1393
|
-
|
|
1394
|
-
# Apply pre-chat filters ONCE
|
|
1395
|
-
context["chat"] = current_chat
|
|
1396
|
-
for filter_func in g_app.chat_request_filters:
|
|
1397
|
-
await filter_func(current_chat, context)
|
|
1398
|
-
|
|
1399
|
-
# Tool execution loop
|
|
1400
|
-
max_iterations = 10
|
|
1401
|
-
tool_history = []
|
|
1402
|
-
final_response = None
|
|
1403
|
-
|
|
1404
|
-
for _ in range(max_iterations):
|
|
1405
|
-
if should_cancel_thread(context):
|
|
1406
|
-
return
|
|
1407
|
-
|
|
1408
|
-
response = await provider.chat(current_chat)
|
|
1409
|
-
|
|
1410
|
-
if should_cancel_thread(context):
|
|
1411
|
-
return
|
|
1412
|
-
|
|
1413
|
-
# Aggregate usage
|
|
1414
|
-
if "usage" in response:
|
|
1415
|
-
usage = response["usage"]
|
|
1416
|
-
total_usage["prompt_tokens"] += usage.get("prompt_tokens", 0)
|
|
1417
|
-
total_usage["completion_tokens"] += usage.get("completion_tokens", 0)
|
|
1418
|
-
total_usage["total_tokens"] += usage.get("total_tokens", 0)
|
|
1419
|
-
|
|
1420
|
-
# Calculate cost for this step if available
|
|
1421
|
-
if "cost" in response and isinstance(response["cost"], (int, float)):
|
|
1422
|
-
accumulated_cost += response["cost"]
|
|
1423
|
-
elif "cost" in usage and isinstance(usage["cost"], (int, float)):
|
|
1424
|
-
accumulated_cost += usage["cost"]
|
|
1425
|
-
|
|
1426
|
-
# Check for tool_calls in the response
|
|
1427
|
-
choice = response.get("choices", [])[0] if response.get("choices") else {}
|
|
1428
|
-
message = choice.get("message", {})
|
|
1429
|
-
tool_calls = message.get("tool_calls")
|
|
1430
|
-
|
|
1431
|
-
if tool_calls:
|
|
1432
|
-
# Append the assistant's message with tool calls to history
|
|
1433
|
-
if "messages" not in current_chat:
|
|
1434
|
-
current_chat["messages"] = []
|
|
1435
|
-
if "timestamp" not in message:
|
|
1436
|
-
message["timestamp"] = int(time.time() * 1000)
|
|
1437
|
-
current_chat["messages"].append(message)
|
|
1438
|
-
tool_history.append(message)
|
|
1439
|
-
|
|
1440
|
-
await g_app.on_chat_tool(current_chat, context)
|
|
1441
|
-
|
|
1442
|
-
for tool_call in tool_calls:
|
|
1443
|
-
function_name = tool_call["function"]["name"]
|
|
1444
|
-
try:
|
|
1445
|
-
function_args = json.loads(tool_call["function"]["arguments"])
|
|
1446
|
-
except Exception as e:
|
|
1447
|
-
tool_result = f"Error parsing JSON arguments for tool {function_name}: {e}"
|
|
1448
|
-
else:
|
|
1449
|
-
tool_result = f"Error: Tool {function_name} not found"
|
|
1450
|
-
if function_name in g_app.tools:
|
|
1451
|
-
try:
|
|
1452
|
-
func = g_app.tools[function_name]
|
|
1453
|
-
if inspect.iscoroutinefunction(func):
|
|
1454
|
-
tool_result = await func(**function_args)
|
|
1455
|
-
else:
|
|
1456
|
-
tool_result = func(**function_args)
|
|
1457
|
-
except Exception as e:
|
|
1458
|
-
tool_result = f"Error executing tool {function_name}: {e}"
|
|
1459
|
-
|
|
1460
|
-
# Append tool result to history
|
|
1461
|
-
tool_msg = {"role": "tool", "tool_call_id": tool_call["id"], "content": to_content(tool_result)}
|
|
1462
|
-
current_chat["messages"].append(tool_msg)
|
|
1463
|
-
tool_history.append(tool_msg)
|
|
1464
|
-
|
|
1465
|
-
await g_app.on_chat_tool(current_chat, context)
|
|
1466
|
-
|
|
1467
|
-
if should_cancel_thread(context):
|
|
1468
|
-
return
|
|
1469
|
-
|
|
1470
|
-
# Continue loop to send tool results back to LLM
|
|
1471
|
-
continue
|
|
1472
|
-
|
|
1473
|
-
# If no tool calls, this is the final response
|
|
1474
|
-
if tool_history:
|
|
1475
|
-
response["tool_history"] = tool_history
|
|
1476
|
-
|
|
1477
|
-
# Update final response with aggregated usage
|
|
1478
|
-
if "usage" not in response:
|
|
1479
|
-
response["usage"] = {}
|
|
1480
|
-
# convert to int seconds
|
|
1481
|
-
context["duration"] = duration = int(time.time() - started_at)
|
|
1482
|
-
total_usage.update({"duration": duration})
|
|
1483
|
-
response["usage"].update(total_usage)
|
|
1484
|
-
# If we accumulated cost, set it on the response
|
|
1485
|
-
if accumulated_cost > 0:
|
|
1486
|
-
response["cost"] = accumulated_cost
|
|
1487
|
-
|
|
1488
|
-
final_response = response
|
|
1489
|
-
break # Exit tool loop
|
|
1490
|
-
|
|
1491
|
-
if final_response:
|
|
1492
|
-
# Apply post-chat filters ONCE on final response
|
|
1493
|
-
for filter_func in g_app.chat_response_filters:
|
|
1494
|
-
await filter_func(final_response, context)
|
|
1495
|
-
|
|
1496
|
-
if DEBUG:
|
|
1497
|
-
_dbg(json.dumps(final_response, indent=2))
|
|
1498
|
-
|
|
1499
|
-
return final_response
|
|
1500
|
-
|
|
1248
|
+
response = await provider.chat(chat.copy())
|
|
1249
|
+
return response
|
|
1501
1250
|
except Exception as e:
|
|
1502
1251
|
if first_exception is None:
|
|
1503
1252
|
first_exception = e
|
|
1504
|
-
|
|
1505
|
-
_err(f"Provider {provider_name} failed", first_exception)
|
|
1506
|
-
await g_app.on_chat_error(e, context)
|
|
1507
|
-
|
|
1253
|
+
_log(f"Provider {name} failed: {e}")
|
|
1508
1254
|
continue
|
|
1509
1255
|
|
|
1510
1256
|
# If we get here, all providers failed
|
|
1511
1257
|
raise first_exception
|
|
1512
1258
|
|
|
1513
1259
|
|
|
1514
|
-
async def cli_chat(chat,
|
|
1260
|
+
async def cli_chat(chat, image=None, audio=None, file=None, args=None, raw=False):
|
|
1515
1261
|
if g_default_model:
|
|
1516
1262
|
chat["model"] = g_default_model
|
|
1517
1263
|
|
|
@@ -1586,53 +1332,25 @@ async def cli_chat(chat, tools=None, image=None, audio=None, file=None, args=Non
|
|
|
1586
1332
|
printdump(chat)
|
|
1587
1333
|
|
|
1588
1334
|
try:
|
|
1589
|
-
|
|
1590
|
-
"tools": tools or "all",
|
|
1591
|
-
}
|
|
1592
|
-
response = await g_app.chat_completion(chat, context=context)
|
|
1593
|
-
|
|
1335
|
+
response = await chat_completion(chat)
|
|
1594
1336
|
if raw:
|
|
1595
1337
|
print(json.dumps(response, indent=2))
|
|
1596
1338
|
exit(0)
|
|
1597
1339
|
else:
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
print(msg["content"])
|
|
1601
|
-
|
|
1602
|
-
generated_files = []
|
|
1603
|
-
for choice in response["choices"]:
|
|
1604
|
-
if "message" in choice:
|
|
1605
|
-
msg = choice["message"]
|
|
1606
|
-
if "images" in msg:
|
|
1607
|
-
for image in msg["images"]:
|
|
1608
|
-
image_url = image["image_url"]["url"]
|
|
1609
|
-
generated_files.append(image_url)
|
|
1610
|
-
if "audios" in msg:
|
|
1611
|
-
for audio in msg["audios"]:
|
|
1612
|
-
audio_url = audio["audio_url"]["url"]
|
|
1613
|
-
generated_files.append(audio_url)
|
|
1614
|
-
|
|
1615
|
-
if len(generated_files) > 0:
|
|
1616
|
-
print("\nSaved files:")
|
|
1617
|
-
for file in generated_files:
|
|
1618
|
-
if file.startswith("/~cache"):
|
|
1619
|
-
print(get_cache_path(file[8:]))
|
|
1620
|
-
print(urljoin("http://localhost:8000", file))
|
|
1621
|
-
else:
|
|
1622
|
-
print(file)
|
|
1623
|
-
|
|
1340
|
+
answer = response["choices"][0]["message"]["content"]
|
|
1341
|
+
print(answer)
|
|
1624
1342
|
except HTTPError as e:
|
|
1625
1343
|
# HTTP error (4xx, 5xx)
|
|
1626
1344
|
print(f"{e}:\n{e.body}")
|
|
1627
|
-
|
|
1345
|
+
exit(1)
|
|
1628
1346
|
except aiohttp.ClientConnectionError as e:
|
|
1629
1347
|
# Connection issues
|
|
1630
1348
|
print(f"Connection error: {e}")
|
|
1631
|
-
|
|
1349
|
+
exit(1)
|
|
1632
1350
|
except asyncio.TimeoutError as e:
|
|
1633
1351
|
# Timeout
|
|
1634
1352
|
print(f"Timeout error: {e}")
|
|
1635
|
-
|
|
1353
|
+
exit(1)
|
|
1636
1354
|
|
|
1637
1355
|
|
|
1638
1356
|
def config_str(key):
|
|
@@ -1655,33 +1373,29 @@ def init_llms(config, providers):
|
|
|
1655
1373
|
# iterate over config and replace $ENV with env value
|
|
1656
1374
|
for key, value in g_config.items():
|
|
1657
1375
|
if isinstance(value, str) and value.startswith("$"):
|
|
1658
|
-
g_config[key] = os.
|
|
1376
|
+
g_config[key] = os.environ.get(value[1:], "")
|
|
1659
1377
|
|
|
1660
1378
|
# if g_verbose:
|
|
1661
1379
|
# printdump(g_config)
|
|
1662
1380
|
providers = g_config["providers"]
|
|
1663
1381
|
|
|
1664
1382
|
for id, orig in providers.items():
|
|
1665
|
-
|
|
1383
|
+
definition = orig.copy()
|
|
1384
|
+
if "enabled" in definition and not definition["enabled"]:
|
|
1666
1385
|
continue
|
|
1667
1386
|
|
|
1668
|
-
|
|
1387
|
+
provider_id = definition.get("id", id)
|
|
1388
|
+
if "id" not in definition:
|
|
1389
|
+
definition["id"] = provider_id
|
|
1390
|
+
provider = g_providers.get(provider_id)
|
|
1391
|
+
constructor_kwargs = create_provider_kwargs(definition, provider)
|
|
1392
|
+
provider = create_provider(constructor_kwargs)
|
|
1393
|
+
|
|
1669
1394
|
if provider and provider.test(**constructor_kwargs):
|
|
1670
1395
|
g_handlers[id] = provider
|
|
1671
1396
|
return g_handlers
|
|
1672
1397
|
|
|
1673
1398
|
|
|
1674
|
-
def create_provider_from_definition(id, orig):
|
|
1675
|
-
definition = orig.copy()
|
|
1676
|
-
provider_id = definition.get("id", id)
|
|
1677
|
-
if "id" not in definition:
|
|
1678
|
-
definition["id"] = provider_id
|
|
1679
|
-
provider = g_providers.get(provider_id)
|
|
1680
|
-
constructor_kwargs = create_provider_kwargs(definition, provider)
|
|
1681
|
-
provider = create_provider(constructor_kwargs)
|
|
1682
|
-
return provider, constructor_kwargs
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
1399
|
def create_provider_kwargs(definition, provider=None):
|
|
1686
1400
|
if provider:
|
|
1687
1401
|
provider = provider.copy()
|
|
@@ -1693,11 +1407,11 @@ def create_provider_kwargs(definition, provider=None):
|
|
|
1693
1407
|
if "api_key" in provider:
|
|
1694
1408
|
value = provider["api_key"]
|
|
1695
1409
|
if isinstance(value, str) and value.startswith("$"):
|
|
1696
|
-
provider["api_key"] = os.
|
|
1410
|
+
provider["api_key"] = os.environ.get(value[1:], "")
|
|
1697
1411
|
|
|
1698
1412
|
if "api_key" not in provider and "env" in provider:
|
|
1699
1413
|
for env_var in provider["env"]:
|
|
1700
|
-
val = os.
|
|
1414
|
+
val = os.environ.get(env_var)
|
|
1701
1415
|
if val:
|
|
1702
1416
|
provider["api_key"] = val
|
|
1703
1417
|
break
|
|
@@ -1709,15 +1423,6 @@ def create_provider_kwargs(definition, provider=None):
|
|
|
1709
1423
|
if isinstance(value, (list, dict)):
|
|
1710
1424
|
constructor_kwargs[key] = value.copy()
|
|
1711
1425
|
constructor_kwargs["headers"] = g_config["defaults"]["headers"].copy()
|
|
1712
|
-
|
|
1713
|
-
if "modalities" in definition:
|
|
1714
|
-
constructor_kwargs["modalities"] = {}
|
|
1715
|
-
for modality, modality_definition in definition["modalities"].items():
|
|
1716
|
-
modality_provider = create_provider(modality_definition)
|
|
1717
|
-
if not modality_provider:
|
|
1718
|
-
return None
|
|
1719
|
-
constructor_kwargs["modalities"][modality] = modality_provider
|
|
1720
|
-
|
|
1721
1426
|
return constructor_kwargs
|
|
1722
1427
|
|
|
1723
1428
|
|
|
@@ -1733,8 +1438,6 @@ def create_provider(provider):
|
|
|
1733
1438
|
for provider_type in g_app.all_providers:
|
|
1734
1439
|
if provider_type.sdk == npm_sdk:
|
|
1735
1440
|
kwargs = create_provider_kwargs(provider)
|
|
1736
|
-
if kwargs is None:
|
|
1737
|
-
kwargs = provider
|
|
1738
1441
|
return provider_type(**kwargs)
|
|
1739
1442
|
|
|
1740
1443
|
_log(f"Could not find provider {provider_label} with npm sdk {npm_sdk}")
|
|
@@ -1788,23 +1491,11 @@ async def update_providers(home_providers_path):
|
|
|
1788
1491
|
global g_providers
|
|
1789
1492
|
text = await get_text("https://models.dev/api.json")
|
|
1790
1493
|
all_providers = json.loads(text)
|
|
1791
|
-
extra_providers = {}
|
|
1792
|
-
extra_providers_path = home_providers_path.replace("providers.json", "providers-extra.json")
|
|
1793
|
-
if os.path.exists(extra_providers_path):
|
|
1794
|
-
with open(extra_providers_path) as f:
|
|
1795
|
-
extra_providers = json.load(f)
|
|
1796
1494
|
|
|
1797
1495
|
filtered_providers = {}
|
|
1798
1496
|
for id, provider in all_providers.items():
|
|
1799
1497
|
if id in g_config["providers"]:
|
|
1800
1498
|
filtered_providers[id] = provider
|
|
1801
|
-
if id in extra_providers and "models" in extra_providers[id]:
|
|
1802
|
-
for model_id, model in extra_providers[id]["models"].items():
|
|
1803
|
-
if "id" not in model:
|
|
1804
|
-
model["id"] = model_id
|
|
1805
|
-
if "name" not in model:
|
|
1806
|
-
model["name"] = id_to_name(model["id"])
|
|
1807
|
-
filtered_providers[id]["models"][model_id] = model
|
|
1808
1499
|
|
|
1809
1500
|
os.makedirs(os.path.dirname(home_providers_path), exist_ok=True)
|
|
1810
1501
|
with open(home_providers_path, "w", encoding="utf-8") as f:
|
|
@@ -1834,11 +1525,11 @@ def print_status():
|
|
|
1834
1525
|
|
|
1835
1526
|
|
|
1836
1527
|
def home_llms_path(filename):
|
|
1837
|
-
return f"{os.
|
|
1528
|
+
return f"{os.environ.get('HOME')}/.llms/{filename}"
|
|
1838
1529
|
|
|
1839
1530
|
|
|
1840
|
-
def get_cache_path(
|
|
1841
|
-
return home_llms_path(f"cache/{
|
|
1531
|
+
def get_cache_path(filename):
|
|
1532
|
+
return home_llms_path(f"cache/{filename}")
|
|
1842
1533
|
|
|
1843
1534
|
|
|
1844
1535
|
def get_config_path():
|
|
@@ -1847,8 +1538,8 @@ def get_config_path():
|
|
|
1847
1538
|
"./llms.json",
|
|
1848
1539
|
home_config_path,
|
|
1849
1540
|
]
|
|
1850
|
-
if os.
|
|
1851
|
-
check_paths.insert(0, os.
|
|
1541
|
+
if os.environ.get("LLMS_CONFIG_PATH"):
|
|
1542
|
+
check_paths.insert(0, os.environ.get("LLMS_CONFIG_PATH"))
|
|
1852
1543
|
|
|
1853
1544
|
for check_path in check_paths:
|
|
1854
1545
|
g_config_path = os.path.normpath(os.path.join(os.path.dirname(__file__), check_path))
|
|
@@ -1857,18 +1548,26 @@ def get_config_path():
|
|
|
1857
1548
|
return None
|
|
1858
1549
|
|
|
1859
1550
|
|
|
1551
|
+
def get_ui_path():
|
|
1552
|
+
ui_paths = [home_llms_path("ui.json"), "ui.json"]
|
|
1553
|
+
for ui_path in ui_paths:
|
|
1554
|
+
if os.path.exists(ui_path):
|
|
1555
|
+
return ui_path
|
|
1556
|
+
return None
|
|
1557
|
+
|
|
1558
|
+
|
|
1860
1559
|
def enable_provider(provider):
|
|
1861
1560
|
msg = None
|
|
1862
1561
|
provider_config = g_config["providers"][provider]
|
|
1863
|
-
if not provider_config:
|
|
1864
|
-
return None, f"Provider {provider} not found"
|
|
1865
|
-
|
|
1866
|
-
provider, constructor_kwargs = create_provider_from_definition(provider, provider_config)
|
|
1867
|
-
msg = provider.validate(**constructor_kwargs)
|
|
1868
|
-
if msg:
|
|
1869
|
-
return None, msg
|
|
1870
|
-
|
|
1871
1562
|
provider_config["enabled"] = True
|
|
1563
|
+
if "api_key" in provider_config:
|
|
1564
|
+
api_key = provider_config["api_key"]
|
|
1565
|
+
if isinstance(api_key, str):
|
|
1566
|
+
if api_key.startswith("$"):
|
|
1567
|
+
if not os.environ.get(api_key[1:], ""):
|
|
1568
|
+
msg = f"WARNING: {provider} requires missing API Key in Environment Variable {api_key}"
|
|
1569
|
+
else:
|
|
1570
|
+
msg = f"WARNING: {provider} is not configured with an API Key"
|
|
1872
1571
|
save_config(g_config)
|
|
1873
1572
|
init_llms(g_config, g_providers)
|
|
1874
1573
|
return provider_config, msg
|
|
@@ -2193,14 +1892,9 @@ async def text_from_resource_or_url(filename):
|
|
|
2193
1892
|
|
|
2194
1893
|
async def save_home_configs():
|
|
2195
1894
|
home_config_path = home_llms_path("llms.json")
|
|
1895
|
+
home_ui_path = home_llms_path("ui.json")
|
|
2196
1896
|
home_providers_path = home_llms_path("providers.json")
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
if (
|
|
2200
|
-
os.path.exists(home_config_path)
|
|
2201
|
-
and os.path.exists(home_providers_path)
|
|
2202
|
-
and os.path.exists(home_providers_extra_path)
|
|
2203
|
-
):
|
|
1897
|
+
if os.path.exists(home_config_path) and os.path.exists(home_ui_path) and os.path.exists(home_providers_path):
|
|
2204
1898
|
return
|
|
2205
1899
|
|
|
2206
1900
|
llms_home = os.path.dirname(home_config_path)
|
|
@@ -2212,17 +1906,17 @@ async def save_home_configs():
|
|
|
2212
1906
|
f.write(config_json)
|
|
2213
1907
|
_log(f"Created default config at {home_config_path}")
|
|
2214
1908
|
|
|
1909
|
+
if not os.path.exists(home_ui_path):
|
|
1910
|
+
ui_json = await text_from_resource_or_url("ui.json")
|
|
1911
|
+
with open(home_ui_path, "w", encoding="utf-8") as f:
|
|
1912
|
+
f.write(ui_json)
|
|
1913
|
+
_log(f"Created default ui config at {home_ui_path}")
|
|
1914
|
+
|
|
2215
1915
|
if not os.path.exists(home_providers_path):
|
|
2216
1916
|
providers_json = await text_from_resource_or_url("providers.json")
|
|
2217
1917
|
with open(home_providers_path, "w", encoding="utf-8") as f:
|
|
2218
1918
|
f.write(providers_json)
|
|
2219
1919
|
_log(f"Created default providers config at {home_providers_path}")
|
|
2220
|
-
|
|
2221
|
-
if not os.path.exists(home_providers_extra_path):
|
|
2222
|
-
extra_json = await text_from_resource_or_url("providers-extra.json")
|
|
2223
|
-
with open(home_providers_extra_path, "w", encoding="utf-8") as f:
|
|
2224
|
-
f.write(extra_json)
|
|
2225
|
-
_log(f"Created default extra providers config at {home_providers_extra_path}")
|
|
2226
1920
|
except Exception:
|
|
2227
1921
|
print("Could not create llms.json. Create one with --init or use --config <path>")
|
|
2228
1922
|
exit(1)
|
|
@@ -2259,52 +1953,59 @@ async def reload_providers():
|
|
|
2259
1953
|
return g_handlers
|
|
2260
1954
|
|
|
2261
1955
|
|
|
2262
|
-
async def watch_config_files(config_path,
|
|
1956
|
+
async def watch_config_files(config_path, ui_path, interval=1):
|
|
2263
1957
|
"""Watch config files and reload providers when they change"""
|
|
2264
1958
|
global g_config
|
|
2265
1959
|
|
|
2266
1960
|
config_path = Path(config_path)
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
_log(f"Watching config file: {config_path}")
|
|
2270
|
-
_log(f"Watching providers file: {providers_path}")
|
|
1961
|
+
ui_path = Path(ui_path) if ui_path else None
|
|
2271
1962
|
|
|
2272
|
-
|
|
2273
|
-
ret = 0
|
|
2274
|
-
name = "llms.json"
|
|
2275
|
-
if config_path.is_file():
|
|
2276
|
-
ret = config_path.stat().st_mtime
|
|
2277
|
-
name = config_path.name
|
|
2278
|
-
if providers_path.is_file() and providers_path.stat().st_mtime > ret:
|
|
2279
|
-
ret = providers_path.stat().st_mtime
|
|
2280
|
-
name = providers_path.name
|
|
2281
|
-
return ret, name
|
|
1963
|
+
file_mtimes = {}
|
|
2282
1964
|
|
|
2283
|
-
|
|
1965
|
+
_log(f"Watching config files: {config_path}" + (f", {ui_path}" if ui_path else ""))
|
|
2284
1966
|
|
|
2285
1967
|
while True:
|
|
2286
1968
|
await asyncio.sleep(interval)
|
|
2287
1969
|
|
|
2288
1970
|
# Check llms.json
|
|
2289
1971
|
try:
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
_log(f"Config file changed: {name}")
|
|
2293
|
-
latest_mtime = new_mtime
|
|
1972
|
+
if config_path.is_file():
|
|
1973
|
+
mtime = config_path.stat().st_mtime
|
|
2294
1974
|
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
1975
|
+
if str(config_path) not in file_mtimes:
|
|
1976
|
+
file_mtimes[str(config_path)] = mtime
|
|
1977
|
+
elif file_mtimes[str(config_path)] != mtime:
|
|
1978
|
+
_log(f"Config file changed: {config_path.name}")
|
|
1979
|
+
file_mtimes[str(config_path)] = mtime
|
|
2299
1980
|
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
1981
|
+
try:
|
|
1982
|
+
# Reload llms.json
|
|
1983
|
+
with open(config_path) as f:
|
|
1984
|
+
g_config = json.load(f)
|
|
1985
|
+
|
|
1986
|
+
# Reload providers
|
|
1987
|
+
await reload_providers()
|
|
1988
|
+
_log("Providers reloaded successfully")
|
|
1989
|
+
except Exception as e:
|
|
1990
|
+
_log(f"Error reloading config: {e}")
|
|
2305
1991
|
except FileNotFoundError:
|
|
2306
1992
|
pass
|
|
2307
1993
|
|
|
1994
|
+
# Check ui.json
|
|
1995
|
+
if ui_path:
|
|
1996
|
+
try:
|
|
1997
|
+
if ui_path.is_file():
|
|
1998
|
+
mtime = ui_path.stat().st_mtime
|
|
1999
|
+
|
|
2000
|
+
if str(ui_path) not in file_mtimes:
|
|
2001
|
+
file_mtimes[str(ui_path)] = mtime
|
|
2002
|
+
elif file_mtimes[str(ui_path)] != mtime:
|
|
2003
|
+
_log(f"Config file changed: {ui_path.name}")
|
|
2004
|
+
file_mtimes[str(ui_path)] = mtime
|
|
2005
|
+
_log("ui.json reloaded - reload page to update")
|
|
2006
|
+
except FileNotFoundError:
|
|
2007
|
+
pass
|
|
2008
|
+
|
|
2308
2009
|
|
|
2309
2010
|
def get_session_token(request):
|
|
2310
2011
|
return request.query.get("session") or request.headers.get("X-Session-Token") or request.cookies.get("llms-token")
|
|
@@ -2318,236 +2019,46 @@ class AppExtensions:
|
|
|
2318
2019
|
def __init__(self, cli_args, extra_args):
|
|
2319
2020
|
self.cli_args = cli_args
|
|
2320
2021
|
self.extra_args = extra_args
|
|
2321
|
-
self.config = None
|
|
2322
|
-
self.error_auth_required = create_error_response("Authentication required", "Unauthorized")
|
|
2323
2022
|
self.ui_extensions = []
|
|
2324
2023
|
self.chat_request_filters = []
|
|
2325
|
-
self.chat_tool_filters = []
|
|
2326
2024
|
self.chat_response_filters = []
|
|
2327
|
-
self.chat_error_filters = []
|
|
2328
2025
|
self.server_add_get = []
|
|
2329
2026
|
self.server_add_post = []
|
|
2330
|
-
self.server_add_put = []
|
|
2331
|
-
self.server_add_delete = []
|
|
2332
|
-
self.server_add_patch = []
|
|
2333
|
-
self.cache_saved_filters = []
|
|
2334
|
-
self.shutdown_handlers = []
|
|
2335
|
-
self.tools = {}
|
|
2336
|
-
self.tool_definitions = []
|
|
2337
|
-
self.index_headers = []
|
|
2338
|
-
self.index_footers = []
|
|
2339
|
-
self.request_args = {
|
|
2340
|
-
"image_config": dict, # e.g. { "aspect_ratio": "1:1" }
|
|
2341
|
-
"temperature": float, # e.g: 0.7
|
|
2342
|
-
"max_completion_tokens": int, # e.g: 2048
|
|
2343
|
-
"seed": int, # e.g: 42
|
|
2344
|
-
"top_p": float, # e.g: 0.9
|
|
2345
|
-
"frequency_penalty": float, # e.g: 0.5
|
|
2346
|
-
"presence_penalty": float, # e.g: 0.5
|
|
2347
|
-
"stop": list, # e.g: ["Stop"]
|
|
2348
|
-
"reasoning_effort": str, # e.g: minimal, low, medium, high
|
|
2349
|
-
"verbosity": str, # e.g: low, medium, high
|
|
2350
|
-
"service_tier": str, # e.g: auto, default
|
|
2351
|
-
"top_logprobs": int,
|
|
2352
|
-
"safety_identifier": str,
|
|
2353
|
-
"store": bool,
|
|
2354
|
-
"enable_thinking": bool,
|
|
2355
|
-
}
|
|
2356
2027
|
self.all_providers = [
|
|
2357
2028
|
OpenAiCompatible,
|
|
2029
|
+
OpenAiProvider,
|
|
2030
|
+
AnthropicProvider,
|
|
2358
2031
|
MistralProvider,
|
|
2359
2032
|
GroqProvider,
|
|
2360
2033
|
XaiProvider,
|
|
2361
2034
|
CodestralProvider,
|
|
2035
|
+
GoogleProvider,
|
|
2362
2036
|
OllamaProvider,
|
|
2363
2037
|
LMStudioProvider,
|
|
2364
2038
|
]
|
|
2365
|
-
self.aspect_ratios = {
|
|
2366
|
-
"1:1": "1024×1024",
|
|
2367
|
-
"2:3": "832×1248",
|
|
2368
|
-
"3:2": "1248×832",
|
|
2369
|
-
"3:4": "864×1184",
|
|
2370
|
-
"4:3": "1184×864",
|
|
2371
|
-
"4:5": "896×1152",
|
|
2372
|
-
"5:4": "1152×896",
|
|
2373
|
-
"9:16": "768×1344",
|
|
2374
|
-
"16:9": "1344×768",
|
|
2375
|
-
"21:9": "1536×672",
|
|
2376
|
-
}
|
|
2377
|
-
self.import_maps = {
|
|
2378
|
-
"vue-prod": "/ui/lib/vue.min.mjs",
|
|
2379
|
-
"vue": "/ui/lib/vue.mjs",
|
|
2380
|
-
"vue-router": "/ui/lib/vue-router.min.mjs",
|
|
2381
|
-
"@servicestack/client": "/ui/lib/servicestack-client.mjs",
|
|
2382
|
-
"@servicestack/vue": "/ui/lib/servicestack-vue.mjs",
|
|
2383
|
-
"idb": "/ui/lib/idb.min.mjs",
|
|
2384
|
-
"marked": "/ui/lib/marked.min.mjs",
|
|
2385
|
-
"highlight.js": "/ui/lib/highlight.min.mjs",
|
|
2386
|
-
"chart.js": "/ui/lib/chart.js",
|
|
2387
|
-
"color.js": "/ui/lib/color.js",
|
|
2388
|
-
"ctx.mjs": "/ui/ctx.mjs",
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
def set_config(self, config):
|
|
2392
|
-
self.config = config
|
|
2393
|
-
self.auth_enabled = self.config.get("auth", {}).get("enabled", False)
|
|
2394
|
-
|
|
2395
|
-
# Authentication middleware helper
|
|
2396
|
-
def check_auth(self, request):
|
|
2397
|
-
"""Check if request is authenticated. Returns (is_authenticated, user_data)"""
|
|
2398
|
-
if not self.auth_enabled:
|
|
2399
|
-
return True, None
|
|
2400
|
-
|
|
2401
|
-
# Check for OAuth session token
|
|
2402
|
-
session_token = get_session_token(request)
|
|
2403
|
-
if session_token and session_token in g_sessions:
|
|
2404
|
-
return True, g_sessions[session_token]
|
|
2405
|
-
|
|
2406
|
-
# Check for API key
|
|
2407
|
-
auth_header = request.headers.get("Authorization", "")
|
|
2408
|
-
if auth_header.startswith("Bearer "):
|
|
2409
|
-
api_key = auth_header[7:]
|
|
2410
|
-
if api_key:
|
|
2411
|
-
return True, {"authProvider": "apikey"}
|
|
2412
|
-
|
|
2413
|
-
return False, None
|
|
2414
|
-
|
|
2415
|
-
def get_session(self, request):
|
|
2416
|
-
session_token = get_session_token(request)
|
|
2417
|
-
|
|
2418
|
-
if not session_token or session_token not in g_sessions:
|
|
2419
|
-
return None
|
|
2420
|
-
|
|
2421
|
-
session_data = g_sessions[session_token]
|
|
2422
|
-
return session_data
|
|
2423
|
-
|
|
2424
|
-
def get_username(self, request):
|
|
2425
|
-
session = self.get_session(request)
|
|
2426
|
-
if session:
|
|
2427
|
-
return session.get("userName")
|
|
2428
|
-
return None
|
|
2429
|
-
|
|
2430
|
-
def get_user_path(self, username=None):
|
|
2431
|
-
if username:
|
|
2432
|
-
return home_llms_path(os.path.join("user", username))
|
|
2433
|
-
return home_llms_path(os.path.join("user", "default"))
|
|
2434
|
-
|
|
2435
|
-
def chat_request(self, template=None, text=None, model=None, system_prompt=None):
|
|
2436
|
-
return g_chat_request(template=template, text=text, model=model, system_prompt=system_prompt)
|
|
2437
|
-
|
|
2438
|
-
async def chat_completion(self, chat, context=None):
|
|
2439
|
-
response = await g_chat_completion(chat, context)
|
|
2440
|
-
return response
|
|
2441
|
-
|
|
2442
|
-
def on_cache_saved_filters(self, context):
|
|
2443
|
-
# _log(f"on_cache_saved_filters {len(self.cache_saved_filters)}: {context['url']}")
|
|
2444
|
-
for filter_func in self.cache_saved_filters:
|
|
2445
|
-
filter_func(context)
|
|
2446
|
-
|
|
2447
|
-
async def on_chat_error(self, e, context):
|
|
2448
|
-
# Apply chat error filters
|
|
2449
|
-
if "stackTrace" not in context:
|
|
2450
|
-
context["stackTrace"] = traceback.format_exc()
|
|
2451
|
-
for filter_func in self.chat_error_filters:
|
|
2452
|
-
try:
|
|
2453
|
-
await filter_func(e, context)
|
|
2454
|
-
except Exception as e:
|
|
2455
|
-
_err("chat error filter failed", e)
|
|
2456
|
-
|
|
2457
|
-
async def on_chat_tool(self, chat, context):
|
|
2458
|
-
m_len = len(chat.get("messages", []))
|
|
2459
|
-
t_len = len(self.chat_tool_filters)
|
|
2460
|
-
_dbg(
|
|
2461
|
-
f"on_tool_call for thread {context.get('threadId', None)} with {m_len} {pluralize('message', m_len)}, invoking {t_len} {pluralize('filter', t_len)}:"
|
|
2462
|
-
)
|
|
2463
|
-
for filter_func in self.chat_tool_filters:
|
|
2464
|
-
await filter_func(chat, context)
|
|
2465
|
-
|
|
2466
|
-
def exit(self, exit_code=0):
|
|
2467
|
-
if len(self.shutdown_handlers) > 0:
|
|
2468
|
-
_dbg(f"running {len(self.shutdown_handlers)} shutdown handlers...")
|
|
2469
|
-
for handler in self.shutdown_handlers:
|
|
2470
|
-
handler()
|
|
2471
|
-
|
|
2472
|
-
_dbg(f"exit({exit_code})")
|
|
2473
|
-
sys.exit(exit_code)
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
def handler_name(handler):
|
|
2477
|
-
if hasattr(handler, "__name__"):
|
|
2478
|
-
return handler.__name__
|
|
2479
|
-
return "unknown"
|
|
2480
2039
|
|
|
2481
2040
|
|
|
2482
2041
|
class ExtensionContext:
|
|
2483
2042
|
def __init__(self, app, path):
|
|
2484
2043
|
self.app = app
|
|
2485
|
-
self.cli_args = app.cli_args
|
|
2486
|
-
self.extra_args = app.extra_args
|
|
2487
|
-
self.error_auth_required = app.error_auth_required
|
|
2488
2044
|
self.path = path
|
|
2489
2045
|
self.name = os.path.basename(path)
|
|
2490
|
-
if self.name.endswith(".py"):
|
|
2491
|
-
self.name = self.name[:-3]
|
|
2492
2046
|
self.ext_prefix = f"/ext/{self.name}"
|
|
2493
|
-
self.MOCK = MOCK
|
|
2494
|
-
self.MOCK_DIR = MOCK_DIR
|
|
2495
|
-
self.debug = DEBUG
|
|
2496
|
-
self.verbose = g_verbose
|
|
2497
|
-
self.aspect_ratios = app.aspect_ratios
|
|
2498
|
-
self.request_args = app.request_args
|
|
2499
|
-
|
|
2500
|
-
def chat_to_prompt(self, chat):
|
|
2501
|
-
return chat_to_prompt(chat)
|
|
2502
|
-
|
|
2503
|
-
def chat_to_system_prompt(self, chat):
|
|
2504
|
-
return chat_to_system_prompt(chat)
|
|
2505
|
-
|
|
2506
|
-
def chat_response_to_message(self, response):
|
|
2507
|
-
return chat_response_to_message(response)
|
|
2508
|
-
|
|
2509
|
-
def last_user_prompt(self, chat):
|
|
2510
|
-
return last_user_prompt(chat)
|
|
2511
|
-
|
|
2512
|
-
def to_file_info(self, chat, info=None, response=None):
|
|
2513
|
-
return to_file_info(chat, info=info, response=response)
|
|
2514
|
-
|
|
2515
|
-
def save_image_to_cache(self, base64_data, filename, image_info):
|
|
2516
|
-
return save_image_to_cache(base64_data, filename, image_info)
|
|
2517
|
-
|
|
2518
|
-
def save_bytes_to_cache(self, bytes_data, filename, file_info):
|
|
2519
|
-
return save_bytes_to_cache(bytes_data, filename, file_info)
|
|
2520
|
-
|
|
2521
|
-
def text_from_file(self, path):
|
|
2522
|
-
return text_from_file(path)
|
|
2523
2047
|
|
|
2524
2048
|
def log(self, message):
|
|
2525
|
-
|
|
2526
|
-
print(f"[{self.name}] {message}", flush=True)
|
|
2527
|
-
return message
|
|
2528
|
-
|
|
2529
|
-
def log_json(self, obj):
|
|
2530
|
-
if self.verbose:
|
|
2531
|
-
print(f"[{self.name}] {json.dumps(obj, indent=2)}", flush=True)
|
|
2532
|
-
return obj
|
|
2049
|
+
print(f"[{self.name}] {message}", flush=True)
|
|
2533
2050
|
|
|
2534
2051
|
def dbg(self, message):
|
|
2535
|
-
if
|
|
2052
|
+
if DEBUG:
|
|
2536
2053
|
print(f"DEBUG [{self.name}]: {message}", flush=True)
|
|
2537
2054
|
|
|
2538
2055
|
def err(self, message, e):
|
|
2539
2056
|
print(f"ERROR [{self.name}]: {message}", e)
|
|
2540
|
-
if
|
|
2057
|
+
if g_verbose:
|
|
2541
2058
|
print(traceback.format_exc(), flush=True)
|
|
2542
2059
|
|
|
2543
|
-
def error_message(self, e):
|
|
2544
|
-
return to_error_message(e)
|
|
2545
|
-
|
|
2546
|
-
def error_response(self, e, stacktrace=False):
|
|
2547
|
-
return to_error_response(e, stacktrace=stacktrace)
|
|
2548
|
-
|
|
2549
2060
|
def add_provider(self, provider):
|
|
2550
|
-
self.log(f"Registered provider: {provider
|
|
2061
|
+
self.log(f"Registered provider: {provider}")
|
|
2551
2062
|
self.app.all_providers.append(provider)
|
|
2552
2063
|
|
|
2553
2064
|
def register_ui_extension(self, index):
|
|
@@ -2556,29 +2067,13 @@ class ExtensionContext:
|
|
|
2556
2067
|
self.app.ui_extensions.append({"id": self.name, "path": path})
|
|
2557
2068
|
|
|
2558
2069
|
def register_chat_request_filter(self, handler):
|
|
2559
|
-
self.log(f"Registered chat request filter: {
|
|
2070
|
+
self.log(f"Registered chat request filter: {handler}")
|
|
2560
2071
|
self.app.chat_request_filters.append(handler)
|
|
2561
2072
|
|
|
2562
|
-
def register_chat_tool_filter(self, handler):
|
|
2563
|
-
self.log(f"Registered chat tool filter: {handler_name(handler)}")
|
|
2564
|
-
self.app.chat_tool_filters.append(handler)
|
|
2565
|
-
|
|
2566
2073
|
def register_chat_response_filter(self, handler):
|
|
2567
|
-
self.log(f"Registered chat response filter: {
|
|
2074
|
+
self.log(f"Registered chat response filter: {handler}")
|
|
2568
2075
|
self.app.chat_response_filters.append(handler)
|
|
2569
2076
|
|
|
2570
|
-
def register_chat_error_filter(self, handler):
|
|
2571
|
-
self.log(f"Registered chat error filter: {handler_name(handler)}")
|
|
2572
|
-
self.app.chat_error_filters.append(handler)
|
|
2573
|
-
|
|
2574
|
-
def register_cache_saved_filter(self, handler):
|
|
2575
|
-
self.log(f"Registered cache saved filter: {handler_name(handler)}")
|
|
2576
|
-
self.app.cache_saved_filters.append(handler)
|
|
2577
|
-
|
|
2578
|
-
def register_shutdown_handler(self, handler):
|
|
2579
|
-
self.log(f"Registered shutdown handler: {handler_name(handler)}")
|
|
2580
|
-
self.app.shutdown_handlers.append(handler)
|
|
2581
|
-
|
|
2582
2077
|
def add_static_files(self, ext_dir):
|
|
2583
2078
|
self.log(f"Registered static files: {ext_dir}")
|
|
2584
2079
|
|
|
@@ -2599,38 +2094,11 @@ class ExtensionContext:
|
|
|
2599
2094
|
self.dbg(f"Registered POST: {os.path.join(self.ext_prefix, path)}")
|
|
2600
2095
|
self.app.server_add_post.append((os.path.join(self.ext_prefix, path), handler, kwargs))
|
|
2601
2096
|
|
|
2602
|
-
def add_put(self, path, handler, **kwargs):
|
|
2603
|
-
self.dbg(f"Registered PUT: {os.path.join(self.ext_prefix, path)}")
|
|
2604
|
-
self.app.server_add_put.append((os.path.join(self.ext_prefix, path), handler, kwargs))
|
|
2605
|
-
|
|
2606
|
-
def add_delete(self, path, handler, **kwargs):
|
|
2607
|
-
self.dbg(f"Registered DELETE: {os.path.join(self.ext_prefix, path)}")
|
|
2608
|
-
self.app.server_add_delete.append((os.path.join(self.ext_prefix, path), handler, kwargs))
|
|
2609
|
-
|
|
2610
|
-
def add_patch(self, path, handler, **kwargs):
|
|
2611
|
-
self.dbg(f"Registered PATCH: {os.path.join(self.ext_prefix, path)}")
|
|
2612
|
-
self.app.server_add_patch.append((os.path.join(self.ext_prefix, path), handler, kwargs))
|
|
2613
|
-
|
|
2614
|
-
def add_importmaps(self, dict):
|
|
2615
|
-
self.app.import_maps.update(dict)
|
|
2616
|
-
|
|
2617
|
-
def add_index_header(self, html):
|
|
2618
|
-
self.app.index_headers.append(html)
|
|
2619
|
-
|
|
2620
|
-
def add_index_footer(self, html):
|
|
2621
|
-
self.app.index_footers.append(html)
|
|
2622
|
-
|
|
2623
2097
|
def get_config(self):
|
|
2624
2098
|
return g_config
|
|
2625
2099
|
|
|
2626
|
-
def
|
|
2627
|
-
return
|
|
2628
|
-
|
|
2629
|
-
def chat_request(self, template=None, text=None, model=None, system_prompt=None):
|
|
2630
|
-
return self.app.chat_request(template=template, text=text, model=model, system_prompt=system_prompt)
|
|
2631
|
-
|
|
2632
|
-
def chat_completion(self, chat, context=None):
|
|
2633
|
-
return self.app.chat_completion(chat, context=context)
|
|
2100
|
+
def chat_completion(self, chat):
|
|
2101
|
+
return chat_completion(chat)
|
|
2634
2102
|
|
|
2635
2103
|
def get_providers(self):
|
|
2636
2104
|
return g_handlers
|
|
@@ -2638,92 +2106,32 @@ class ExtensionContext:
|
|
|
2638
2106
|
def get_provider(self, name):
|
|
2639
2107
|
return g_handlers.get(name)
|
|
2640
2108
|
|
|
2641
|
-
def register_tool(self, func, tool_def=None):
|
|
2642
|
-
if tool_def is None:
|
|
2643
|
-
tool_def = function_to_tool_definition(func)
|
|
2644
|
-
|
|
2645
|
-
name = tool_def["function"]["name"]
|
|
2646
|
-
self.log(f"Registered tool: {name}")
|
|
2647
|
-
self.app.tools[name] = func
|
|
2648
|
-
self.app.tool_definitions.append(tool_def)
|
|
2649
|
-
|
|
2650
|
-
def check_auth(self, request):
|
|
2651
|
-
return self.app.check_auth(request)
|
|
2652
|
-
|
|
2653
2109
|
def get_session(self, request):
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
def get_username(self, request):
|
|
2657
|
-
return self.app.get_username(request)
|
|
2658
|
-
|
|
2659
|
-
def get_user_path(self, username=None):
|
|
2660
|
-
return self.app.get_user_path(username)
|
|
2110
|
+
session_token = get_session_token(request)
|
|
2661
2111
|
|
|
2662
|
-
|
|
2663
|
-
|
|
2112
|
+
if not session_token or session_token not in g_sessions:
|
|
2113
|
+
return None
|
|
2664
2114
|
|
|
2665
|
-
|
|
2666
|
-
return
|
|
2115
|
+
session_data = g_sessions[session_token]
|
|
2116
|
+
return session_data
|
|
2667
2117
|
|
|
2668
|
-
def
|
|
2669
|
-
|
|
2118
|
+
def get_username(self, request):
|
|
2119
|
+
session = self.get_session(request)
|
|
2120
|
+
if session:
|
|
2121
|
+
return session.get("userName")
|
|
2122
|
+
return None
|
|
2670
2123
|
|
|
2671
2124
|
|
|
2672
2125
|
def get_extensions_path():
|
|
2673
|
-
return os.
|
|
2126
|
+
return os.path.join(Path.home(), ".llms", "extensions")
|
|
2674
2127
|
|
|
2675
2128
|
|
|
2676
|
-
def
|
|
2677
|
-
ret = DISABLE_EXTENSIONS.copy()
|
|
2678
|
-
if g_config:
|
|
2679
|
-
for ext in g_config.get("disable_extensions", []):
|
|
2680
|
-
if ext not in ret:
|
|
2681
|
-
ret.append(ext)
|
|
2682
|
-
return ret
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
def get_extensions_dirs():
|
|
2686
|
-
"""
|
|
2687
|
-
Returns a list of extension directories.
|
|
2688
|
-
"""
|
|
2129
|
+
def init_extensions(parser):
|
|
2689
2130
|
extensions_path = get_extensions_path()
|
|
2690
2131
|
os.makedirs(extensions_path, exist_ok=True)
|
|
2691
2132
|
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
if os.path.exists(extensions_path):
|
|
2695
|
-
override_extensions = os.listdir(extensions_path)
|
|
2696
|
-
|
|
2697
|
-
ret = []
|
|
2698
|
-
disabled_extensions = get_disabled_extensions()
|
|
2699
|
-
|
|
2700
|
-
builtin_extensions_dir = _ROOT / "extensions"
|
|
2701
|
-
if os.path.exists(builtin_extensions_dir):
|
|
2702
|
-
for item in os.listdir(builtin_extensions_dir):
|
|
2703
|
-
if os.path.isdir(os.path.join(builtin_extensions_dir, item)):
|
|
2704
|
-
if item in override_extensions:
|
|
2705
|
-
continue
|
|
2706
|
-
if item in disabled_extensions:
|
|
2707
|
-
continue
|
|
2708
|
-
ret.append(os.path.join(builtin_extensions_dir, item))
|
|
2709
|
-
|
|
2710
|
-
if os.path.exists(extensions_path):
|
|
2711
|
-
for item in os.listdir(extensions_path):
|
|
2712
|
-
if os.path.isdir(os.path.join(extensions_path, item)):
|
|
2713
|
-
if item in disabled_extensions:
|
|
2714
|
-
continue
|
|
2715
|
-
ret.append(os.path.join(extensions_path, item))
|
|
2716
|
-
|
|
2717
|
-
return ret
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
def init_extensions(parser):
|
|
2721
|
-
"""
|
|
2722
|
-
Initializes extensions by loading their __init__.py files and calling the __parser__ function if it exists.
|
|
2723
|
-
"""
|
|
2724
|
-
for item_path in get_extensions_dirs():
|
|
2725
|
-
item = os.path.basename(item_path)
|
|
2726
|
-
|
|
2133
|
+
for item in os.listdir(extensions_path):
|
|
2134
|
+
item_path = os.path.join(extensions_path, item)
|
|
2727
2135
|
if os.path.isdir(item_path):
|
|
2728
2136
|
try:
|
|
2729
2137
|
# check for __parser__ function if exists in __init.__.py and call it with parser
|
|
@@ -2748,28 +2156,25 @@ def install_extensions():
|
|
|
2748
2156
|
Scans ensure ~/.llms/extensions/ for directories with __init__.py and loads them as extensions.
|
|
2749
2157
|
Calls the `__install__(ctx)` function in the extension module.
|
|
2750
2158
|
"""
|
|
2159
|
+
extensions_path = get_extensions_path()
|
|
2160
|
+
os.makedirs(extensions_path, exist_ok=True)
|
|
2751
2161
|
|
|
2752
|
-
|
|
2753
|
-
ext_count = len(list(extension_dirs))
|
|
2162
|
+
ext_count = len(os.listdir(extensions_path))
|
|
2754
2163
|
if ext_count == 0:
|
|
2755
2164
|
_log("No extensions found")
|
|
2756
2165
|
return
|
|
2757
2166
|
|
|
2758
|
-
disabled_extensions = get_disabled_extensions()
|
|
2759
|
-
if len(disabled_extensions) > 0:
|
|
2760
|
-
_log(f"Disabled extensions: {', '.join(disabled_extensions)}")
|
|
2761
|
-
|
|
2762
2167
|
_log(f"Installing {ext_count} extension{'' if ext_count == 1 else 's'}...")
|
|
2763
2168
|
|
|
2764
|
-
|
|
2765
|
-
item = os.path.basename(item_path)
|
|
2169
|
+
sys.path.append(extensions_path)
|
|
2766
2170
|
|
|
2171
|
+
for item in os.listdir(extensions_path):
|
|
2172
|
+
item_path = os.path.join(extensions_path, item)
|
|
2767
2173
|
if os.path.isdir(item_path):
|
|
2768
|
-
|
|
2769
|
-
|
|
2174
|
+
init_file = os.path.join(item_path, "__init__.py")
|
|
2175
|
+
if os.path.exists(init_file):
|
|
2770
2176
|
ctx = ExtensionContext(g_app, item_path)
|
|
2771
|
-
|
|
2772
|
-
if os.path.exists(init_file):
|
|
2177
|
+
try:
|
|
2773
2178
|
spec = importlib.util.spec_from_file_location(item, init_file)
|
|
2774
2179
|
if spec and spec.loader:
|
|
2775
2180
|
module = importlib.util.module_from_spec(spec)
|
|
@@ -2784,20 +2189,20 @@ def install_extensions():
|
|
|
2784
2189
|
_dbg(f"Extension {item} has no __install__ function")
|
|
2785
2190
|
else:
|
|
2786
2191
|
_dbg(f"Extension {item} has no __init__.py")
|
|
2787
|
-
else:
|
|
2788
|
-
_dbg(f"Extension {init_file} not found")
|
|
2789
2192
|
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2193
|
+
# if ui folder exists, serve as static files at /ext/{item}/
|
|
2194
|
+
ui_path = os.path.join(item_path, "ui")
|
|
2195
|
+
if os.path.exists(ui_path):
|
|
2196
|
+
ctx.add_static_files(ui_path)
|
|
2794
2197
|
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2198
|
+
# Register UI extension if index.mjs exists (/ext/{item}/index.mjs)
|
|
2199
|
+
if os.path.exists(os.path.join(ui_path, "index.mjs")):
|
|
2200
|
+
ctx.register_ui_extension("index.mjs")
|
|
2798
2201
|
|
|
2799
|
-
|
|
2800
|
-
|
|
2202
|
+
except Exception as e:
|
|
2203
|
+
_err(f"Failed to install extension {item}", e)
|
|
2204
|
+
else:
|
|
2205
|
+
_dbg(f"Extension {init_file} not found")
|
|
2801
2206
|
else:
|
|
2802
2207
|
_dbg(f"Extension {item} not found: {item_path} is not a directory {os.path.exists(item_path)}")
|
|
2803
2208
|
|
|
@@ -2806,9 +2211,11 @@ def run_extension_cli():
|
|
|
2806
2211
|
"""
|
|
2807
2212
|
Run the CLI for an extension.
|
|
2808
2213
|
"""
|
|
2809
|
-
|
|
2810
|
-
|
|
2214
|
+
extensions_path = get_extensions_path()
|
|
2215
|
+
os.makedirs(extensions_path, exist_ok=True)
|
|
2811
2216
|
|
|
2217
|
+
for item in os.listdir(extensions_path):
|
|
2218
|
+
item_path = os.path.join(extensions_path, item)
|
|
2812
2219
|
if os.path.isdir(item_path):
|
|
2813
2220
|
init_file = os.path.join(item_path, "__init__.py")
|
|
2814
2221
|
if os.path.exists(init_file):
|
|
@@ -2823,8 +2230,8 @@ def run_extension_cli():
|
|
|
2823
2230
|
# Check for __run__ function if exists in __init__.py and call it with ctx
|
|
2824
2231
|
run_func = getattr(module, "__run__", None)
|
|
2825
2232
|
if callable(run_func):
|
|
2826
|
-
_log(f"Running extension {item}...")
|
|
2827
2233
|
handled = run_func(ctx)
|
|
2234
|
+
_log(f"Extension {item} was run")
|
|
2828
2235
|
return handled
|
|
2829
2236
|
|
|
2830
2237
|
except Exception as e:
|
|
@@ -2833,12 +2240,7 @@ def run_extension_cli():
|
|
|
2833
2240
|
|
|
2834
2241
|
|
|
2835
2242
|
def main():
|
|
2836
|
-
global _ROOT, g_verbose, g_default_model, g_logprefix, g_providers, g_config, g_config_path, g_app
|
|
2837
|
-
|
|
2838
|
-
_ROOT = os.getenv("LLMS_ROOT", resolve_root())
|
|
2839
|
-
if not _ROOT:
|
|
2840
|
-
print("Resource root not found")
|
|
2841
|
-
exit(1)
|
|
2243
|
+
global _ROOT, g_verbose, g_default_model, g_logprefix, g_providers, g_config, g_config_path, g_ui_path, g_app
|
|
2842
2244
|
|
|
2843
2245
|
parser = argparse.ArgumentParser(description=f"llms v{VERSION}")
|
|
2844
2246
|
parser.add_argument("--config", default=None, help="Path to config file", metavar="FILE")
|
|
@@ -2849,13 +2251,9 @@ def main():
|
|
|
2849
2251
|
parser.add_argument(
|
|
2850
2252
|
"-s", "--system", default=None, help="System prompt to use for chat completion", metavar="PROMPT"
|
|
2851
2253
|
)
|
|
2852
|
-
parser.add_argument(
|
|
2853
|
-
"--tools", default=None, help="Tools to use for chat completion (all|none|<tool>,<tool>...)", metavar="TOOLS"
|
|
2854
|
-
)
|
|
2855
2254
|
parser.add_argument("--image", default=None, help="Image input to use in chat completion")
|
|
2856
2255
|
parser.add_argument("--audio", default=None, help="Audio input to use in chat completion")
|
|
2857
2256
|
parser.add_argument("--file", default=None, help="File input to use in chat completion")
|
|
2858
|
-
parser.add_argument("--out", default=None, help="Image or Video Generation Request", metavar="MODALITY")
|
|
2859
2257
|
parser.add_argument(
|
|
2860
2258
|
"--args",
|
|
2861
2259
|
default=None,
|
|
@@ -2878,8 +2276,9 @@ def main():
|
|
|
2878
2276
|
parser.add_argument("--default", default=None, help="Configure the default model to use", metavar="MODEL")
|
|
2879
2277
|
|
|
2880
2278
|
parser.add_argument("--init", action="store_true", help="Create a default llms.json")
|
|
2881
|
-
parser.add_argument("--update
|
|
2279
|
+
parser.add_argument("--update", action="store_true", help="Update local models.dev providers.json")
|
|
2882
2280
|
|
|
2281
|
+
parser.add_argument("--root", default=None, help="Change root directory for UI files", metavar="PATH")
|
|
2883
2282
|
parser.add_argument("--logprefix", default="", help="Prefix used in log messages", metavar="PREFIX")
|
|
2884
2283
|
parser.add_argument("--verbose", action="store_true", help="Verbose output")
|
|
2885
2284
|
|
|
@@ -2900,15 +2299,6 @@ def main():
|
|
|
2900
2299
|
metavar="EXTENSION",
|
|
2901
2300
|
)
|
|
2902
2301
|
|
|
2903
|
-
parser.add_argument(
|
|
2904
|
-
"--update",
|
|
2905
|
-
nargs="?",
|
|
2906
|
-
const="ls",
|
|
2907
|
-
default=None,
|
|
2908
|
-
help="Update an extension (use 'all' to update all extensions)",
|
|
2909
|
-
metavar="EXTENSION",
|
|
2910
|
-
)
|
|
2911
|
-
|
|
2912
2302
|
# Load parser extensions, go through all extensions and load their parser arguments
|
|
2913
2303
|
init_extensions(parser)
|
|
2914
2304
|
|
|
@@ -2917,7 +2307,7 @@ def main():
|
|
|
2917
2307
|
g_app = AppExtensions(cli_args, extra_args)
|
|
2918
2308
|
|
|
2919
2309
|
# Check for verbose mode from CLI argument or environment variables
|
|
2920
|
-
verbose_env = os.
|
|
2310
|
+
verbose_env = os.environ.get("VERBOSE", "").lower()
|
|
2921
2311
|
if cli_args.verbose or verbose_env in ("1", "true"):
|
|
2922
2312
|
g_verbose = True
|
|
2923
2313
|
# printdump(cli_args)
|
|
@@ -2926,9 +2316,14 @@ def main():
|
|
|
2926
2316
|
if cli_args.logprefix:
|
|
2927
2317
|
g_logprefix = cli_args.logprefix
|
|
2928
2318
|
|
|
2319
|
+
_ROOT = Path(cli_args.root) if cli_args.root else resolve_root()
|
|
2320
|
+
if not _ROOT:
|
|
2321
|
+
print("Resource root not found")
|
|
2322
|
+
exit(1)
|
|
2323
|
+
|
|
2929
2324
|
home_config_path = home_llms_path("llms.json")
|
|
2325
|
+
home_ui_path = home_llms_path("ui.json")
|
|
2930
2326
|
home_providers_path = home_llms_path("providers.json")
|
|
2931
|
-
home_providers_extra_path = home_llms_path("providers-extra.json")
|
|
2932
2327
|
|
|
2933
2328
|
if cli_args.init:
|
|
2934
2329
|
if os.path.exists(home_config_path):
|
|
@@ -2937,17 +2332,17 @@ def main():
|
|
|
2937
2332
|
asyncio.run(save_default_config(home_config_path))
|
|
2938
2333
|
print(f"Created default config at {home_config_path}")
|
|
2939
2334
|
|
|
2335
|
+
if os.path.exists(home_ui_path):
|
|
2336
|
+
print(f"ui.json already exists at {home_ui_path}")
|
|
2337
|
+
else:
|
|
2338
|
+
asyncio.run(save_text_url(github_url("ui.json"), home_ui_path))
|
|
2339
|
+
print(f"Created default ui config at {home_ui_path}")
|
|
2340
|
+
|
|
2940
2341
|
if os.path.exists(home_providers_path):
|
|
2941
2342
|
print(f"providers.json already exists at {home_providers_path}")
|
|
2942
2343
|
else:
|
|
2943
2344
|
asyncio.run(save_text_url(github_url("providers.json"), home_providers_path))
|
|
2944
2345
|
print(f"Created default providers config at {home_providers_path}")
|
|
2945
|
-
|
|
2946
|
-
if os.path.exists(home_providers_extra_path):
|
|
2947
|
-
print(f"providers-extra.json already exists at {home_providers_extra_path}")
|
|
2948
|
-
else:
|
|
2949
|
-
asyncio.run(save_text_url(github_url("providers-extra.json"), home_providers_extra_path))
|
|
2950
|
-
print(f"Created default extra providers config at {home_providers_extra_path}")
|
|
2951
2346
|
exit(0)
|
|
2952
2347
|
|
|
2953
2348
|
if cli_args.providers:
|
|
@@ -2964,38 +2359,36 @@ def main():
|
|
|
2964
2359
|
g_config = load_config_json(config_json)
|
|
2965
2360
|
|
|
2966
2361
|
config_dir = os.path.dirname(g_config_path)
|
|
2362
|
+
# look for ui.json in same directory as config
|
|
2363
|
+
ui_path = os.path.join(config_dir, "ui.json")
|
|
2364
|
+
if os.path.exists(ui_path):
|
|
2365
|
+
g_ui_path = ui_path
|
|
2366
|
+
else:
|
|
2367
|
+
if not os.path.exists(home_ui_path):
|
|
2368
|
+
ui_json = text_from_resource("ui.json")
|
|
2369
|
+
with open(home_ui_path, "w", encoding="utf-8") as f:
|
|
2370
|
+
f.write(ui_json)
|
|
2371
|
+
_log(f"Created default ui config at {home_ui_path}")
|
|
2372
|
+
g_ui_path = home_ui_path
|
|
2967
2373
|
|
|
2968
2374
|
if not g_providers and os.path.exists(os.path.join(config_dir, "providers.json")):
|
|
2969
2375
|
g_providers = json.loads(text_from_file(os.path.join(config_dir, "providers.json")))
|
|
2970
2376
|
|
|
2971
2377
|
else:
|
|
2972
|
-
# ensure llms.json and
|
|
2378
|
+
# ensure llms.json and ui.json exist in home directory
|
|
2973
2379
|
asyncio.run(save_home_configs())
|
|
2974
2380
|
g_config_path = home_config_path
|
|
2381
|
+
g_ui_path = home_ui_path
|
|
2975
2382
|
g_config = load_config_json(text_from_file(g_config_path))
|
|
2976
2383
|
|
|
2977
|
-
g_app.set_config(g_config)
|
|
2978
|
-
|
|
2979
2384
|
if not g_providers:
|
|
2980
2385
|
g_providers = json.loads(text_from_file(home_providers_path))
|
|
2981
2386
|
|
|
2982
|
-
if cli_args.
|
|
2387
|
+
if cli_args.update:
|
|
2983
2388
|
asyncio.run(update_providers(home_providers_path))
|
|
2984
2389
|
print(f"Updated {home_providers_path}")
|
|
2985
2390
|
exit(0)
|
|
2986
2391
|
|
|
2987
|
-
# if home_providers_path is older than 1 day, update providers list
|
|
2988
|
-
if (
|
|
2989
|
-
os.path.exists(home_providers_path)
|
|
2990
|
-
and (time.time() - os.path.getmtime(home_providers_path)) > 86400
|
|
2991
|
-
and os.getenv("LLMS_DISABLE_UPDATE", "") != "1"
|
|
2992
|
-
):
|
|
2993
|
-
try:
|
|
2994
|
-
asyncio.run(update_providers(home_providers_path))
|
|
2995
|
-
_log(f"Updated {home_providers_path}")
|
|
2996
|
-
except Exception as e:
|
|
2997
|
-
_err("Failed to update providers", e)
|
|
2998
|
-
|
|
2999
2392
|
if cli_args.add is not None:
|
|
3000
2393
|
if cli_args.add == "ls":
|
|
3001
2394
|
|
|
@@ -3044,29 +2437,9 @@ def main():
|
|
|
3044
2437
|
requirements_path = os.path.join(target_path, "requirements.txt")
|
|
3045
2438
|
if os.path.exists(requirements_path):
|
|
3046
2439
|
print(f"Installing dependencies from {requirements_path}...")
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
try:
|
|
3051
|
-
subprocess.run(
|
|
3052
|
-
["uv", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True
|
|
3053
|
-
)
|
|
3054
|
-
has_uv = True
|
|
3055
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
3056
|
-
pass
|
|
3057
|
-
|
|
3058
|
-
if has_uv:
|
|
3059
|
-
subprocess.run(
|
|
3060
|
-
["uv", "pip", "install", "-p", sys.executable, "-r", "requirements.txt"],
|
|
3061
|
-
cwd=target_path,
|
|
3062
|
-
check=True,
|
|
3063
|
-
)
|
|
3064
|
-
else:
|
|
3065
|
-
subprocess.run(
|
|
3066
|
-
[sys.executable, "-m", "pip", "install", "-r", "requirements.txt"],
|
|
3067
|
-
cwd=target_path,
|
|
3068
|
-
check=True,
|
|
3069
|
-
)
|
|
2440
|
+
subprocess.run(
|
|
2441
|
+
[sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], cwd=target_path, check=True
|
|
2442
|
+
)
|
|
3070
2443
|
print("Dependencies installed successfully.")
|
|
3071
2444
|
|
|
3072
2445
|
print(f"Extension {target_name} installed successfully.")
|
|
@@ -3111,44 +2484,10 @@ def main():
|
|
|
3111
2484
|
|
|
3112
2485
|
exit(0)
|
|
3113
2486
|
|
|
3114
|
-
|
|
3115
|
-
if cli_args.update == "ls":
|
|
3116
|
-
# List installed extensions
|
|
3117
|
-
extensions_path = get_extensions_path()
|
|
3118
|
-
extensions = os.listdir(extensions_path)
|
|
3119
|
-
if len(extensions) == 0:
|
|
3120
|
-
print("No extensions installed.")
|
|
3121
|
-
exit(0)
|
|
3122
|
-
print("Installed extensions:")
|
|
3123
|
-
for extension in extensions:
|
|
3124
|
-
print(f" {extension}")
|
|
3125
|
-
|
|
3126
|
-
print("\nUsage:")
|
|
3127
|
-
print(" llms --update <extension>")
|
|
3128
|
-
print(" llms --update all")
|
|
3129
|
-
exit(0)
|
|
3130
|
-
|
|
3131
|
-
async def update_extensions(extension_name):
|
|
3132
|
-
extensions_path = get_extensions_path()
|
|
3133
|
-
for extension in os.listdir(extensions_path):
|
|
3134
|
-
extension_path = os.path.join(extensions_path, extension)
|
|
3135
|
-
if os.path.isdir(extension_path):
|
|
3136
|
-
if extension_name != "all" and extension != extension_name:
|
|
3137
|
-
continue
|
|
3138
|
-
result = subprocess.run(["git", "pull"], cwd=extension_path, capture_output=True)
|
|
3139
|
-
if result.returncode != 0:
|
|
3140
|
-
print(f"Failed to update extension {extension}: {result.stderr.decode('utf-8')}")
|
|
3141
|
-
continue
|
|
3142
|
-
print(f"Updated extension {extension}")
|
|
3143
|
-
_log(result.stdout.decode("utf-8"))
|
|
3144
|
-
|
|
3145
|
-
asyncio.run(update_extensions(cli_args.update))
|
|
3146
|
-
exit(0)
|
|
2487
|
+
asyncio.run(reload_providers())
|
|
3147
2488
|
|
|
3148
2489
|
install_extensions()
|
|
3149
2490
|
|
|
3150
|
-
asyncio.run(reload_providers())
|
|
3151
|
-
|
|
3152
2491
|
# print names
|
|
3153
2492
|
_log(f"enabled providers: {', '.join(g_handlers.keys())}")
|
|
3154
2493
|
|
|
@@ -3194,14 +2533,14 @@ def main():
|
|
|
3194
2533
|
print(f"\n{model_count} models available from {provider_count} providers")
|
|
3195
2534
|
|
|
3196
2535
|
print_status()
|
|
3197
|
-
|
|
2536
|
+
exit(0)
|
|
3198
2537
|
|
|
3199
2538
|
if cli_args.check is not None:
|
|
3200
2539
|
# Check validity of models for a provider
|
|
3201
2540
|
provider_name = cli_args.check
|
|
3202
2541
|
model_names = extra_args if len(extra_args) > 0 else None
|
|
3203
2542
|
asyncio.run(check_models(provider_name, model_names))
|
|
3204
|
-
|
|
2543
|
+
exit(0)
|
|
3205
2544
|
|
|
3206
2545
|
if cli_args.serve is not None:
|
|
3207
2546
|
# Disable inactive providers and save to config before starting server
|
|
@@ -3221,6 +2560,10 @@ def main():
|
|
|
3221
2560
|
# Start server
|
|
3222
2561
|
port = int(cli_args.serve)
|
|
3223
2562
|
|
|
2563
|
+
if not os.path.exists(g_ui_path):
|
|
2564
|
+
print(f"UI not found at {g_ui_path}")
|
|
2565
|
+
exit(1)
|
|
2566
|
+
|
|
3224
2567
|
# Validate auth configuration if enabled
|
|
3225
2568
|
auth_enabled = g_config.get("auth", {}).get("enabled", False)
|
|
3226
2569
|
if auth_enabled:
|
|
@@ -3234,8 +2577,8 @@ def main():
|
|
|
3234
2577
|
if client_secret.startswith("$"):
|
|
3235
2578
|
client_secret = client_secret[1:]
|
|
3236
2579
|
|
|
3237
|
-
client_id = os.
|
|
3238
|
-
client_secret = os.
|
|
2580
|
+
client_id = os.environ.get(client_id, client_id)
|
|
2581
|
+
client_secret = os.environ.get(client_secret, client_secret)
|
|
3239
2582
|
|
|
3240
2583
|
if (
|
|
3241
2584
|
not client_id
|
|
@@ -3256,25 +2599,74 @@ def main():
|
|
|
3256
2599
|
_log(f"client_max_size set to {client_max_size} bytes ({client_max_size / 1024 / 1024:.1f}MB)")
|
|
3257
2600
|
app = web.Application(client_max_size=client_max_size)
|
|
3258
2601
|
|
|
2602
|
+
# Authentication middleware helper
|
|
2603
|
+
def check_auth(request):
|
|
2604
|
+
"""Check if request is authenticated. Returns (is_authenticated, user_data)"""
|
|
2605
|
+
if not auth_enabled:
|
|
2606
|
+
return True, None
|
|
2607
|
+
|
|
2608
|
+
# Check for OAuth session token
|
|
2609
|
+
session_token = get_session_token(request)
|
|
2610
|
+
if session_token and session_token in g_sessions:
|
|
2611
|
+
return True, g_sessions[session_token]
|
|
2612
|
+
|
|
2613
|
+
# Check for API key
|
|
2614
|
+
auth_header = request.headers.get("Authorization", "")
|
|
2615
|
+
if auth_header.startswith("Bearer "):
|
|
2616
|
+
api_key = auth_header[7:]
|
|
2617
|
+
if api_key:
|
|
2618
|
+
return True, {"authProvider": "apikey"}
|
|
2619
|
+
|
|
2620
|
+
return False, None
|
|
2621
|
+
|
|
3259
2622
|
async def chat_handler(request):
|
|
3260
2623
|
# Check authentication if enabled
|
|
3261
|
-
is_authenticated, user_data =
|
|
2624
|
+
is_authenticated, user_data = check_auth(request)
|
|
3262
2625
|
if not is_authenticated:
|
|
3263
|
-
return web.json_response(
|
|
2626
|
+
return web.json_response(
|
|
2627
|
+
{
|
|
2628
|
+
"error": {
|
|
2629
|
+
"message": "Authentication required",
|
|
2630
|
+
"type": "authentication_error",
|
|
2631
|
+
"code": "unauthorized",
|
|
2632
|
+
}
|
|
2633
|
+
},
|
|
2634
|
+
status=401,
|
|
2635
|
+
)
|
|
3264
2636
|
|
|
3265
2637
|
try:
|
|
3266
2638
|
chat = await request.json()
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
context
|
|
3270
|
-
|
|
3271
|
-
|
|
2639
|
+
|
|
2640
|
+
# Apply pre-chat filters
|
|
2641
|
+
context = {"request": request}
|
|
2642
|
+
# Apply pre-chat filters
|
|
2643
|
+
context = {"request": request}
|
|
2644
|
+
for filter_func in g_app.chat_request_filters:
|
|
2645
|
+
chat = await filter_func(chat, context)
|
|
2646
|
+
|
|
2647
|
+
response = await chat_completion(chat)
|
|
2648
|
+
|
|
2649
|
+
# Apply post-chat filters
|
|
2650
|
+
# Apply post-chat filters
|
|
2651
|
+
for filter_func in g_app.chat_response_filters:
|
|
2652
|
+
response = await filter_func(response, context)
|
|
2653
|
+
|
|
3272
2654
|
return web.json_response(response)
|
|
3273
2655
|
except Exception as e:
|
|
3274
|
-
return web.json_response(
|
|
2656
|
+
return web.json_response({"error": str(e)}, status=500)
|
|
3275
2657
|
|
|
3276
2658
|
app.router.add_post("/v1/chat/completions", chat_handler)
|
|
3277
2659
|
|
|
2660
|
+
async def extensions_handler(request):
|
|
2661
|
+
return web.json_response(g_app.ui_extensions)
|
|
2662
|
+
|
|
2663
|
+
app.router.add_get("/ext", extensions_handler)
|
|
2664
|
+
|
|
2665
|
+
async def models_handler(request):
|
|
2666
|
+
return web.json_response(get_models())
|
|
2667
|
+
|
|
2668
|
+
app.router.add_get("/models/list", models_handler)
|
|
2669
|
+
|
|
3278
2670
|
async def active_models_handler(request):
|
|
3279
2671
|
return web.json_response(get_active_models())
|
|
3280
2672
|
|
|
@@ -3304,9 +2696,8 @@ def main():
|
|
|
3304
2696
|
if provider:
|
|
3305
2697
|
if data.get("enable", False):
|
|
3306
2698
|
provider_config, msg = enable_provider(provider)
|
|
3307
|
-
_log(f"Enabled provider {provider}
|
|
3308
|
-
|
|
3309
|
-
await load_llms()
|
|
2699
|
+
_log(f"Enabled provider {provider}")
|
|
2700
|
+
await load_llms()
|
|
3310
2701
|
elif data.get("disable", False):
|
|
3311
2702
|
disable_provider(provider)
|
|
3312
2703
|
_log(f"Disabled provider {provider}")
|
|
@@ -3323,9 +2714,18 @@ def main():
|
|
|
3323
2714
|
|
|
3324
2715
|
async def upload_handler(request):
|
|
3325
2716
|
# Check authentication if enabled
|
|
3326
|
-
is_authenticated, user_data =
|
|
2717
|
+
is_authenticated, user_data = check_auth(request)
|
|
3327
2718
|
if not is_authenticated:
|
|
3328
|
-
return web.json_response(
|
|
2719
|
+
return web.json_response(
|
|
2720
|
+
{
|
|
2721
|
+
"error": {
|
|
2722
|
+
"message": "Authentication required",
|
|
2723
|
+
"type": "authentication_error",
|
|
2724
|
+
"code": "unauthorized",
|
|
2725
|
+
}
|
|
2726
|
+
},
|
|
2727
|
+
status=401,
|
|
2728
|
+
)
|
|
3329
2729
|
|
|
3330
2730
|
reader = await request.multipart()
|
|
3331
2731
|
|
|
@@ -3335,7 +2735,7 @@ def main():
|
|
|
3335
2735
|
field = await reader.next()
|
|
3336
2736
|
|
|
3337
2737
|
if not field:
|
|
3338
|
-
return web.json_response(
|
|
2738
|
+
return web.json_response({"error": "No file provided"}, status=400)
|
|
3339
2739
|
|
|
3340
2740
|
filename = field.filename or "file"
|
|
3341
2741
|
content = await field.read()
|
|
@@ -3373,10 +2773,9 @@ def main():
|
|
|
3373
2773
|
with open(full_path, "wb") as f:
|
|
3374
2774
|
f.write(content)
|
|
3375
2775
|
|
|
3376
|
-
url = f"/~cache/{relative_path}"
|
|
3377
2776
|
response_data = {
|
|
3378
2777
|
"date": int(time.time()),
|
|
3379
|
-
"url":
|
|
2778
|
+
"url": f"/~cache/{relative_path}",
|
|
3380
2779
|
"size": len(content),
|
|
3381
2780
|
"type": mimetype,
|
|
3382
2781
|
"name": filename,
|
|
@@ -3396,22 +2795,10 @@ def main():
|
|
|
3396
2795
|
with open(info_path, "w") as f:
|
|
3397
2796
|
json.dump(response_data, f)
|
|
3398
2797
|
|
|
3399
|
-
g_app.on_cache_saved_filters({"url": url, "info": response_data})
|
|
3400
|
-
|
|
3401
2798
|
return web.json_response(response_data)
|
|
3402
2799
|
|
|
3403
2800
|
app.router.add_post("/upload", upload_handler)
|
|
3404
2801
|
|
|
3405
|
-
async def extensions_handler(request):
|
|
3406
|
-
return web.json_response(g_app.ui_extensions)
|
|
3407
|
-
|
|
3408
|
-
app.router.add_get("/ext", extensions_handler)
|
|
3409
|
-
|
|
3410
|
-
async def tools_handler(request):
|
|
3411
|
-
return web.json_response(g_app.tool_definitions)
|
|
3412
|
-
|
|
3413
|
-
app.router.add_get("/ext/tools", tools_handler)
|
|
3414
|
-
|
|
3415
2802
|
async def cache_handler(request):
|
|
3416
2803
|
path = request.match_info["tail"]
|
|
3417
2804
|
full_path = get_cache_path(path)
|
|
@@ -3423,7 +2810,7 @@ def main():
|
|
|
3423
2810
|
|
|
3424
2811
|
# Check for directory traversal for info path
|
|
3425
2812
|
try:
|
|
3426
|
-
cache_root = Path(get_cache_path())
|
|
2813
|
+
cache_root = Path(get_cache_path(""))
|
|
3427
2814
|
requested_path = Path(info_path).resolve()
|
|
3428
2815
|
if not str(requested_path).startswith(str(cache_root)):
|
|
3429
2816
|
return web.Response(text="403: Forbidden", status=403)
|
|
@@ -3439,7 +2826,7 @@ def main():
|
|
|
3439
2826
|
|
|
3440
2827
|
# Check for directory traversal
|
|
3441
2828
|
try:
|
|
3442
|
-
cache_root = Path(get_cache_path())
|
|
2829
|
+
cache_root = Path(get_cache_path(""))
|
|
3443
2830
|
requested_path = Path(full_path).resolve()
|
|
3444
2831
|
if not str(requested_path).startswith(str(cache_root)):
|
|
3445
2832
|
return web.Response(text="403: Forbidden", status=403)
|
|
@@ -3458,7 +2845,7 @@ def main():
|
|
|
3458
2845
|
async def github_auth_handler(request):
|
|
3459
2846
|
"""Initiate GitHub OAuth flow"""
|
|
3460
2847
|
if "auth" not in g_config or "github" not in g_config["auth"]:
|
|
3461
|
-
return web.json_response(
|
|
2848
|
+
return web.json_response({"error": "GitHub OAuth not configured"}, status=500)
|
|
3462
2849
|
|
|
3463
2850
|
auth_config = g_config["auth"]["github"]
|
|
3464
2851
|
client_id = auth_config.get("client_id", "")
|
|
@@ -3470,11 +2857,11 @@ def main():
|
|
|
3470
2857
|
if redirect_uri.startswith("$"):
|
|
3471
2858
|
redirect_uri = redirect_uri[1:]
|
|
3472
2859
|
|
|
3473
|
-
client_id = os.
|
|
3474
|
-
redirect_uri = os.
|
|
2860
|
+
client_id = os.environ.get(client_id, client_id)
|
|
2861
|
+
redirect_uri = os.environ.get(redirect_uri, redirect_uri)
|
|
3475
2862
|
|
|
3476
2863
|
if not client_id:
|
|
3477
|
-
return web.json_response(
|
|
2864
|
+
return web.json_response({"error": "GitHub client_id not configured"}, status=500)
|
|
3478
2865
|
|
|
3479
2866
|
# Generate CSRF state token
|
|
3480
2867
|
state = secrets.token_urlsafe(32)
|
|
@@ -3506,7 +2893,7 @@ def main():
|
|
|
3506
2893
|
if restrict_to.startswith("$"):
|
|
3507
2894
|
restrict_to = restrict_to[1:]
|
|
3508
2895
|
|
|
3509
|
-
restrict_to = os.
|
|
2896
|
+
restrict_to = os.environ.get(restrict_to, None if restrict_to == "GITHUB_USERS" else restrict_to)
|
|
3510
2897
|
|
|
3511
2898
|
# If restrict_to is configured, validate the user
|
|
3512
2899
|
if restrict_to:
|
|
@@ -3545,7 +2932,7 @@ def main():
|
|
|
3545
2932
|
g_oauth_states.pop(state)
|
|
3546
2933
|
|
|
3547
2934
|
if "auth" not in g_config or "github" not in g_config["auth"]:
|
|
3548
|
-
return web.json_response(
|
|
2935
|
+
return web.json_response({"error": "GitHub OAuth not configured"}, status=500)
|
|
3549
2936
|
|
|
3550
2937
|
auth_config = g_config["auth"]["github"]
|
|
3551
2938
|
client_id = auth_config.get("client_id", "")
|
|
@@ -3560,12 +2947,12 @@ def main():
|
|
|
3560
2947
|
if redirect_uri.startswith("$"):
|
|
3561
2948
|
redirect_uri = redirect_uri[1:]
|
|
3562
2949
|
|
|
3563
|
-
client_id = os.
|
|
3564
|
-
client_secret = os.
|
|
3565
|
-
redirect_uri = os.
|
|
2950
|
+
client_id = os.environ.get(client_id, client_id)
|
|
2951
|
+
client_secret = os.environ.get(client_secret, client_secret)
|
|
2952
|
+
redirect_uri = os.environ.get(redirect_uri, redirect_uri)
|
|
3566
2953
|
|
|
3567
2954
|
if not client_id or not client_secret:
|
|
3568
|
-
return web.json_response(
|
|
2955
|
+
return web.json_response({"error": "GitHub OAuth credentials not configured"}, status=500)
|
|
3569
2956
|
|
|
3570
2957
|
# Exchange code for access token
|
|
3571
2958
|
async with aiohttp.ClientSession() as session:
|
|
@@ -3584,7 +2971,7 @@ def main():
|
|
|
3584
2971
|
|
|
3585
2972
|
if not access_token:
|
|
3586
2973
|
error = token_response.get("error_description", "Failed to get access token")
|
|
3587
|
-
return web.
|
|
2974
|
+
return web.Response(text=f"OAuth error: {error}", status=400)
|
|
3588
2975
|
|
|
3589
2976
|
# Fetch user info
|
|
3590
2977
|
user_url = "https://api.github.com/user"
|
|
@@ -3619,7 +3006,7 @@ def main():
|
|
|
3619
3006
|
session_token = get_session_token(request)
|
|
3620
3007
|
|
|
3621
3008
|
if not session_token or session_token not in g_sessions:
|
|
3622
|
-
return web.json_response(
|
|
3009
|
+
return web.json_response({"error": "Invalid or expired session"}, status=401)
|
|
3623
3010
|
|
|
3624
3011
|
session_data = g_sessions[session_token]
|
|
3625
3012
|
|
|
@@ -3675,7 +3062,9 @@ def main():
|
|
|
3675
3062
|
# })
|
|
3676
3063
|
|
|
3677
3064
|
# Not authenticated - return error in expected format
|
|
3678
|
-
return web.json_response(
|
|
3065
|
+
return web.json_response(
|
|
3066
|
+
{"responseStatus": {"errorCode": "Unauthorized", "message": "Not authenticated"}}, status=401
|
|
3067
|
+
)
|
|
3679
3068
|
|
|
3680
3069
|
app.router.add_get("/auth", auth_handler)
|
|
3681
3070
|
app.router.add_get("/auth/github", github_auth_handler)
|
|
@@ -3715,18 +3104,19 @@ def main():
|
|
|
3715
3104
|
|
|
3716
3105
|
app.router.add_get("/ui/{path:.*}", ui_static, name="ui_static")
|
|
3717
3106
|
|
|
3718
|
-
async def
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3107
|
+
async def ui_config_handler(request):
|
|
3108
|
+
with open(g_ui_path, encoding="utf-8") as f:
|
|
3109
|
+
ui = json.load(f)
|
|
3110
|
+
if "defaults" not in ui:
|
|
3111
|
+
ui["defaults"] = g_config["defaults"]
|
|
3112
|
+
enabled, disabled = provider_status()
|
|
3113
|
+
ui["status"] = {"all": list(g_config["providers"].keys()), "enabled": enabled, "disabled": disabled}
|
|
3114
|
+
# Add auth configuration
|
|
3115
|
+
ui["requiresAuth"] = auth_enabled
|
|
3116
|
+
ui["authType"] = "oauth" if auth_enabled else "apikey"
|
|
3117
|
+
return web.json_response(ui)
|
|
3728
3118
|
|
|
3729
|
-
app.router.add_get("/config",
|
|
3119
|
+
app.router.add_get("/config", ui_config_handler)
|
|
3730
3120
|
|
|
3731
3121
|
async def not_found_handler(request):
|
|
3732
3122
|
return web.Response(text="404: Not Found", status=404)
|
|
@@ -3735,81 +3125,15 @@ def main():
|
|
|
3735
3125
|
|
|
3736
3126
|
# go through and register all g_app extensions
|
|
3737
3127
|
for handler in g_app.server_add_get:
|
|
3738
|
-
|
|
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_get(handler[0], managed_handler, **handler[2])
|
|
3128
|
+
app.router.add_get(handler[0], handler[1], **handler[2])
|
|
3747
3129
|
for handler in g_app.server_add_post:
|
|
3748
|
-
|
|
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_post(handler[0], managed_handler, **handler[2])
|
|
3757
|
-
for handler in g_app.server_add_put:
|
|
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_put(handler[0], managed_handler, **handler[2])
|
|
3767
|
-
for handler in g_app.server_add_delete:
|
|
3768
|
-
handler_fn = handler[1]
|
|
3769
|
-
|
|
3770
|
-
async def managed_handler(request, handler_fn=handler_fn):
|
|
3771
|
-
try:
|
|
3772
|
-
return await handler_fn(request)
|
|
3773
|
-
except Exception as e:
|
|
3774
|
-
return web.json_response(to_error_response(e, stacktrace=g_verbose), status=500)
|
|
3775
|
-
|
|
3776
|
-
app.router.add_delete(handler[0], managed_handler, **handler[2])
|
|
3777
|
-
for handler in g_app.server_add_patch:
|
|
3778
|
-
handler_fn = handler[1]
|
|
3779
|
-
|
|
3780
|
-
async def managed_handler(request, handler_fn=handler_fn):
|
|
3781
|
-
try:
|
|
3782
|
-
return await handler_fn(request)
|
|
3783
|
-
except Exception as e:
|
|
3784
|
-
return web.json_response(to_error_response(e, stacktrace=g_verbose), status=500)
|
|
3785
|
-
|
|
3786
|
-
app.router.add_patch(handler[0], managed_handler, **handler[2])
|
|
3130
|
+
app.router.add_post(handler[0], handler[1], **handler[2])
|
|
3787
3131
|
|
|
3788
3132
|
# Serve index.html from root
|
|
3789
3133
|
async def index_handler(request):
|
|
3790
3134
|
index_content = read_resource_file_bytes("index.html")
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
importmaps_script = '<script type="importmap">\n' + json.dumps(importmaps, indent=4) + "\n</script>"
|
|
3794
|
-
index_content = index_content.replace(
|
|
3795
|
-
b'<script type="importmap"></script>',
|
|
3796
|
-
importmaps_script.encode("utf-8"),
|
|
3797
|
-
)
|
|
3798
|
-
|
|
3799
|
-
if len(g_app.index_headers) > 0:
|
|
3800
|
-
html_header = ""
|
|
3801
|
-
for header in g_app.index_headers:
|
|
3802
|
-
html_header += header
|
|
3803
|
-
# replace </head> with html_header
|
|
3804
|
-
index_content = index_content.replace(b"</head>", html_header.encode("utf-8") + b"\n</head>")
|
|
3805
|
-
|
|
3806
|
-
if len(g_app.index_footers) > 0:
|
|
3807
|
-
html_footer = ""
|
|
3808
|
-
for footer in g_app.index_footers:
|
|
3809
|
-
html_footer += footer
|
|
3810
|
-
# replace </body> with html_footer
|
|
3811
|
-
index_content = index_content.replace(b"</body>", html_footer.encode("utf-8") + b"\n</body>")
|
|
3812
|
-
|
|
3135
|
+
if index_content is None:
|
|
3136
|
+
raise web.HTTPNotFound
|
|
3813
3137
|
return web.Response(body=index_content, content_type="text/html")
|
|
3814
3138
|
|
|
3815
3139
|
app.router.add_get("/", index_handler)
|
|
@@ -3821,7 +3145,7 @@ def main():
|
|
|
3821
3145
|
async def start_background_tasks(app):
|
|
3822
3146
|
"""Start background tasks when the app starts"""
|
|
3823
3147
|
# Start watching config files in the background
|
|
3824
|
-
asyncio.create_task(watch_config_files(g_config_path,
|
|
3148
|
+
asyncio.create_task(watch_config_files(g_config_path, g_ui_path))
|
|
3825
3149
|
|
|
3826
3150
|
app.on_startup.append(start_background_tasks)
|
|
3827
3151
|
|
|
@@ -3829,7 +3153,7 @@ def main():
|
|
|
3829
3153
|
|
|
3830
3154
|
print(f"Starting server on port {port}...")
|
|
3831
3155
|
web.run_app(app, host="0.0.0.0", port=port, print=_log)
|
|
3832
|
-
|
|
3156
|
+
exit(0)
|
|
3833
3157
|
|
|
3834
3158
|
if cli_args.enable is not None:
|
|
3835
3159
|
if cli_args.enable.endswith(","):
|
|
@@ -3846,7 +3170,7 @@ def main():
|
|
|
3846
3170
|
|
|
3847
3171
|
for provider in enable_providers:
|
|
3848
3172
|
if provider not in g_config["providers"]:
|
|
3849
|
-
print(f"Provider
|
|
3173
|
+
print(f"Provider {provider} not found")
|
|
3850
3174
|
print(f"Available providers: {', '.join(g_config['providers'].keys())}")
|
|
3851
3175
|
exit(1)
|
|
3852
3176
|
if provider in g_config["providers"]:
|
|
@@ -3859,7 +3183,7 @@ def main():
|
|
|
3859
3183
|
print_status()
|
|
3860
3184
|
if len(msgs) > 0:
|
|
3861
3185
|
print("\n" + "\n".join(msgs))
|
|
3862
|
-
|
|
3186
|
+
exit(0)
|
|
3863
3187
|
|
|
3864
3188
|
if cli_args.disable is not None:
|
|
3865
3189
|
if cli_args.disable.endswith(","):
|
|
@@ -3882,7 +3206,7 @@ def main():
|
|
|
3882
3206
|
print(f"\nDisabled provider {provider}")
|
|
3883
3207
|
|
|
3884
3208
|
print_status()
|
|
3885
|
-
|
|
3209
|
+
exit(0)
|
|
3886
3210
|
|
|
3887
3211
|
if cli_args.default is not None:
|
|
3888
3212
|
default_model = cli_args.default
|
|
@@ -3894,14 +3218,13 @@ def main():
|
|
|
3894
3218
|
default_text["model"] = default_model
|
|
3895
3219
|
save_config(g_config)
|
|
3896
3220
|
print(f"\nDefault model set to: {default_model}")
|
|
3897
|
-
|
|
3221
|
+
exit(0)
|
|
3898
3222
|
|
|
3899
3223
|
if (
|
|
3900
3224
|
cli_args.chat is not None
|
|
3901
3225
|
or cli_args.image is not None
|
|
3902
3226
|
or cli_args.audio is not None
|
|
3903
3227
|
or cli_args.file is not None
|
|
3904
|
-
or cli_args.out is not None
|
|
3905
3228
|
or len(extra_args) > 0
|
|
3906
3229
|
):
|
|
3907
3230
|
try:
|
|
@@ -3912,12 +3235,6 @@ def main():
|
|
|
3912
3235
|
chat = g_config["defaults"]["audio"]
|
|
3913
3236
|
elif cli_args.file is not None:
|
|
3914
3237
|
chat = g_config["defaults"]["file"]
|
|
3915
|
-
elif cli_args.out is not None:
|
|
3916
|
-
template = f"out:{cli_args.out}"
|
|
3917
|
-
if template not in g_config["defaults"]:
|
|
3918
|
-
print(f"Template for output modality '{cli_args.out}' not found")
|
|
3919
|
-
exit(1)
|
|
3920
|
-
chat = g_config["defaults"][template]
|
|
3921
3238
|
if cli_args.chat is not None:
|
|
3922
3239
|
chat_path = os.path.join(os.path.dirname(__file__), cli_args.chat)
|
|
3923
3240
|
if not os.path.exists(chat_path):
|
|
@@ -3934,9 +3251,6 @@ def main():
|
|
|
3934
3251
|
|
|
3935
3252
|
if len(extra_args) > 0:
|
|
3936
3253
|
prompt = " ".join(extra_args)
|
|
3937
|
-
if not chat["messages"] or len(chat["messages"]) == 0:
|
|
3938
|
-
chat["messages"] = [{"role": "user", "content": [{"type": "text", "text": ""}]}]
|
|
3939
|
-
|
|
3940
3254
|
# replace content of last message if exists, else add
|
|
3941
3255
|
last_msg = chat["messages"][-1] if "messages" in chat else None
|
|
3942
3256
|
if last_msg and last_msg["role"] == "user":
|
|
@@ -3954,31 +3268,22 @@ def main():
|
|
|
3954
3268
|
|
|
3955
3269
|
asyncio.run(
|
|
3956
3270
|
cli_chat(
|
|
3957
|
-
chat,
|
|
3958
|
-
tools=cli_args.tools,
|
|
3959
|
-
image=cli_args.image,
|
|
3960
|
-
audio=cli_args.audio,
|
|
3961
|
-
file=cli_args.file,
|
|
3962
|
-
args=args,
|
|
3963
|
-
raw=cli_args.raw,
|
|
3271
|
+
chat, image=cli_args.image, audio=cli_args.audio, file=cli_args.file, args=args, raw=cli_args.raw
|
|
3964
3272
|
)
|
|
3965
3273
|
)
|
|
3966
|
-
|
|
3274
|
+
exit(0)
|
|
3967
3275
|
except Exception as e:
|
|
3968
3276
|
print(f"{cli_args.logprefix}Error: {e}")
|
|
3969
3277
|
if cli_args.verbose:
|
|
3970
3278
|
traceback.print_exc()
|
|
3971
|
-
|
|
3279
|
+
exit(1)
|
|
3972
3280
|
|
|
3973
3281
|
handled = run_extension_cli()
|
|
3974
3282
|
|
|
3975
3283
|
if not handled:
|
|
3976
3284
|
# show usage from ArgumentParser
|
|
3977
3285
|
parser.print_help()
|
|
3978
|
-
g_app.exit(0)
|
|
3979
3286
|
|
|
3980
3287
|
|
|
3981
3288
|
if __name__ == "__main__":
|
|
3982
|
-
if MOCK or DEBUG:
|
|
3983
|
-
print(f"MOCK={MOCK} or DEBUG={DEBUG}")
|
|
3984
3289
|
main()
|