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 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": false,
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
- from urllib.parse import parse_qs
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.24"
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 is not None and api_key is not None and len(models) > 0
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 is not None and (len(models) > 0 or all_models)
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 is not None and len(models) > 0
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 save_text(url, save_path):
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 save_text(github_url("llms.json"), config_path)
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.config is not None:
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(save_text(github_url("ui.json"), home_ui_path))
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 not g_config_path or not os.path.exists(g_config_path):
1332
- # copy llms.json and ui.json to llms_home
1333
-
1334
- if not os.path.exists(home_config_path):
1335
- llms_home = os.path.dirname(home_config_path)
1336
- os.makedirs(llms_home, exist_ok=True)
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
- with open(home_ui_path, "w") as f:
1381
- f.write(ui_json)
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
- # Update g_config_path to point to the copied file
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
- if not g_ui_path or not os.path.exists(g_ui_path):
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
- # read contents
1391
- with open(g_config_path, "r") as f:
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