llms-py 2.0.18__py3-none-any.whl → 2.0.33__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/index.html +17 -1
- llms/llms.json +1132 -1075
- llms/main.py +561 -103
- llms/ui/Analytics.mjs +115 -104
- llms/ui/App.mjs +81 -4
- llms/ui/Avatar.mjs +61 -4
- llms/ui/Brand.mjs +29 -11
- llms/ui/ChatPrompt.mjs +163 -16
- llms/ui/Main.mjs +177 -94
- llms/ui/ModelSelector.mjs +28 -10
- llms/ui/OAuthSignIn.mjs +92 -0
- llms/ui/ProviderStatus.mjs +12 -12
- llms/ui/Recents.mjs +13 -13
- llms/ui/SettingsDialog.mjs +65 -65
- llms/ui/Sidebar.mjs +24 -19
- llms/ui/SystemPromptEditor.mjs +5 -5
- llms/ui/SystemPromptSelector.mjs +26 -6
- llms/ui/Welcome.mjs +2 -2
- llms/ui/ai.mjs +69 -5
- llms/ui/app.css +548 -34
- llms/ui/lib/servicestack-vue.mjs +9 -9
- llms/ui/markdown.mjs +8 -8
- llms/ui/tailwind.input.css +2 -0
- llms/ui/threadStore.mjs +39 -0
- llms/ui/typography.css +54 -36
- {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/METADATA +403 -47
- llms_py-2.0.33.dist-info/RECORD +48 -0
- {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/licenses/LICENSE +1 -2
- llms/__pycache__/__init__.cpython-312.pyc +0 -0
- llms/__pycache__/__init__.cpython-313.pyc +0 -0
- llms/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/__pycache__/__main__.cpython-312.pyc +0 -0
- llms/__pycache__/__main__.cpython-314.pyc +0 -0
- llms/__pycache__/llms.cpython-312.pyc +0 -0
- llms/__pycache__/main.cpython-312.pyc +0 -0
- llms/__pycache__/main.cpython-313.pyc +0 -0
- llms/__pycache__/main.cpython-314.pyc +0 -0
- llms_py-2.0.18.dist-info/RECORD +0 -56
- {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/WHEEL +0 -0
- {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/entry_points.txt +0 -0
- {llms_py-2.0.18.dist-info → llms_py-2.0.33.dist-info}/top_level.txt +0 -0
llms/main.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
|
|
3
|
+
# Copyright (c) Demis Bellot, ServiceStack <https://servicestack.net>
|
|
4
|
+
# License: https://github.com/ServiceStack/llms/blob/main/LICENSE
|
|
5
|
+
|
|
3
6
|
# A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers.
|
|
4
7
|
# Docs: https://github.com/ServiceStack/llms
|
|
5
8
|
|
|
@@ -14,7 +17,10 @@ import mimetypes
|
|
|
14
17
|
import traceback
|
|
15
18
|
import sys
|
|
16
19
|
import site
|
|
17
|
-
|
|
20
|
+
import secrets
|
|
21
|
+
import re
|
|
22
|
+
from io import BytesIO
|
|
23
|
+
from urllib.parse import parse_qs, urlencode
|
|
18
24
|
|
|
19
25
|
import aiohttp
|
|
20
26
|
from aiohttp import web
|
|
@@ -22,7 +28,13 @@ from aiohttp import web
|
|
|
22
28
|
from pathlib import Path
|
|
23
29
|
from importlib import resources # Py≥3.9 (pip install importlib_resources for 3.7/3.8)
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
try:
|
|
32
|
+
from PIL import Image
|
|
33
|
+
HAS_PIL = True
|
|
34
|
+
except ImportError:
|
|
35
|
+
HAS_PIL = False
|
|
36
|
+
|
|
37
|
+
VERSION = "2.0.33"
|
|
26
38
|
_ROOT = None
|
|
27
39
|
g_config_path = None
|
|
28
40
|
g_ui_path = None
|
|
@@ -31,6 +43,8 @@ g_handlers = {}
|
|
|
31
43
|
g_verbose = False
|
|
32
44
|
g_logprefix=""
|
|
33
45
|
g_default_model=""
|
|
46
|
+
g_sessions = {} # OAuth session storage: {session_token: {userId, userName, displayName, profileUrl, email, created}}
|
|
47
|
+
g_oauth_states = {} # CSRF protection: {state: {created, redirect_uri}}
|
|
34
48
|
|
|
35
49
|
def _log(message):
|
|
36
50
|
"""Helper method for logging from the global polling task."""
|
|
@@ -197,6 +211,77 @@ def price_to_string(price: float | int | str | None) -> str | None:
|
|
|
197
211
|
except (ValueError, TypeError):
|
|
198
212
|
return None
|
|
199
213
|
|
|
214
|
+
def convert_image_if_needed(image_bytes, mimetype='image/png'):
|
|
215
|
+
"""
|
|
216
|
+
Convert and resize image to WebP if it exceeds configured limits.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
image_bytes: Raw image bytes
|
|
220
|
+
mimetype: Original image MIME type
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
tuple: (converted_bytes, new_mimetype) or (original_bytes, original_mimetype) if no conversion needed
|
|
224
|
+
"""
|
|
225
|
+
if not HAS_PIL:
|
|
226
|
+
return image_bytes, mimetype
|
|
227
|
+
|
|
228
|
+
# Get conversion config
|
|
229
|
+
convert_config = g_config.get('convert', {}).get('image', {}) if g_config else {}
|
|
230
|
+
if not convert_config:
|
|
231
|
+
return image_bytes, mimetype
|
|
232
|
+
|
|
233
|
+
max_size_str = convert_config.get('max_size', '1536x1024')
|
|
234
|
+
max_length = convert_config.get('max_length', 1.5*1024*1024) # 1.5MB
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
# Parse max_size (e.g., "1536x1024")
|
|
238
|
+
max_width, max_height = map(int, max_size_str.split('x'))
|
|
239
|
+
|
|
240
|
+
# Open image
|
|
241
|
+
with Image.open(BytesIO(image_bytes)) as img:
|
|
242
|
+
original_width, original_height = img.size
|
|
243
|
+
|
|
244
|
+
# Check if image exceeds limits
|
|
245
|
+
needs_resize = original_width > max_width or original_height > max_height
|
|
246
|
+
|
|
247
|
+
# Check if base64 length would exceed max_length (in KB)
|
|
248
|
+
# Base64 encoding increases size by ~33%, so check raw bytes * 1.33 / 1024
|
|
249
|
+
estimated_kb = (len(image_bytes) * 1.33) / 1024
|
|
250
|
+
needs_conversion = estimated_kb > max_length
|
|
251
|
+
|
|
252
|
+
if not needs_resize and not needs_conversion:
|
|
253
|
+
return image_bytes, mimetype
|
|
254
|
+
|
|
255
|
+
# Convert RGBA to RGB if necessary (WebP doesn't support transparency in RGB mode)
|
|
256
|
+
if img.mode in ('RGBA', 'LA', 'P'):
|
|
257
|
+
# Create a white background
|
|
258
|
+
background = Image.new('RGB', img.size, (255, 255, 255))
|
|
259
|
+
if img.mode == 'P':
|
|
260
|
+
img = img.convert('RGBA')
|
|
261
|
+
background.paste(img, mask=img.split()[-1] if img.mode in ('RGBA', 'LA') else None)
|
|
262
|
+
img = background
|
|
263
|
+
elif img.mode != 'RGB':
|
|
264
|
+
img = img.convert('RGB')
|
|
265
|
+
|
|
266
|
+
# Resize if needed (preserve aspect ratio)
|
|
267
|
+
if needs_resize:
|
|
268
|
+
img.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
|
|
269
|
+
_log(f"Resized image from {original_width}x{original_height} to {img.size[0]}x{img.size[1]}")
|
|
270
|
+
|
|
271
|
+
# Convert to WebP
|
|
272
|
+
output = BytesIO()
|
|
273
|
+
img.save(output, format='WEBP', quality=85, method=6)
|
|
274
|
+
converted_bytes = output.getvalue()
|
|
275
|
+
|
|
276
|
+
_log(f"Converted image to WebP: {len(image_bytes)} bytes -> {len(converted_bytes)} bytes ({len(converted_bytes)*100//len(image_bytes)}%)")
|
|
277
|
+
|
|
278
|
+
return converted_bytes, 'image/webp'
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
_log(f"Error converting image: {e}")
|
|
282
|
+
# Return original if conversion fails
|
|
283
|
+
return image_bytes, mimetype
|
|
284
|
+
|
|
200
285
|
async def process_chat(chat):
|
|
201
286
|
if not chat:
|
|
202
287
|
raise Exception("No chat provided")
|
|
@@ -227,19 +312,31 @@ async def process_chat(chat):
|
|
|
227
312
|
mimetype = get_file_mime_type(get_filename(url))
|
|
228
313
|
if 'Content-Type' in response.headers:
|
|
229
314
|
mimetype = response.headers['Content-Type']
|
|
315
|
+
# convert/resize image if needed
|
|
316
|
+
content, mimetype = convert_image_if_needed(content, mimetype)
|
|
230
317
|
# convert to data uri
|
|
231
318
|
image_url['url'] = f"data:{mimetype};base64,{base64.b64encode(content).decode('utf-8')}"
|
|
232
319
|
elif is_file_path(url):
|
|
233
320
|
_log(f"Reading image: {url}")
|
|
234
321
|
with open(url, "rb") as f:
|
|
235
322
|
content = f.read()
|
|
236
|
-
ext = os.path.splitext(url)[1].lower().lstrip('.') if '.' in url else 'png'
|
|
237
323
|
# get mimetype from file extension
|
|
238
324
|
mimetype = get_file_mime_type(get_filename(url))
|
|
325
|
+
# convert/resize image if needed
|
|
326
|
+
content, mimetype = convert_image_if_needed(content, mimetype)
|
|
239
327
|
# convert to data uri
|
|
240
328
|
image_url['url'] = f"data:{mimetype};base64,{base64.b64encode(content).decode('utf-8')}"
|
|
241
329
|
elif url.startswith('data:'):
|
|
242
|
-
|
|
330
|
+
# Extract existing data URI and process it
|
|
331
|
+
if ';base64,' in url:
|
|
332
|
+
prefix = url.split(';base64,')[0]
|
|
333
|
+
mimetype = prefix.split(':')[1] if ':' in prefix else 'image/png'
|
|
334
|
+
base64_data = url.split(';base64,')[1]
|
|
335
|
+
content = base64.b64decode(base64_data)
|
|
336
|
+
# convert/resize image if needed
|
|
337
|
+
content, mimetype = convert_image_if_needed(content, mimetype)
|
|
338
|
+
# update data uri with potentially converted image
|
|
339
|
+
image_url['url'] = f"data:{mimetype};base64,{base64.b64encode(content).decode('utf-8')}"
|
|
243
340
|
else:
|
|
244
341
|
raise Exception(f"Invalid image: {url}")
|
|
245
342
|
elif item['type'] == 'input_audio' and 'input_audio' in item:
|
|
@@ -354,7 +451,7 @@ class OpenAiProvider:
|
|
|
354
451
|
|
|
355
452
|
@classmethod
|
|
356
453
|
def test(cls, base_url=None, api_key=None, models={}, **kwargs):
|
|
357
|
-
return base_url
|
|
454
|
+
return base_url and api_key and len(models) > 0
|
|
358
455
|
|
|
359
456
|
async def load(self):
|
|
360
457
|
pass
|
|
@@ -425,6 +522,8 @@ class OpenAiProvider:
|
|
|
425
522
|
chat = await process_chat(chat)
|
|
426
523
|
_log(f"POST {self.chat_url}")
|
|
427
524
|
_log(chat_summary(chat))
|
|
525
|
+
# remove metadata if any (conflicts with some providers, e.g. Z.ai)
|
|
526
|
+
chat.pop('metadata', None)
|
|
428
527
|
|
|
429
528
|
async with aiohttp.ClientSession() as session:
|
|
430
529
|
started_at = time.time()
|
|
@@ -467,7 +566,7 @@ class OllamaProvider(OpenAiProvider):
|
|
|
467
566
|
|
|
468
567
|
@classmethod
|
|
469
568
|
def test(cls, base_url=None, models={}, all_models=False, **kwargs):
|
|
470
|
-
return base_url
|
|
569
|
+
return base_url and (len(models) > 0 or all_models)
|
|
471
570
|
|
|
472
571
|
class GoogleOpenAiProvider(OpenAiProvider):
|
|
473
572
|
def __init__(self, api_key, models, **kwargs):
|
|
@@ -476,7 +575,7 @@ class GoogleOpenAiProvider(OpenAiProvider):
|
|
|
476
575
|
|
|
477
576
|
@classmethod
|
|
478
577
|
def test(cls, api_key=None, models={}, **kwargs):
|
|
479
|
-
return api_key
|
|
578
|
+
return api_key and len(models) > 0
|
|
480
579
|
|
|
481
580
|
class GoogleProvider(OpenAiProvider):
|
|
482
581
|
def __init__(self, models, api_key, safety_settings=None, thinking_config=None, curl=False, **kwargs):
|
|
@@ -912,7 +1011,7 @@ async def load_llms():
|
|
|
912
1011
|
await provider.load()
|
|
913
1012
|
|
|
914
1013
|
def save_config(config):
|
|
915
|
-
global g_config
|
|
1014
|
+
global g_config, g_config_path
|
|
916
1015
|
g_config = config
|
|
917
1016
|
with open(g_config_path, "w") as f:
|
|
918
1017
|
json.dump(g_config, f, indent=4)
|
|
@@ -921,29 +1020,27 @@ def save_config(config):
|
|
|
921
1020
|
def github_url(filename):
|
|
922
1021
|
return f"https://raw.githubusercontent.com/ServiceStack/llms/refs/heads/main/llms/{filename}"
|
|
923
1022
|
|
|
924
|
-
async def
|
|
1023
|
+
async def get_text(url):
|
|
925
1024
|
async with aiohttp.ClientSession() as session:
|
|
926
1025
|
_log(f"GET {url}")
|
|
927
1026
|
async with session.get(url) as resp:
|
|
928
1027
|
text = await resp.text()
|
|
929
1028
|
if resp.status >= 400:
|
|
930
1029
|
raise HTTPError(resp.status, reason=resp.reason, body=text, headers=dict(resp.headers))
|
|
931
|
-
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
|
932
|
-
with open(save_path, "w") as f:
|
|
933
|
-
f.write(text)
|
|
934
1030
|
return text
|
|
935
1031
|
|
|
1032
|
+
async def save_text_url(url, save_path):
|
|
1033
|
+
text = await get_text(url)
|
|
1034
|
+
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
|
1035
|
+
with open(save_path, "w") as f:
|
|
1036
|
+
f.write(text)
|
|
1037
|
+
return text
|
|
1038
|
+
|
|
936
1039
|
async def save_default_config(config_path):
|
|
937
1040
|
global g_config
|
|
938
|
-
config_json = await
|
|
1041
|
+
config_json = await save_text_url(github_url("llms.json"), config_path)
|
|
939
1042
|
g_config = json.loads(config_json)
|
|
940
1043
|
|
|
941
|
-
async def update_llms():
|
|
942
|
-
"""
|
|
943
|
-
Update llms.py from GitHub
|
|
944
|
-
"""
|
|
945
|
-
await save_text(github_url("llms.py"), __file__)
|
|
946
|
-
|
|
947
1044
|
def provider_status():
|
|
948
1045
|
enabled = list(g_handlers.keys())
|
|
949
1046
|
disabled = [provider for provider in g_config['providers'].keys() if provider not in enabled]
|
|
@@ -1262,8 +1359,119 @@ async def check_models(provider_name, model_names=None):
|
|
|
1262
1359
|
|
|
1263
1360
|
print()
|
|
1264
1361
|
|
|
1362
|
+
def text_from_resource(filename):
|
|
1363
|
+
global _ROOT
|
|
1364
|
+
resource_path = _ROOT / filename
|
|
1365
|
+
if resource_exists(resource_path):
|
|
1366
|
+
try:
|
|
1367
|
+
return read_resource_text(resource_path)
|
|
1368
|
+
except (OSError, AttributeError) as e:
|
|
1369
|
+
_log(f"Error reading resource config {filename}: {e}")
|
|
1370
|
+
return None
|
|
1371
|
+
|
|
1372
|
+
def text_from_file(filename):
|
|
1373
|
+
if os.path.exists(filename):
|
|
1374
|
+
with open(filename, "r") as f:
|
|
1375
|
+
return f.read()
|
|
1376
|
+
return None
|
|
1377
|
+
|
|
1378
|
+
async def text_from_resource_or_url(filename):
|
|
1379
|
+
text = text_from_resource(filename)
|
|
1380
|
+
if not text:
|
|
1381
|
+
try:
|
|
1382
|
+
resource_url = github_url(filename)
|
|
1383
|
+
text = await get_text(resource_url)
|
|
1384
|
+
except Exception as e:
|
|
1385
|
+
_log(f"Error downloading JSON from {resource_url}: {e}")
|
|
1386
|
+
raise e
|
|
1387
|
+
return text
|
|
1388
|
+
|
|
1389
|
+
async def save_home_configs():
|
|
1390
|
+
home_config_path = home_llms_path("llms.json")
|
|
1391
|
+
home_ui_path = home_llms_path("ui.json")
|
|
1392
|
+
if os.path.exists(home_config_path) and os.path.exists(home_ui_path):
|
|
1393
|
+
return
|
|
1394
|
+
|
|
1395
|
+
llms_home = os.path.dirname(home_config_path)
|
|
1396
|
+
os.makedirs(llms_home, exist_ok=True)
|
|
1397
|
+
try:
|
|
1398
|
+
if not os.path.exists(home_config_path):
|
|
1399
|
+
config_json = await text_from_resource_or_url("llms.json")
|
|
1400
|
+
with open(home_config_path, "w") as f:
|
|
1401
|
+
f.write(config_json)
|
|
1402
|
+
_log(f"Created default config at {home_config_path}")
|
|
1403
|
+
|
|
1404
|
+
if not os.path.exists(home_ui_path):
|
|
1405
|
+
ui_json = await text_from_resource_or_url("ui.json")
|
|
1406
|
+
with open(home_ui_path, "w") as f:
|
|
1407
|
+
f.write(ui_json)
|
|
1408
|
+
_log(f"Created default ui config at {home_ui_path}")
|
|
1409
|
+
except Exception as e:
|
|
1410
|
+
print("Could not create llms.json. Create one with --init or use --config <path>")
|
|
1411
|
+
exit(1)
|
|
1412
|
+
|
|
1413
|
+
async def reload_providers():
|
|
1414
|
+
global g_config, g_handlers
|
|
1415
|
+
g_handlers = init_llms(g_config)
|
|
1416
|
+
await load_llms()
|
|
1417
|
+
_log(f"{len(g_handlers)} providers loaded")
|
|
1418
|
+
return g_handlers
|
|
1419
|
+
|
|
1420
|
+
async def watch_config_files(config_path, ui_path, interval=1):
|
|
1421
|
+
"""Watch config files and reload providers when they change"""
|
|
1422
|
+
global g_config
|
|
1423
|
+
|
|
1424
|
+
config_path = Path(config_path)
|
|
1425
|
+
ui_path = Path(ui_path) if ui_path else None
|
|
1426
|
+
|
|
1427
|
+
file_mtimes = {}
|
|
1428
|
+
|
|
1429
|
+
_log(f"Watching config files: {config_path}" + (f", {ui_path}" if ui_path else ""))
|
|
1430
|
+
|
|
1431
|
+
while True:
|
|
1432
|
+
await asyncio.sleep(interval)
|
|
1433
|
+
|
|
1434
|
+
# Check llms.json
|
|
1435
|
+
try:
|
|
1436
|
+
if config_path.is_file():
|
|
1437
|
+
mtime = config_path.stat().st_mtime
|
|
1438
|
+
|
|
1439
|
+
if str(config_path) not in file_mtimes:
|
|
1440
|
+
file_mtimes[str(config_path)] = mtime
|
|
1441
|
+
elif file_mtimes[str(config_path)] != mtime:
|
|
1442
|
+
_log(f"Config file changed: {config_path.name}")
|
|
1443
|
+
file_mtimes[str(config_path)] = mtime
|
|
1444
|
+
|
|
1445
|
+
try:
|
|
1446
|
+
# Reload llms.json
|
|
1447
|
+
with open(config_path, "r") as f:
|
|
1448
|
+
g_config = json.load(f)
|
|
1449
|
+
|
|
1450
|
+
# Reload providers
|
|
1451
|
+
await reload_providers()
|
|
1452
|
+
_log("Providers reloaded successfully")
|
|
1453
|
+
except Exception as e:
|
|
1454
|
+
_log(f"Error reloading config: {e}")
|
|
1455
|
+
except FileNotFoundError:
|
|
1456
|
+
pass
|
|
1457
|
+
|
|
1458
|
+
# Check ui.json
|
|
1459
|
+
if ui_path:
|
|
1460
|
+
try:
|
|
1461
|
+
if ui_path.is_file():
|
|
1462
|
+
mtime = ui_path.stat().st_mtime
|
|
1463
|
+
|
|
1464
|
+
if str(ui_path) not in file_mtimes:
|
|
1465
|
+
file_mtimes[str(ui_path)] = mtime
|
|
1466
|
+
elif file_mtimes[str(ui_path)] != mtime:
|
|
1467
|
+
_log(f"Config file changed: {ui_path.name}")
|
|
1468
|
+
file_mtimes[str(ui_path)] = mtime
|
|
1469
|
+
_log("ui.json reloaded - reload page to update")
|
|
1470
|
+
except FileNotFoundError:
|
|
1471
|
+
pass
|
|
1472
|
+
|
|
1265
1473
|
def main():
|
|
1266
|
-
global _ROOT, g_verbose, g_default_model, g_logprefix, g_config_path, g_ui_path
|
|
1474
|
+
global _ROOT, g_verbose, g_default_model, g_logprefix, g_config, g_config_path, g_ui_path
|
|
1267
1475
|
|
|
1268
1476
|
parser = argparse.ArgumentParser(description=f"llms v{VERSION}")
|
|
1269
1477
|
parser.add_argument('--config', default=None, help='Path to config file', metavar='FILE')
|
|
@@ -1291,10 +1499,12 @@ def main():
|
|
|
1291
1499
|
parser.add_argument('--root', default=None, help='Change root directory for UI files', metavar='PATH')
|
|
1292
1500
|
parser.add_argument('--logprefix', default="", help='Prefix used in log messages', metavar='PREFIX')
|
|
1293
1501
|
parser.add_argument('--verbose', action='store_true', help='Verbose output')
|
|
1294
|
-
parser.add_argument('--update', action='store_true', help='Update to latest version')
|
|
1295
1502
|
|
|
1296
1503
|
cli_args, extra_args = parser.parse_known_args()
|
|
1297
|
-
|
|
1504
|
+
|
|
1505
|
+
# Check for verbose mode from CLI argument or environment variables
|
|
1506
|
+
verbose_env = os.environ.get('VERBOSE', '').lower()
|
|
1507
|
+
if cli_args.verbose or verbose_env in ('1', 'true'):
|
|
1298
1508
|
g_verbose = True
|
|
1299
1509
|
# printdump(cli_args)
|
|
1300
1510
|
if cli_args.model:
|
|
@@ -1302,24 +1512,13 @@ def main():
|
|
|
1302
1512
|
if cli_args.logprefix:
|
|
1303
1513
|
g_logprefix = cli_args.logprefix
|
|
1304
1514
|
|
|
1305
|
-
if cli_args.
|
|
1306
|
-
g_config_path = os.path.join(os.path.dirname(__file__), cli_args.config)
|
|
1307
|
-
|
|
1308
|
-
_ROOT = resolve_root()
|
|
1309
|
-
if cli_args.root:
|
|
1310
|
-
_ROOT = Path(cli_args.root)
|
|
1311
|
-
|
|
1515
|
+
_ROOT = Path(cli_args.root) if cli_args.root else resolve_root()
|
|
1312
1516
|
if not _ROOT:
|
|
1313
1517
|
print("Resource root not found")
|
|
1314
1518
|
exit(1)
|
|
1315
1519
|
|
|
1316
|
-
g_config_path = os.path.join(os.path.dirname(__file__), cli_args.config) if cli_args.config else get_config_path()
|
|
1317
|
-
g_ui_path = get_ui_path()
|
|
1318
|
-
|
|
1319
1520
|
home_config_path = home_llms_path("llms.json")
|
|
1320
|
-
resource_config_path = _ROOT / "llms.json"
|
|
1321
1521
|
home_ui_path = home_llms_path("ui.json")
|
|
1322
|
-
resource_ui_path = _ROOT / "ui.json"
|
|
1323
1522
|
|
|
1324
1523
|
if cli_args.init:
|
|
1325
1524
|
if os.path.exists(home_config_path):
|
|
@@ -1331,74 +1530,37 @@ def main():
|
|
|
1331
1530
|
if os.path.exists(home_ui_path):
|
|
1332
1531
|
print(f"ui.json already exists at {home_ui_path}")
|
|
1333
1532
|
else:
|
|
1334
|
-
asyncio.run(
|
|
1533
|
+
asyncio.run(save_text_url(github_url("ui.json"), home_ui_path))
|
|
1335
1534
|
print(f"Created default ui config at {home_ui_path}")
|
|
1336
1535
|
exit(0)
|
|
1337
1536
|
|
|
1338
|
-
if
|
|
1339
|
-
#
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
if resource_exists(resource_config_path):
|
|
1346
|
-
try:
|
|
1347
|
-
# Read config from resource (handle both Path and Traversable objects)
|
|
1348
|
-
config_json = read_resource_text(resource_config_path)
|
|
1349
|
-
except (OSError, AttributeError) as e:
|
|
1350
|
-
_log(f"Error reading resource config: {e}")
|
|
1351
|
-
if not config_json:
|
|
1352
|
-
try:
|
|
1353
|
-
config_json = asyncio.run(save_text(github_url("llms.json"), home_config_path))
|
|
1354
|
-
except Exception as e:
|
|
1355
|
-
_log(f"Error downloading llms.json: {e}")
|
|
1356
|
-
print("Could not create llms.json. Create one with --init or use --config <path>")
|
|
1357
|
-
exit(1)
|
|
1358
|
-
|
|
1359
|
-
with open(home_config_path, "w") as f:
|
|
1360
|
-
f.write(config_json)
|
|
1361
|
-
_log(f"Created default config at {home_config_path}")
|
|
1362
|
-
# Update g_config_path to point to the copied file
|
|
1363
|
-
g_config_path = home_config_path
|
|
1364
|
-
if not g_config_path or not os.path.exists(g_config_path):
|
|
1365
|
-
print("llms.json not found. Create one with --init or use --config <path>")
|
|
1366
|
-
exit(1)
|
|
1537
|
+
if cli_args.config:
|
|
1538
|
+
# read contents
|
|
1539
|
+
g_config_path = os.path.join(os.path.dirname(__file__), cli_args.config)
|
|
1540
|
+
with open(g_config_path, "r") as f:
|
|
1541
|
+
config_json = f.read()
|
|
1542
|
+
g_config = json.loads(config_json)
|
|
1367
1543
|
|
|
1368
|
-
|
|
1369
|
-
#
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
_log(f"Error reading resource ui config: {e}")
|
|
1379
|
-
if not ui_json:
|
|
1380
|
-
try:
|
|
1381
|
-
ui_json = asyncio.run(save_text(github_url("ui.json"), home_ui_path))
|
|
1382
|
-
except Exception as e:
|
|
1383
|
-
_log(f"Error downloading ui.json: {e}")
|
|
1384
|
-
print("Could not create ui.json. Create one with --init or use --config <path>")
|
|
1385
|
-
exit(1)
|
|
1386
|
-
|
|
1387
|
-
with open(home_ui_path, "w") as f:
|
|
1388
|
-
f.write(ui_json)
|
|
1544
|
+
config_dir = os.path.dirname(g_config_path)
|
|
1545
|
+
# look for ui.json in same directory as config
|
|
1546
|
+
ui_path = os.path.join(config_dir, "ui.json")
|
|
1547
|
+
if os.path.exists(ui_path):
|
|
1548
|
+
g_ui_path = ui_path
|
|
1549
|
+
else:
|
|
1550
|
+
if not os.path.exists(home_ui_path):
|
|
1551
|
+
ui_json = text_from_resource("ui.json")
|
|
1552
|
+
with open(home_ui_path, "w") as f:
|
|
1553
|
+
f.write(ui_json)
|
|
1389
1554
|
_log(f"Created default ui config at {home_ui_path}")
|
|
1390
|
-
|
|
1391
|
-
|
|
1555
|
+
g_ui_path = home_ui_path
|
|
1556
|
+
else:
|
|
1557
|
+
# ensure llms.json and ui.json exist in home directory
|
|
1558
|
+
asyncio.run(save_home_configs())
|
|
1559
|
+
g_config_path = home_config_path
|
|
1392
1560
|
g_ui_path = home_ui_path
|
|
1393
|
-
|
|
1394
|
-
print("ui.json not found. Create one with --init or use --config <path>")
|
|
1395
|
-
exit(1)
|
|
1561
|
+
g_config = json.loads(text_from_file(g_config_path))
|
|
1396
1562
|
|
|
1397
|
-
|
|
1398
|
-
with open(g_config_path, "r") as f:
|
|
1399
|
-
config_json = f.read()
|
|
1400
|
-
init_llms(json.loads(config_json))
|
|
1401
|
-
asyncio.run(load_llms())
|
|
1563
|
+
asyncio.run(reload_providers())
|
|
1402
1564
|
|
|
1403
1565
|
# print names
|
|
1404
1566
|
_log(f"enabled providers: {', '.join(g_handlers.keys())}")
|
|
@@ -1433,15 +1595,85 @@ def main():
|
|
|
1433
1595
|
exit(0)
|
|
1434
1596
|
|
|
1435
1597
|
if cli_args.serve is not None:
|
|
1598
|
+
# Disable inactive providers and save to config before starting server
|
|
1599
|
+
all_providers = g_config['providers'].keys()
|
|
1600
|
+
enabled_providers = list(g_handlers.keys())
|
|
1601
|
+
disable_providers = []
|
|
1602
|
+
for provider in all_providers:
|
|
1603
|
+
provider_config = g_config['providers'][provider]
|
|
1604
|
+
if provider not in enabled_providers:
|
|
1605
|
+
if 'enabled' in provider_config and provider_config['enabled']:
|
|
1606
|
+
provider_config['enabled'] = False
|
|
1607
|
+
disable_providers.append(provider)
|
|
1608
|
+
|
|
1609
|
+
if len(disable_providers) > 0:
|
|
1610
|
+
_log(f"Disabled unavailable providers: {', '.join(disable_providers)}")
|
|
1611
|
+
save_config(g_config)
|
|
1612
|
+
|
|
1613
|
+
# Start server
|
|
1436
1614
|
port = int(cli_args.serve)
|
|
1437
1615
|
|
|
1438
1616
|
if not os.path.exists(g_ui_path):
|
|
1439
1617
|
print(f"UI not found at {g_ui_path}")
|
|
1440
1618
|
exit(1)
|
|
1441
1619
|
|
|
1442
|
-
|
|
1620
|
+
# Validate auth configuration if enabled
|
|
1621
|
+
auth_enabled = g_config.get('auth', {}).get('enabled', False)
|
|
1622
|
+
if auth_enabled:
|
|
1623
|
+
github_config = g_config.get('auth', {}).get('github', {})
|
|
1624
|
+
client_id = github_config.get('client_id', '')
|
|
1625
|
+
client_secret = github_config.get('client_secret', '')
|
|
1626
|
+
|
|
1627
|
+
# Expand environment variables
|
|
1628
|
+
if client_id.startswith('$'):
|
|
1629
|
+
client_id = os.environ.get(client_id[1:], '')
|
|
1630
|
+
if client_secret.startswith('$'):
|
|
1631
|
+
client_secret = os.environ.get(client_secret[1:], '')
|
|
1632
|
+
|
|
1633
|
+
if not client_id or not client_secret:
|
|
1634
|
+
print("ERROR: Authentication is enabled but GitHub OAuth is not properly configured.")
|
|
1635
|
+
print("Please set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET environment variables,")
|
|
1636
|
+
print("or disable authentication by setting 'auth.enabled' to false in llms.json")
|
|
1637
|
+
exit(1)
|
|
1638
|
+
|
|
1639
|
+
_log("Authentication enabled - GitHub OAuth configured")
|
|
1640
|
+
|
|
1641
|
+
client_max_size = g_config.get('limits', {}).get('client_max_size', 20*1024*1024) # 20MB max request size (to handle base64 encoding overhead)
|
|
1642
|
+
_log(f"client_max_size set to {client_max_size} bytes ({client_max_size/1024/1024:.1f}MB)")
|
|
1643
|
+
app = web.Application(client_max_size=client_max_size)
|
|
1644
|
+
|
|
1645
|
+
# Authentication middleware helper
|
|
1646
|
+
def check_auth(request):
|
|
1647
|
+
"""Check if request is authenticated. Returns (is_authenticated, user_data)"""
|
|
1648
|
+
if not auth_enabled:
|
|
1649
|
+
return True, None
|
|
1650
|
+
|
|
1651
|
+
# Check for OAuth session token
|
|
1652
|
+
session_token = request.query.get('session') or request.headers.get('X-Session-Token')
|
|
1653
|
+
if session_token and session_token in g_sessions:
|
|
1654
|
+
return True, g_sessions[session_token]
|
|
1655
|
+
|
|
1656
|
+
# Check for API key
|
|
1657
|
+
auth_header = request.headers.get('Authorization', '')
|
|
1658
|
+
if auth_header.startswith('Bearer '):
|
|
1659
|
+
api_key = auth_header[7:]
|
|
1660
|
+
if api_key:
|
|
1661
|
+
return True, {"authProvider": "apikey"}
|
|
1662
|
+
|
|
1663
|
+
return False, None
|
|
1443
1664
|
|
|
1444
1665
|
async def chat_handler(request):
|
|
1666
|
+
# Check authentication if enabled
|
|
1667
|
+
is_authenticated, user_data = check_auth(request)
|
|
1668
|
+
if not is_authenticated:
|
|
1669
|
+
return web.json_response({
|
|
1670
|
+
"error": {
|
|
1671
|
+
"message": "Authentication required",
|
|
1672
|
+
"type": "authentication_error",
|
|
1673
|
+
"code": "unauthorized"
|
|
1674
|
+
}
|
|
1675
|
+
}, status=401)
|
|
1676
|
+
|
|
1445
1677
|
try:
|
|
1446
1678
|
chat = await request.json()
|
|
1447
1679
|
response = await chat_completion(chat)
|
|
@@ -1487,6 +1719,226 @@ def main():
|
|
|
1487
1719
|
})
|
|
1488
1720
|
app.router.add_post('/providers/{provider}', provider_handler)
|
|
1489
1721
|
|
|
1722
|
+
# OAuth handlers
|
|
1723
|
+
async def github_auth_handler(request):
|
|
1724
|
+
"""Initiate GitHub OAuth flow"""
|
|
1725
|
+
if 'auth' not in g_config or 'github' not in g_config['auth']:
|
|
1726
|
+
return web.json_response({"error": "GitHub OAuth not configured"}, status=500)
|
|
1727
|
+
|
|
1728
|
+
auth_config = g_config['auth']['github']
|
|
1729
|
+
client_id = auth_config.get('client_id', '')
|
|
1730
|
+
redirect_uri = auth_config.get('redirect_uri', '')
|
|
1731
|
+
|
|
1732
|
+
# Expand environment variables
|
|
1733
|
+
if client_id.startswith('$'):
|
|
1734
|
+
client_id = os.environ.get(client_id[1:], '')
|
|
1735
|
+
if redirect_uri.startswith('$'):
|
|
1736
|
+
redirect_uri = os.environ.get(redirect_uri[1:], '')
|
|
1737
|
+
|
|
1738
|
+
if not client_id:
|
|
1739
|
+
return web.json_response({"error": "GitHub client_id not configured"}, status=500)
|
|
1740
|
+
|
|
1741
|
+
# Generate CSRF state token
|
|
1742
|
+
state = secrets.token_urlsafe(32)
|
|
1743
|
+
g_oauth_states[state] = {
|
|
1744
|
+
'created': time.time(),
|
|
1745
|
+
'redirect_uri': redirect_uri
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
# Clean up old states (older than 10 minutes)
|
|
1749
|
+
current_time = time.time()
|
|
1750
|
+
expired_states = [s for s, data in g_oauth_states.items() if current_time - data['created'] > 600]
|
|
1751
|
+
for s in expired_states:
|
|
1752
|
+
del g_oauth_states[s]
|
|
1753
|
+
|
|
1754
|
+
# Build GitHub authorization URL
|
|
1755
|
+
params = {
|
|
1756
|
+
'client_id': client_id,
|
|
1757
|
+
'redirect_uri': redirect_uri,
|
|
1758
|
+
'state': state,
|
|
1759
|
+
'scope': 'read:user user:email'
|
|
1760
|
+
}
|
|
1761
|
+
auth_url = f"https://github.com/login/oauth/authorize?{urlencode(params)}"
|
|
1762
|
+
|
|
1763
|
+
return web.HTTPFound(auth_url)
|
|
1764
|
+
|
|
1765
|
+
def validate_user(github_username):
|
|
1766
|
+
auth_config = g_config['auth']['github']
|
|
1767
|
+
# Check if user is restricted
|
|
1768
|
+
restrict_to = auth_config.get('restrict_to', '')
|
|
1769
|
+
|
|
1770
|
+
# Expand environment variables
|
|
1771
|
+
if restrict_to.startswith('$'):
|
|
1772
|
+
restrict_to = os.environ.get(restrict_to[1:], '')
|
|
1773
|
+
|
|
1774
|
+
# If restrict_to is configured, validate the user
|
|
1775
|
+
if restrict_to:
|
|
1776
|
+
# Parse allowed users (comma or space delimited)
|
|
1777
|
+
allowed_users = [u.strip() for u in re.split(r'[,\s]+', restrict_to) if u.strip()]
|
|
1778
|
+
|
|
1779
|
+
# Check if user is in the allowed list
|
|
1780
|
+
if not github_username or github_username not in allowed_users:
|
|
1781
|
+
_log(f"Access denied for user: {github_username}. Not in allowed list: {allowed_users}")
|
|
1782
|
+
return web.Response(
|
|
1783
|
+
text=f"Access denied. User '{github_username}' is not authorized to access this application.",
|
|
1784
|
+
status=403
|
|
1785
|
+
)
|
|
1786
|
+
return None
|
|
1787
|
+
|
|
1788
|
+
async def github_callback_handler(request):
|
|
1789
|
+
"""Handle GitHub OAuth callback"""
|
|
1790
|
+
code = request.query.get('code')
|
|
1791
|
+
state = request.query.get('state')
|
|
1792
|
+
|
|
1793
|
+
if not code or not state:
|
|
1794
|
+
return web.Response(text="Missing code or state parameter", status=400)
|
|
1795
|
+
|
|
1796
|
+
# Verify state token (CSRF protection)
|
|
1797
|
+
if state not in g_oauth_states:
|
|
1798
|
+
return web.Response(text="Invalid state parameter", status=400)
|
|
1799
|
+
|
|
1800
|
+
state_data = g_oauth_states.pop(state)
|
|
1801
|
+
|
|
1802
|
+
if 'auth' not in g_config or 'github' not in g_config['auth']:
|
|
1803
|
+
return web.json_response({"error": "GitHub OAuth not configured"}, status=500)
|
|
1804
|
+
|
|
1805
|
+
auth_config = g_config['auth']['github']
|
|
1806
|
+
client_id = auth_config.get('client_id', '')
|
|
1807
|
+
client_secret = auth_config.get('client_secret', '')
|
|
1808
|
+
redirect_uri = auth_config.get('redirect_uri', '')
|
|
1809
|
+
|
|
1810
|
+
# Expand environment variables
|
|
1811
|
+
if client_id.startswith('$'):
|
|
1812
|
+
client_id = os.environ.get(client_id[1:], '')
|
|
1813
|
+
if client_secret.startswith('$'):
|
|
1814
|
+
client_secret = os.environ.get(client_secret[1:], '')
|
|
1815
|
+
if redirect_uri.startswith('$'):
|
|
1816
|
+
redirect_uri = os.environ.get(redirect_uri[1:], '')
|
|
1817
|
+
|
|
1818
|
+
if not client_id or not client_secret:
|
|
1819
|
+
return web.json_response({"error": "GitHub OAuth credentials not configured"}, status=500)
|
|
1820
|
+
|
|
1821
|
+
# Exchange code for access token
|
|
1822
|
+
async with aiohttp.ClientSession() as session:
|
|
1823
|
+
token_url = "https://github.com/login/oauth/access_token"
|
|
1824
|
+
token_data = {
|
|
1825
|
+
'client_id': client_id,
|
|
1826
|
+
'client_secret': client_secret,
|
|
1827
|
+
'code': code,
|
|
1828
|
+
'redirect_uri': redirect_uri
|
|
1829
|
+
}
|
|
1830
|
+
headers = {'Accept': 'application/json'}
|
|
1831
|
+
|
|
1832
|
+
async with session.post(token_url, data=token_data, headers=headers) as resp:
|
|
1833
|
+
token_response = await resp.json()
|
|
1834
|
+
access_token = token_response.get('access_token')
|
|
1835
|
+
|
|
1836
|
+
if not access_token:
|
|
1837
|
+
error = token_response.get('error_description', 'Failed to get access token')
|
|
1838
|
+
return web.Response(text=f"OAuth error: {error}", status=400)
|
|
1839
|
+
|
|
1840
|
+
# Fetch user info
|
|
1841
|
+
user_url = "https://api.github.com/user"
|
|
1842
|
+
headers = {
|
|
1843
|
+
"Authorization": f"Bearer {access_token}",
|
|
1844
|
+
"Accept": "application/json"
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
async with session.get(user_url, headers=headers) as resp:
|
|
1848
|
+
user_data = await resp.json()
|
|
1849
|
+
|
|
1850
|
+
# Validate user
|
|
1851
|
+
error_response = validate_user(user_data.get('login', ''))
|
|
1852
|
+
if error_response:
|
|
1853
|
+
return error_response
|
|
1854
|
+
|
|
1855
|
+
# Create session
|
|
1856
|
+
session_token = secrets.token_urlsafe(32)
|
|
1857
|
+
g_sessions[session_token] = {
|
|
1858
|
+
"userId": str(user_data.get('id', '')),
|
|
1859
|
+
"userName": user_data.get('login', ''),
|
|
1860
|
+
"displayName": user_data.get('name', ''),
|
|
1861
|
+
"profileUrl": user_data.get('avatar_url', ''),
|
|
1862
|
+
"email": user_data.get('email', ''),
|
|
1863
|
+
"created": time.time()
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
# Redirect to UI with session token
|
|
1867
|
+
return web.HTTPFound(f"/?session={session_token}")
|
|
1868
|
+
|
|
1869
|
+
async def session_handler(request):
|
|
1870
|
+
"""Validate and return session info"""
|
|
1871
|
+
session_token = request.query.get('session') or request.headers.get('X-Session-Token')
|
|
1872
|
+
|
|
1873
|
+
if not session_token or session_token not in g_sessions:
|
|
1874
|
+
return web.json_response({"error": "Invalid or expired session"}, status=401)
|
|
1875
|
+
|
|
1876
|
+
session_data = g_sessions[session_token]
|
|
1877
|
+
|
|
1878
|
+
# Clean up old sessions (older than 24 hours)
|
|
1879
|
+
current_time = time.time()
|
|
1880
|
+
expired_sessions = [token for token, data in g_sessions.items() if current_time - data['created'] > 86400]
|
|
1881
|
+
for token in expired_sessions:
|
|
1882
|
+
del g_sessions[token]
|
|
1883
|
+
|
|
1884
|
+
return web.json_response({
|
|
1885
|
+
**session_data,
|
|
1886
|
+
"sessionToken": session_token
|
|
1887
|
+
})
|
|
1888
|
+
|
|
1889
|
+
async def logout_handler(request):
|
|
1890
|
+
"""End OAuth session"""
|
|
1891
|
+
session_token = request.query.get('session') or request.headers.get('X-Session-Token')
|
|
1892
|
+
|
|
1893
|
+
if session_token and session_token in g_sessions:
|
|
1894
|
+
del g_sessions[session_token]
|
|
1895
|
+
|
|
1896
|
+
return web.json_response({"success": True})
|
|
1897
|
+
|
|
1898
|
+
async def auth_handler(request):
|
|
1899
|
+
"""Check authentication status and return user info"""
|
|
1900
|
+
# Check for OAuth session token
|
|
1901
|
+
session_token = request.query.get('session') or request.headers.get('X-Session-Token')
|
|
1902
|
+
|
|
1903
|
+
if session_token and session_token in g_sessions:
|
|
1904
|
+
session_data = g_sessions[session_token]
|
|
1905
|
+
return web.json_response({
|
|
1906
|
+
"userId": session_data.get("userId", ""),
|
|
1907
|
+
"userName": session_data.get("userName", ""),
|
|
1908
|
+
"displayName": session_data.get("displayName", ""),
|
|
1909
|
+
"profileUrl": session_data.get("profileUrl", ""),
|
|
1910
|
+
"authProvider": "github"
|
|
1911
|
+
})
|
|
1912
|
+
|
|
1913
|
+
# Check for API key in Authorization header
|
|
1914
|
+
# auth_header = request.headers.get('Authorization', '')
|
|
1915
|
+
# if auth_header.startswith('Bearer '):
|
|
1916
|
+
# # For API key auth, return a basic response
|
|
1917
|
+
# # You can customize this based on your API key validation logic
|
|
1918
|
+
# api_key = auth_header[7:]
|
|
1919
|
+
# if api_key: # Add your API key validation logic here
|
|
1920
|
+
# return web.json_response({
|
|
1921
|
+
# "userId": "1",
|
|
1922
|
+
# "userName": "apiuser",
|
|
1923
|
+
# "displayName": "API User",
|
|
1924
|
+
# "profileUrl": "",
|
|
1925
|
+
# "authProvider": "apikey"
|
|
1926
|
+
# })
|
|
1927
|
+
|
|
1928
|
+
# Not authenticated - return error in expected format
|
|
1929
|
+
return web.json_response({
|
|
1930
|
+
"responseStatus": {
|
|
1931
|
+
"errorCode": "Unauthorized",
|
|
1932
|
+
"message": "Not authenticated"
|
|
1933
|
+
}
|
|
1934
|
+
}, status=401)
|
|
1935
|
+
|
|
1936
|
+
app.router.add_get('/auth', auth_handler)
|
|
1937
|
+
app.router.add_get('/auth/github', github_auth_handler)
|
|
1938
|
+
app.router.add_get('/auth/github/callback', github_callback_handler)
|
|
1939
|
+
app.router.add_get('/auth/session', session_handler)
|
|
1940
|
+
app.router.add_post('/auth/logout', logout_handler)
|
|
1941
|
+
|
|
1490
1942
|
async def ui_static(request: web.Request) -> web.Response:
|
|
1491
1943
|
path = Path(request.match_info["path"])
|
|
1492
1944
|
|
|
@@ -1526,9 +1978,12 @@ def main():
|
|
|
1526
1978
|
enabled, disabled = provider_status()
|
|
1527
1979
|
ui['status'] = {
|
|
1528
1980
|
"all": list(g_config['providers'].keys()),
|
|
1529
|
-
"enabled": enabled,
|
|
1530
|
-
"disabled": disabled
|
|
1981
|
+
"enabled": enabled,
|
|
1982
|
+
"disabled": disabled
|
|
1531
1983
|
}
|
|
1984
|
+
# Add auth configuration
|
|
1985
|
+
ui['requiresAuth'] = auth_enabled
|
|
1986
|
+
ui['authType'] = 'oauth' if auth_enabled else 'apikey'
|
|
1532
1987
|
return web.json_response(ui)
|
|
1533
1988
|
app.router.add_get('/config', ui_config_handler)
|
|
1534
1989
|
|
|
@@ -1547,6 +2002,14 @@ def main():
|
|
|
1547
2002
|
# Serve index.html as fallback route (SPA routing)
|
|
1548
2003
|
app.router.add_route('*', '/{tail:.*}', index_handler)
|
|
1549
2004
|
|
|
2005
|
+
# Setup file watcher for config files
|
|
2006
|
+
async def start_background_tasks(app):
|
|
2007
|
+
"""Start background tasks when the app starts"""
|
|
2008
|
+
# Start watching config files in the background
|
|
2009
|
+
asyncio.create_task(watch_config_files(g_config_path, g_ui_path))
|
|
2010
|
+
|
|
2011
|
+
app.on_startup.append(start_background_tasks)
|
|
2012
|
+
|
|
1550
2013
|
print(f"Starting server on port {port}...")
|
|
1551
2014
|
web.run_app(app, host='0.0.0.0', port=port, print=_log)
|
|
1552
2015
|
exit(0)
|
|
@@ -1617,11 +2080,6 @@ def main():
|
|
|
1617
2080
|
print(f"\nDefault model set to: {default_model}")
|
|
1618
2081
|
exit(0)
|
|
1619
2082
|
|
|
1620
|
-
if cli_args.update:
|
|
1621
|
-
asyncio.run(update_llms())
|
|
1622
|
-
print(f"{__file__} updated")
|
|
1623
|
-
exit(0)
|
|
1624
|
-
|
|
1625
2083
|
if cli_args.chat is not None or cli_args.image is not None or cli_args.audio is not None or cli_args.file is not None or len(extra_args) > 0:
|
|
1626
2084
|
try:
|
|
1627
2085
|
chat = g_config['defaults']['text']
|