llms-py 2.0.24__py3-none-any.whl → 2.0.26__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/llms.json +10 -1
- llms/main.py +361 -87
- llms/ui/App.mjs +7 -2
- llms/ui/Avatar.mjs +61 -4
- llms/ui/Main.mjs +8 -5
- llms/ui/OAuthSignIn.mjs +92 -0
- llms/ui/ai.mjs +68 -5
- llms/ui/app.css +36 -0
- {llms_py-2.0.24.dist-info → llms_py-2.0.26.dist-info}/METADATA +343 -42
- {llms_py-2.0.24.dist-info → llms_py-2.0.26.dist-info}/RECORD +14 -13
- {llms_py-2.0.24.dist-info → llms_py-2.0.26.dist-info}/licenses/LICENSE +1 -2
- {llms_py-2.0.24.dist-info → llms_py-2.0.26.dist-info}/WHEEL +0 -0
- {llms_py-2.0.24.dist-info → llms_py-2.0.26.dist-info}/entry_points.txt +0 -0
- {llms_py-2.0.24.dist-info → llms_py-2.0.26.dist-info}/top_level.txt +0 -0
llms/llms.json
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
+
"auth": {
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"github": {
|
|
5
|
+
"client_id": "$GITHUB_CLIENT_ID",
|
|
6
|
+
"client_secret": "$GITHUB_CLIENT_SECRET",
|
|
7
|
+
"redirect_uri": "http://localhost:8000/auth/github/callback"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
2
10
|
"defaults": {
|
|
3
11
|
"headers": {
|
|
4
12
|
"Content-Type": "application/json",
|
|
@@ -113,6 +121,7 @@
|
|
|
113
121
|
"mai-ds-r1": "microsoft/mai-ds-r1:free",
|
|
114
122
|
"llama3.3:70b": "meta-llama/llama-3.3-70b-instruct:free",
|
|
115
123
|
"nemotron-nano:9b": "nvidia/nemotron-nano-9b-v2:free",
|
|
124
|
+
"nemotron-nano:12b":"nvidia/nemotron-nano-12b-v2-vl:free",
|
|
116
125
|
"deepseek-r1-distill-llama:70b": "deepseek/deepseek-r1-distill-llama-70b:free",
|
|
117
126
|
"gpt-oss:20b": "openai/gpt-oss-20b:free",
|
|
118
127
|
"mistral-small3.2:24b": "mistralai/mistral-small-3.2-24b-instruct:free",
|
|
@@ -163,7 +172,7 @@
|
|
|
163
172
|
}
|
|
164
173
|
},
|
|
165
174
|
"google_free": {
|
|
166
|
-
"enabled":
|
|
175
|
+
"enabled": true,
|
|
167
176
|
"type": "GoogleProvider",
|
|
168
177
|
"api_key": "$GOOGLE_FREE_API_KEY",
|
|
169
178
|
"models": {
|
llms/main.py
CHANGED
|
@@ -14,7 +14,8 @@ import mimetypes
|
|
|
14
14
|
import traceback
|
|
15
15
|
import sys
|
|
16
16
|
import site
|
|
17
|
-
|
|
17
|
+
import secrets
|
|
18
|
+
from urllib.parse import parse_qs, urlencode
|
|
18
19
|
|
|
19
20
|
import aiohttp
|
|
20
21
|
from aiohttp import web
|
|
@@ -22,7 +23,7 @@ from aiohttp import web
|
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
from importlib import resources # Py≥3.9 (pip install importlib_resources for 3.7/3.8)
|
|
24
25
|
|
|
25
|
-
VERSION = "2.0.
|
|
26
|
+
VERSION = "2.0.26"
|
|
26
27
|
_ROOT = None
|
|
27
28
|
g_config_path = None
|
|
28
29
|
g_ui_path = None
|
|
@@ -31,6 +32,8 @@ g_handlers = {}
|
|
|
31
32
|
g_verbose = False
|
|
32
33
|
g_logprefix=""
|
|
33
34
|
g_default_model=""
|
|
35
|
+
g_sessions = {} # OAuth session storage: {session_token: {userId, userName, displayName, profileUrl, email, created}}
|
|
36
|
+
g_oauth_states = {} # CSRF protection: {state: {created, redirect_uri}}
|
|
34
37
|
|
|
35
38
|
def _log(message):
|
|
36
39
|
"""Helper method for logging from the global polling task."""
|
|
@@ -354,7 +357,7 @@ class OpenAiProvider:
|
|
|
354
357
|
|
|
355
358
|
@classmethod
|
|
356
359
|
def test(cls, base_url=None, api_key=None, models={}, **kwargs):
|
|
357
|
-
return base_url
|
|
360
|
+
return base_url and api_key and len(models) > 0
|
|
358
361
|
|
|
359
362
|
async def load(self):
|
|
360
363
|
pass
|
|
@@ -467,7 +470,7 @@ class OllamaProvider(OpenAiProvider):
|
|
|
467
470
|
|
|
468
471
|
@classmethod
|
|
469
472
|
def test(cls, base_url=None, models={}, all_models=False, **kwargs):
|
|
470
|
-
return base_url
|
|
473
|
+
return base_url and (len(models) > 0 or all_models)
|
|
471
474
|
|
|
472
475
|
class GoogleOpenAiProvider(OpenAiProvider):
|
|
473
476
|
def __init__(self, api_key, models, **kwargs):
|
|
@@ -476,7 +479,7 @@ class GoogleOpenAiProvider(OpenAiProvider):
|
|
|
476
479
|
|
|
477
480
|
@classmethod
|
|
478
481
|
def test(cls, api_key=None, models={}, **kwargs):
|
|
479
|
-
return api_key
|
|
482
|
+
return api_key and len(models) > 0
|
|
480
483
|
|
|
481
484
|
class GoogleProvider(OpenAiProvider):
|
|
482
485
|
def __init__(self, models, api_key, safety_settings=None, thinking_config=None, curl=False, **kwargs):
|
|
@@ -912,7 +915,7 @@ async def load_llms():
|
|
|
912
915
|
await provider.load()
|
|
913
916
|
|
|
914
917
|
def save_config(config):
|
|
915
|
-
global g_config
|
|
918
|
+
global g_config, g_config_path
|
|
916
919
|
g_config = config
|
|
917
920
|
with open(g_config_path, "w") as f:
|
|
918
921
|
json.dump(g_config, f, indent=4)
|
|
@@ -921,21 +924,25 @@ def save_config(config):
|
|
|
921
924
|
def github_url(filename):
|
|
922
925
|
return f"https://raw.githubusercontent.com/ServiceStack/llms/refs/heads/main/llms/{filename}"
|
|
923
926
|
|
|
924
|
-
async def
|
|
927
|
+
async def get_text(url):
|
|
925
928
|
async with aiohttp.ClientSession() as session:
|
|
926
929
|
_log(f"GET {url}")
|
|
927
930
|
async with session.get(url) as resp:
|
|
928
931
|
text = await resp.text()
|
|
929
932
|
if resp.status >= 400:
|
|
930
933
|
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
934
|
return text
|
|
935
935
|
|
|
936
|
+
async def save_text_url(url, save_path):
|
|
937
|
+
text = await get_text(url)
|
|
938
|
+
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
|
939
|
+
with open(save_path, "w") as f:
|
|
940
|
+
f.write(text)
|
|
941
|
+
return text
|
|
942
|
+
|
|
936
943
|
async def save_default_config(config_path):
|
|
937
944
|
global g_config
|
|
938
|
-
config_json = await
|
|
945
|
+
config_json = await save_text_url(github_url("llms.json"), config_path)
|
|
939
946
|
g_config = json.loads(config_json)
|
|
940
947
|
|
|
941
948
|
def provider_status():
|
|
@@ -1256,8 +1263,59 @@ async def check_models(provider_name, model_names=None):
|
|
|
1256
1263
|
|
|
1257
1264
|
print()
|
|
1258
1265
|
|
|
1266
|
+
def text_from_resource(filename):
|
|
1267
|
+
global _ROOT
|
|
1268
|
+
resource_path = _ROOT / filename
|
|
1269
|
+
if resource_exists(resource_path):
|
|
1270
|
+
try:
|
|
1271
|
+
return read_resource_text(resource_path)
|
|
1272
|
+
except (OSError, AttributeError) as e:
|
|
1273
|
+
_log(f"Error reading resource config {filename}: {e}")
|
|
1274
|
+
return None
|
|
1275
|
+
|
|
1276
|
+
def text_from_file(filename):
|
|
1277
|
+
if os.path.exists(filename):
|
|
1278
|
+
with open(filename, "r") as f:
|
|
1279
|
+
return f.read()
|
|
1280
|
+
return None
|
|
1281
|
+
|
|
1282
|
+
async def text_from_resource_or_url(filename):
|
|
1283
|
+
text = text_from_resource(filename)
|
|
1284
|
+
if not text:
|
|
1285
|
+
try:
|
|
1286
|
+
resource_url = github_url(filename)
|
|
1287
|
+
text = await get_text(resource_url)
|
|
1288
|
+
except Exception as e:
|
|
1289
|
+
_log(f"Error downloading JSON from {resource_url}: {e}")
|
|
1290
|
+
raise e
|
|
1291
|
+
return text
|
|
1292
|
+
|
|
1293
|
+
async def save_home_configs():
|
|
1294
|
+
home_config_path = home_llms_path("llms.json")
|
|
1295
|
+
home_ui_path = home_llms_path("ui.json")
|
|
1296
|
+
if os.path.exists(home_config_path) and os.path.exists(home_ui_path):
|
|
1297
|
+
return
|
|
1298
|
+
|
|
1299
|
+
llms_home = os.path.dirname(home_config_path)
|
|
1300
|
+
os.makedirs(llms_home, exist_ok=True)
|
|
1301
|
+
try:
|
|
1302
|
+
if not os.path.exists(home_config_path):
|
|
1303
|
+
config_json = await text_from_resource_or_url("llms.json")
|
|
1304
|
+
with open(home_config_path, "w") as f:
|
|
1305
|
+
f.write(config_json)
|
|
1306
|
+
_log(f"Created default config at {home_config_path}")
|
|
1307
|
+
|
|
1308
|
+
if not os.path.exists(home_ui_path):
|
|
1309
|
+
ui_json = await text_from_resource_or_url("ui.json")
|
|
1310
|
+
with open(home_ui_path, "w") as f:
|
|
1311
|
+
f.write(ui_json)
|
|
1312
|
+
_log(f"Created default ui config at {home_ui_path}")
|
|
1313
|
+
except Exception as e:
|
|
1314
|
+
print("Could not create llms.json. Create one with --init or use --config <path>")
|
|
1315
|
+
exit(1)
|
|
1316
|
+
|
|
1259
1317
|
def main():
|
|
1260
|
-
global _ROOT, g_verbose, g_default_model, g_logprefix, g_config_path, g_ui_path
|
|
1318
|
+
global _ROOT, g_verbose, g_default_model, g_logprefix, g_config, g_config_path, g_ui_path
|
|
1261
1319
|
|
|
1262
1320
|
parser = argparse.ArgumentParser(description=f"llms v{VERSION}")
|
|
1263
1321
|
parser.add_argument('--config', default=None, help='Path to config file', metavar='FILE')
|
|
@@ -1295,24 +1353,13 @@ def main():
|
|
|
1295
1353
|
if cli_args.logprefix:
|
|
1296
1354
|
g_logprefix = cli_args.logprefix
|
|
1297
1355
|
|
|
1298
|
-
if cli_args.
|
|
1299
|
-
g_config_path = os.path.join(os.path.dirname(__file__), cli_args.config)
|
|
1300
|
-
|
|
1301
|
-
_ROOT = resolve_root()
|
|
1302
|
-
if cli_args.root:
|
|
1303
|
-
_ROOT = Path(cli_args.root)
|
|
1304
|
-
|
|
1356
|
+
_ROOT = Path(cli_args.root) if cli_args.root else resolve_root()
|
|
1305
1357
|
if not _ROOT:
|
|
1306
1358
|
print("Resource root not found")
|
|
1307
1359
|
exit(1)
|
|
1308
1360
|
|
|
1309
|
-
g_config_path = os.path.join(os.path.dirname(__file__), cli_args.config) if cli_args.config else get_config_path()
|
|
1310
|
-
g_ui_path = get_ui_path()
|
|
1311
|
-
|
|
1312
1361
|
home_config_path = home_llms_path("llms.json")
|
|
1313
|
-
resource_config_path = _ROOT / "llms.json"
|
|
1314
1362
|
home_ui_path = home_llms_path("ui.json")
|
|
1315
|
-
resource_ui_path = _ROOT / "ui.json"
|
|
1316
1363
|
|
|
1317
1364
|
if cli_args.init:
|
|
1318
1365
|
if os.path.exists(home_config_path):
|
|
@@ -1324,74 +1371,38 @@ def main():
|
|
|
1324
1371
|
if os.path.exists(home_ui_path):
|
|
1325
1372
|
print(f"ui.json already exists at {home_ui_path}")
|
|
1326
1373
|
else:
|
|
1327
|
-
asyncio.run(
|
|
1374
|
+
asyncio.run(save_text_url(github_url("ui.json"), home_ui_path))
|
|
1328
1375
|
print(f"Created default ui config at {home_ui_path}")
|
|
1329
1376
|
exit(0)
|
|
1330
1377
|
|
|
1331
|
-
if
|
|
1332
|
-
#
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
if resource_exists(resource_config_path):
|
|
1339
|
-
try:
|
|
1340
|
-
# Read config from resource (handle both Path and Traversable objects)
|
|
1341
|
-
config_json = read_resource_text(resource_config_path)
|
|
1342
|
-
except (OSError, AttributeError) as e:
|
|
1343
|
-
_log(f"Error reading resource config: {e}")
|
|
1344
|
-
if not config_json:
|
|
1345
|
-
try:
|
|
1346
|
-
config_json = asyncio.run(save_text(github_url("llms.json"), home_config_path))
|
|
1347
|
-
except Exception as e:
|
|
1348
|
-
_log(f"Error downloading llms.json: {e}")
|
|
1349
|
-
print("Could not create llms.json. Create one with --init or use --config <path>")
|
|
1350
|
-
exit(1)
|
|
1351
|
-
|
|
1352
|
-
with open(home_config_path, "w") as f:
|
|
1353
|
-
f.write(config_json)
|
|
1354
|
-
_log(f"Created default config at {home_config_path}")
|
|
1355
|
-
# Update g_config_path to point to the copied file
|
|
1356
|
-
g_config_path = home_config_path
|
|
1357
|
-
if not g_config_path or not os.path.exists(g_config_path):
|
|
1358
|
-
print("llms.json not found. Create one with --init or use --config <path>")
|
|
1359
|
-
exit(1)
|
|
1360
|
-
|
|
1361
|
-
if not g_ui_path or not os.path.exists(g_ui_path):
|
|
1362
|
-
# Read UI config from resource
|
|
1363
|
-
if not os.path.exists(home_ui_path):
|
|
1364
|
-
llms_home = os.path.dirname(home_ui_path)
|
|
1365
|
-
os.makedirs(llms_home, exist_ok=True)
|
|
1366
|
-
if resource_exists(resource_ui_path):
|
|
1367
|
-
try:
|
|
1368
|
-
# Read config from resource (handle both Path and Traversable objects)
|
|
1369
|
-
ui_json = read_resource_text(resource_ui_path)
|
|
1370
|
-
except (OSError, AttributeError) as e:
|
|
1371
|
-
_log(f"Error reading resource ui config: {e}")
|
|
1372
|
-
if not ui_json:
|
|
1373
|
-
try:
|
|
1374
|
-
ui_json = asyncio.run(save_text(github_url("ui.json"), home_ui_path))
|
|
1375
|
-
except Exception as e:
|
|
1376
|
-
_log(f"Error downloading ui.json: {e}")
|
|
1377
|
-
print("Could not create ui.json. Create one with --init or use --config <path>")
|
|
1378
|
-
exit(1)
|
|
1378
|
+
if cli_args.config:
|
|
1379
|
+
# read contents
|
|
1380
|
+
g_config_path = os.path.join(os.path.dirname(__file__), cli_args.config)
|
|
1381
|
+
with open(g_config_path, "r") as f:
|
|
1382
|
+
config_json = f.read()
|
|
1383
|
+
g_config = json.loads(config_json)
|
|
1379
1384
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1385
|
+
config_dir = os.path.dirname(g_config_path)
|
|
1386
|
+
# look for ui.json in same directory as config
|
|
1387
|
+
ui_path = os.path.join(config_dir, "ui.json")
|
|
1388
|
+
if os.path.exists(ui_path):
|
|
1389
|
+
g_ui_path = ui_path
|
|
1390
|
+
else:
|
|
1391
|
+
if not os.path.exists(home_ui_path):
|
|
1392
|
+
ui_json = text_from_resource("ui.json")
|
|
1393
|
+
with open(home_ui_path, "w") as f:
|
|
1394
|
+
f.write(ui_json)
|
|
1382
1395
|
_log(f"Created default ui config at {home_ui_path}")
|
|
1383
|
-
|
|
1384
|
-
|
|
1396
|
+
g_ui_path = home_ui_path
|
|
1397
|
+
else:
|
|
1398
|
+
# ensure llms.json and ui.json exist in home directory
|
|
1399
|
+
asyncio.run(save_home_configs())
|
|
1400
|
+
g_config_path = home_config_path
|
|
1385
1401
|
g_ui_path = home_ui_path
|
|
1386
|
-
|
|
1387
|
-
print("ui.json not found. Create one with --init or use --config <path>")
|
|
1388
|
-
exit(1)
|
|
1402
|
+
g_config = json.loads(text_from_file(g_config_path))
|
|
1389
1403
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
config_json = f.read()
|
|
1393
|
-
init_llms(json.loads(config_json))
|
|
1394
|
-
asyncio.run(load_llms())
|
|
1404
|
+
init_llms(g_config)
|
|
1405
|
+
asyncio.run(load_llms())
|
|
1395
1406
|
|
|
1396
1407
|
# print names
|
|
1397
1408
|
_log(f"enabled providers: {', '.join(g_handlers.keys())}")
|
|
@@ -1426,15 +1437,83 @@ def main():
|
|
|
1426
1437
|
exit(0)
|
|
1427
1438
|
|
|
1428
1439
|
if cli_args.serve is not None:
|
|
1440
|
+
# Disable inactive providers and save to config before starting server
|
|
1441
|
+
all_providers = g_config['providers'].keys()
|
|
1442
|
+
enabled_providers = list(g_handlers.keys())
|
|
1443
|
+
disable_providers = []
|
|
1444
|
+
for provider in all_providers:
|
|
1445
|
+
provider_config = g_config['providers'][provider]
|
|
1446
|
+
if provider not in enabled_providers:
|
|
1447
|
+
if 'enabled' in provider_config and provider_config['enabled']:
|
|
1448
|
+
provider_config['enabled'] = False
|
|
1449
|
+
disable_providers.append(provider)
|
|
1450
|
+
|
|
1451
|
+
if len(disable_providers) > 0:
|
|
1452
|
+
_log(f"Disabled unavailable providers: {', '.join(disable_providers)}")
|
|
1453
|
+
save_config(g_config)
|
|
1454
|
+
|
|
1455
|
+
# Start server
|
|
1429
1456
|
port = int(cli_args.serve)
|
|
1430
1457
|
|
|
1431
1458
|
if not os.path.exists(g_ui_path):
|
|
1432
1459
|
print(f"UI not found at {g_ui_path}")
|
|
1433
1460
|
exit(1)
|
|
1434
1461
|
|
|
1462
|
+
# Validate auth configuration if enabled
|
|
1463
|
+
auth_enabled = g_config.get('auth', {}).get('enabled', False)
|
|
1464
|
+
if auth_enabled:
|
|
1465
|
+
github_config = g_config.get('auth', {}).get('github', {})
|
|
1466
|
+
client_id = github_config.get('client_id', '')
|
|
1467
|
+
client_secret = github_config.get('client_secret', '')
|
|
1468
|
+
|
|
1469
|
+
# Expand environment variables
|
|
1470
|
+
if client_id.startswith('$'):
|
|
1471
|
+
client_id = os.environ.get(client_id[1:], '')
|
|
1472
|
+
if client_secret.startswith('$'):
|
|
1473
|
+
client_secret = os.environ.get(client_secret[1:], '')
|
|
1474
|
+
|
|
1475
|
+
if not client_id or not client_secret:
|
|
1476
|
+
print("ERROR: Authentication is enabled but GitHub OAuth is not properly configured.")
|
|
1477
|
+
print("Please set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET environment variables,")
|
|
1478
|
+
print("or disable authentication by setting 'auth.enabled' to false in llms.json")
|
|
1479
|
+
exit(1)
|
|
1480
|
+
|
|
1481
|
+
_log("Authentication enabled - GitHub OAuth configured")
|
|
1482
|
+
|
|
1435
1483
|
app = web.Application()
|
|
1436
1484
|
|
|
1485
|
+
# Authentication middleware helper
|
|
1486
|
+
def check_auth(request):
|
|
1487
|
+
"""Check if request is authenticated. Returns (is_authenticated, user_data)"""
|
|
1488
|
+
if not auth_enabled:
|
|
1489
|
+
return True, None
|
|
1490
|
+
|
|
1491
|
+
# Check for OAuth session token
|
|
1492
|
+
session_token = request.query.get('session') or request.headers.get('X-Session-Token')
|
|
1493
|
+
if session_token and session_token in g_sessions:
|
|
1494
|
+
return True, g_sessions[session_token]
|
|
1495
|
+
|
|
1496
|
+
# Check for API key
|
|
1497
|
+
auth_header = request.headers.get('Authorization', '')
|
|
1498
|
+
if auth_header.startswith('Bearer '):
|
|
1499
|
+
api_key = auth_header[7:]
|
|
1500
|
+
if api_key:
|
|
1501
|
+
return True, {"authProvider": "apikey"}
|
|
1502
|
+
|
|
1503
|
+
return False, None
|
|
1504
|
+
|
|
1437
1505
|
async def chat_handler(request):
|
|
1506
|
+
# Check authentication if enabled
|
|
1507
|
+
is_authenticated, user_data = check_auth(request)
|
|
1508
|
+
if not is_authenticated:
|
|
1509
|
+
return web.json_response({
|
|
1510
|
+
"error": {
|
|
1511
|
+
"message": "Authentication required",
|
|
1512
|
+
"type": "authentication_error",
|
|
1513
|
+
"code": "unauthorized"
|
|
1514
|
+
}
|
|
1515
|
+
}, status=401)
|
|
1516
|
+
|
|
1438
1517
|
try:
|
|
1439
1518
|
chat = await request.json()
|
|
1440
1519
|
response = await chat_completion(chat)
|
|
@@ -1480,6 +1559,198 @@ def main():
|
|
|
1480
1559
|
})
|
|
1481
1560
|
app.router.add_post('/providers/{provider}', provider_handler)
|
|
1482
1561
|
|
|
1562
|
+
# OAuth handlers
|
|
1563
|
+
async def github_auth_handler(request):
|
|
1564
|
+
"""Initiate GitHub OAuth flow"""
|
|
1565
|
+
if 'auth' not in g_config or 'github' not in g_config['auth']:
|
|
1566
|
+
return web.json_response({"error": "GitHub OAuth not configured"}, status=500)
|
|
1567
|
+
|
|
1568
|
+
auth_config = g_config['auth']['github']
|
|
1569
|
+
client_id = auth_config.get('client_id', '')
|
|
1570
|
+
redirect_uri = auth_config.get('redirect_uri', '')
|
|
1571
|
+
|
|
1572
|
+
# Expand environment variables
|
|
1573
|
+
if client_id.startswith('$'):
|
|
1574
|
+
client_id = os.environ.get(client_id[1:], '')
|
|
1575
|
+
if redirect_uri.startswith('$'):
|
|
1576
|
+
redirect_uri = os.environ.get(redirect_uri[1:], '')
|
|
1577
|
+
|
|
1578
|
+
if not client_id:
|
|
1579
|
+
return web.json_response({"error": "GitHub client_id not configured"}, status=500)
|
|
1580
|
+
|
|
1581
|
+
# Generate CSRF state token
|
|
1582
|
+
state = secrets.token_urlsafe(32)
|
|
1583
|
+
g_oauth_states[state] = {
|
|
1584
|
+
'created': time.time(),
|
|
1585
|
+
'redirect_uri': redirect_uri
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
# Clean up old states (older than 10 minutes)
|
|
1589
|
+
current_time = time.time()
|
|
1590
|
+
expired_states = [s for s, data in g_oauth_states.items() if current_time - data['created'] > 600]
|
|
1591
|
+
for s in expired_states:
|
|
1592
|
+
del g_oauth_states[s]
|
|
1593
|
+
|
|
1594
|
+
# Build GitHub authorization URL
|
|
1595
|
+
params = {
|
|
1596
|
+
'client_id': client_id,
|
|
1597
|
+
'redirect_uri': redirect_uri,
|
|
1598
|
+
'state': state,
|
|
1599
|
+
'scope': 'read:user user:email'
|
|
1600
|
+
}
|
|
1601
|
+
auth_url = f"https://github.com/login/oauth/authorize?{urlencode(params)}"
|
|
1602
|
+
|
|
1603
|
+
return web.HTTPFound(auth_url)
|
|
1604
|
+
|
|
1605
|
+
async def github_callback_handler(request):
|
|
1606
|
+
"""Handle GitHub OAuth callback"""
|
|
1607
|
+
code = request.query.get('code')
|
|
1608
|
+
state = request.query.get('state')
|
|
1609
|
+
|
|
1610
|
+
if not code or not state:
|
|
1611
|
+
return web.Response(text="Missing code or state parameter", status=400)
|
|
1612
|
+
|
|
1613
|
+
# Verify state token (CSRF protection)
|
|
1614
|
+
if state not in g_oauth_states:
|
|
1615
|
+
return web.Response(text="Invalid state parameter", status=400)
|
|
1616
|
+
|
|
1617
|
+
state_data = g_oauth_states.pop(state)
|
|
1618
|
+
|
|
1619
|
+
if 'auth' not in g_config or 'github' not in g_config['auth']:
|
|
1620
|
+
return web.json_response({"error": "GitHub OAuth not configured"}, status=500)
|
|
1621
|
+
|
|
1622
|
+
auth_config = g_config['auth']['github']
|
|
1623
|
+
client_id = auth_config.get('client_id', '')
|
|
1624
|
+
client_secret = auth_config.get('client_secret', '')
|
|
1625
|
+
redirect_uri = auth_config.get('redirect_uri', '')
|
|
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
|
+
if redirect_uri.startswith('$'):
|
|
1633
|
+
redirect_uri = os.environ.get(redirect_uri[1:], '')
|
|
1634
|
+
|
|
1635
|
+
if not client_id or not client_secret:
|
|
1636
|
+
return web.json_response({"error": "GitHub OAuth credentials not configured"}, status=500)
|
|
1637
|
+
|
|
1638
|
+
# Exchange code for access token
|
|
1639
|
+
async with aiohttp.ClientSession() as session:
|
|
1640
|
+
token_url = "https://github.com/login/oauth/access_token"
|
|
1641
|
+
token_data = {
|
|
1642
|
+
'client_id': client_id,
|
|
1643
|
+
'client_secret': client_secret,
|
|
1644
|
+
'code': code,
|
|
1645
|
+
'redirect_uri': redirect_uri
|
|
1646
|
+
}
|
|
1647
|
+
headers = {'Accept': 'application/json'}
|
|
1648
|
+
|
|
1649
|
+
async with session.post(token_url, data=token_data, headers=headers) as resp:
|
|
1650
|
+
token_response = await resp.json()
|
|
1651
|
+
access_token = token_response.get('access_token')
|
|
1652
|
+
|
|
1653
|
+
if not access_token:
|
|
1654
|
+
error = token_response.get('error_description', 'Failed to get access token')
|
|
1655
|
+
return web.Response(text=f"OAuth error: {error}", status=400)
|
|
1656
|
+
|
|
1657
|
+
# Fetch user info
|
|
1658
|
+
user_url = "https://api.github.com/user"
|
|
1659
|
+
headers = {
|
|
1660
|
+
"Authorization": f"Bearer {access_token}",
|
|
1661
|
+
"Accept": "application/json"
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
async with session.get(user_url, headers=headers) as resp:
|
|
1665
|
+
user_data = await resp.json()
|
|
1666
|
+
|
|
1667
|
+
# Create session
|
|
1668
|
+
session_token = secrets.token_urlsafe(32)
|
|
1669
|
+
g_sessions[session_token] = {
|
|
1670
|
+
"userId": str(user_data.get('id', '')),
|
|
1671
|
+
"userName": user_data.get('login', ''),
|
|
1672
|
+
"displayName": user_data.get('name', ''),
|
|
1673
|
+
"profileUrl": user_data.get('avatar_url', ''),
|
|
1674
|
+
"email": user_data.get('email', ''),
|
|
1675
|
+
"created": time.time()
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
# Redirect to UI with session token
|
|
1679
|
+
return web.HTTPFound(f"/?session={session_token}")
|
|
1680
|
+
|
|
1681
|
+
async def session_handler(request):
|
|
1682
|
+
"""Validate and return session info"""
|
|
1683
|
+
session_token = request.query.get('session') or request.headers.get('X-Session-Token')
|
|
1684
|
+
|
|
1685
|
+
if not session_token or session_token not in g_sessions:
|
|
1686
|
+
return web.json_response({"error": "Invalid or expired session"}, status=401)
|
|
1687
|
+
|
|
1688
|
+
session_data = g_sessions[session_token]
|
|
1689
|
+
|
|
1690
|
+
# Clean up old sessions (older than 24 hours)
|
|
1691
|
+
current_time = time.time()
|
|
1692
|
+
expired_sessions = [token for token, data in g_sessions.items() if current_time - data['created'] > 86400]
|
|
1693
|
+
for token in expired_sessions:
|
|
1694
|
+
del g_sessions[token]
|
|
1695
|
+
|
|
1696
|
+
return web.json_response({
|
|
1697
|
+
**session_data,
|
|
1698
|
+
"sessionToken": session_token
|
|
1699
|
+
})
|
|
1700
|
+
|
|
1701
|
+
async def logout_handler(request):
|
|
1702
|
+
"""End OAuth session"""
|
|
1703
|
+
session_token = request.query.get('session') or request.headers.get('X-Session-Token')
|
|
1704
|
+
|
|
1705
|
+
if session_token and session_token in g_sessions:
|
|
1706
|
+
del g_sessions[session_token]
|
|
1707
|
+
|
|
1708
|
+
return web.json_response({"success": True})
|
|
1709
|
+
|
|
1710
|
+
async def auth_handler(request):
|
|
1711
|
+
"""Check authentication status and return user info"""
|
|
1712
|
+
# Check for OAuth session token
|
|
1713
|
+
session_token = request.query.get('session') or request.headers.get('X-Session-Token')
|
|
1714
|
+
|
|
1715
|
+
if session_token and session_token in g_sessions:
|
|
1716
|
+
session_data = g_sessions[session_token]
|
|
1717
|
+
return web.json_response({
|
|
1718
|
+
"userId": session_data.get("userId", ""),
|
|
1719
|
+
"userName": session_data.get("userName", ""),
|
|
1720
|
+
"displayName": session_data.get("displayName", ""),
|
|
1721
|
+
"profileUrl": session_data.get("profileUrl", ""),
|
|
1722
|
+
"authProvider": "github"
|
|
1723
|
+
})
|
|
1724
|
+
|
|
1725
|
+
# Check for API key in Authorization header
|
|
1726
|
+
# auth_header = request.headers.get('Authorization', '')
|
|
1727
|
+
# if auth_header.startswith('Bearer '):
|
|
1728
|
+
# # For API key auth, return a basic response
|
|
1729
|
+
# # You can customize this based on your API key validation logic
|
|
1730
|
+
# api_key = auth_header[7:]
|
|
1731
|
+
# if api_key: # Add your API key validation logic here
|
|
1732
|
+
# return web.json_response({
|
|
1733
|
+
# "userId": "1",
|
|
1734
|
+
# "userName": "apiuser",
|
|
1735
|
+
# "displayName": "API User",
|
|
1736
|
+
# "profileUrl": "",
|
|
1737
|
+
# "authProvider": "apikey"
|
|
1738
|
+
# })
|
|
1739
|
+
|
|
1740
|
+
# Not authenticated - return error in expected format
|
|
1741
|
+
return web.json_response({
|
|
1742
|
+
"responseStatus": {
|
|
1743
|
+
"errorCode": "Unauthorized",
|
|
1744
|
+
"message": "Not authenticated"
|
|
1745
|
+
}
|
|
1746
|
+
}, status=401)
|
|
1747
|
+
|
|
1748
|
+
app.router.add_get('/auth', auth_handler)
|
|
1749
|
+
app.router.add_get('/auth/github', github_auth_handler)
|
|
1750
|
+
app.router.add_get('/auth/github/callback', github_callback_handler)
|
|
1751
|
+
app.router.add_get('/auth/session', session_handler)
|
|
1752
|
+
app.router.add_post('/auth/logout', logout_handler)
|
|
1753
|
+
|
|
1483
1754
|
async def ui_static(request: web.Request) -> web.Response:
|
|
1484
1755
|
path = Path(request.match_info["path"])
|
|
1485
1756
|
|
|
@@ -1519,9 +1790,12 @@ def main():
|
|
|
1519
1790
|
enabled, disabled = provider_status()
|
|
1520
1791
|
ui['status'] = {
|
|
1521
1792
|
"all": list(g_config['providers'].keys()),
|
|
1522
|
-
"enabled": enabled,
|
|
1523
|
-
"disabled": disabled
|
|
1793
|
+
"enabled": enabled,
|
|
1794
|
+
"disabled": disabled
|
|
1524
1795
|
}
|
|
1796
|
+
# Add auth configuration
|
|
1797
|
+
ui['requiresAuth'] = auth_enabled
|
|
1798
|
+
ui['authType'] = 'oauth' if auth_enabled else 'apikey'
|
|
1525
1799
|
return web.json_response(ui)
|
|
1526
1800
|
app.router.add_get('/config', ui_config_handler)
|
|
1527
1801
|
|
llms/ui/App.mjs
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
import { inject } from "vue"
|
|
1
2
|
import Sidebar from "./Sidebar.mjs"
|
|
2
3
|
|
|
3
4
|
export default {
|
|
4
5
|
components: {
|
|
5
6
|
Sidebar,
|
|
6
7
|
},
|
|
8
|
+
setup() {
|
|
9
|
+
const ai = inject('ai')
|
|
10
|
+
return { ai }
|
|
11
|
+
},
|
|
7
12
|
template: `
|
|
8
13
|
<div class="flex h-screen bg-white">
|
|
9
|
-
<!-- Sidebar -->
|
|
10
|
-
<div class="w-72 xl:w-80 flex-shrink-0">
|
|
14
|
+
<!-- Sidebar (hidden when auth required and not authenticated) -->
|
|
15
|
+
<div v-if="!(ai.requiresAuth && !ai.auth)" class="w-72 xl:w-80 flex-shrink-0">
|
|
11
16
|
<Sidebar />
|
|
12
17
|
</div>
|
|
13
18
|
|