llms-py 2.0.25__py3-none-any.whl → 2.0.27__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 CHANGED
@@ -1,8 +1,8 @@
1
1
  <html>
2
2
  <head>
3
3
  <title>llms.py</title>
4
- <link rel="stylesheet" href="/ui/typography.css">
5
4
  <link rel="stylesheet" href="/ui/app.css">
5
+ <link rel="stylesheet" href="/ui/typography.css">
6
6
  <link rel="icon" type="image/svg" href="/ui/fav.svg">
7
7
  <style>
8
8
  [type='button'],button[type='submit']{cursor:pointer}
@@ -38,6 +38,22 @@
38
38
  <body>
39
39
  <div id="app"></div>
40
40
  </body>
41
+ <script>
42
+ let colorScheme = location.search === "?dark"
43
+ ? "dark"
44
+ : location.search === "?light"
45
+ ? "light"
46
+ : localStorage.getItem('color-scheme')
47
+ let darkMode = colorScheme != null
48
+ ? colorScheme === 'dark'
49
+ : window.matchMedia('(prefers-color-scheme: dark)').matches
50
+ let html = document.documentElement
51
+ html.classList.toggle('dark', darkMode)
52
+ html.style.setProperty('color-scheme', darkMode ? 'dark' : null)
53
+ if (localStorage.getItem('color-scheme') === null) {
54
+ localStorage.setItem('color-scheme', darkMode ? 'dark' : 'light')
55
+ }
56
+ </script>
41
57
  <script type="module">
42
58
  import { createApp, defineAsyncComponent } from 'vue'
43
59
  import { createWebHistory, createRouter } from "vue-router"
llms/llms.json CHANGED
@@ -1,4 +1,12 @@
1
1
  {
2
+ "auth": {
3
+ "enabled": false,
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",
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.25"
26
+ VERSION = "2.0.27"
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."""
@@ -1456,9 +1459,61 @@ def main():
1456
1459
  print(f"UI not found at {g_ui_path}")
1457
1460
  exit(1)
1458
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
+
1459
1483
  app = web.Application()
1460
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
+
1461
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
+
1462
1517
  try:
1463
1518
  chat = await request.json()
1464
1519
  response = await chat_completion(chat)
@@ -1504,6 +1559,198 @@ def main():
1504
1559
  })
1505
1560
  app.router.add_post('/providers/{provider}', provider_handler)
1506
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
+
1507
1754
  async def ui_static(request: web.Request) -> web.Response:
1508
1755
  path = Path(request.match_info["path"])
1509
1756
 
@@ -1543,9 +1790,12 @@ def main():
1543
1790
  enabled, disabled = provider_status()
1544
1791
  ui['status'] = {
1545
1792
  "all": list(g_config['providers'].keys()),
1546
- "enabled": enabled,
1547
- "disabled": disabled
1793
+ "enabled": enabled,
1794
+ "disabled": disabled
1548
1795
  }
1796
+ # Add auth configuration
1797
+ ui['requiresAuth'] = auth_enabled
1798
+ ui['authType'] = 'oauth' if auth_enabled else 'apikey'
1549
1799
  return web.json_response(ui)
1550
1800
  app.router.add_get('/config', ui_config_handler)
1551
1801