llms-py 3.0.24__py3-none-any.whl → 3.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/extensions/github_auth/README.md +169 -0
- llms/extensions/github_auth/__init__.py +271 -0
- llms/extensions/github_auth/ui/index.mjs +66 -0
- llms/extensions/providers/anthropic.py +1 -1
- llms/extensions/providers/google.py +1 -1
- llms/extensions/providers/nvidia.py +1 -1
- llms/extensions/skills/__init__.py +137 -138
- llms/extensions/skills/ui/index.mjs +8 -10
- llms/llms.json +1 -9
- llms/main.py +140 -298
- llms/ui/App.mjs +4 -1
- llms/ui/ai.mjs +1 -2
- llms/ui/modules/chat/ChatBody.mjs +1 -2
- llms/ui/modules/layout.mjs +1 -57
- {llms_py-3.0.24.dist-info → llms_py-3.0.26.dist-info}/METADATA +1 -1
- {llms_py-3.0.24.dist-info → llms_py-3.0.26.dist-info}/RECORD +20 -17
- {llms_py-3.0.24.dist-info → llms_py-3.0.26.dist-info}/WHEEL +0 -0
- {llms_py-3.0.24.dist-info → llms_py-3.0.26.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.24.dist-info → llms_py-3.0.26.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.24.dist-info → llms_py-3.0.26.dist-info}/top_level.txt +0 -0
llms/main.py
CHANGED
|
@@ -17,7 +17,6 @@ import json
|
|
|
17
17
|
import mimetypes
|
|
18
18
|
import os
|
|
19
19
|
import re
|
|
20
|
-
import secrets
|
|
21
20
|
import shlex
|
|
22
21
|
import shutil
|
|
23
22
|
import site
|
|
@@ -45,7 +44,7 @@ from typing import (
|
|
|
45
44
|
get_origin,
|
|
46
45
|
get_type_hints,
|
|
47
46
|
)
|
|
48
|
-
from urllib.parse import parse_qs,
|
|
47
|
+
from urllib.parse import parse_qs, urljoin
|
|
49
48
|
|
|
50
49
|
import aiohttp
|
|
51
50
|
from aiohttp import web
|
|
@@ -57,12 +56,16 @@ try:
|
|
|
57
56
|
except ImportError:
|
|
58
57
|
HAS_PIL = False
|
|
59
58
|
|
|
60
|
-
VERSION = "3.0.24"
|
|
61
59
|
_ROOT = None
|
|
60
|
+
VERSION = "3.0.26"
|
|
62
61
|
DEBUG = os.getenv("DEBUG") == "1"
|
|
63
62
|
MOCK = os.getenv("MOCK") == "1"
|
|
64
63
|
MOCK_DIR = os.getenv("MOCK_DIR")
|
|
65
64
|
DISABLE_EXTENSIONS = (os.getenv("LLMS_DISABLE") or "").split(",")
|
|
65
|
+
DEFAULT_LIMITS = {
|
|
66
|
+
"client_timeout": 120,
|
|
67
|
+
"client_max_size": 20971520,
|
|
68
|
+
}
|
|
66
69
|
g_config_path = None
|
|
67
70
|
g_config = None
|
|
68
71
|
g_providers = None
|
|
@@ -70,8 +73,6 @@ g_handlers = {}
|
|
|
70
73
|
g_verbose = False
|
|
71
74
|
g_logprefix = ""
|
|
72
75
|
g_default_model = ""
|
|
73
|
-
g_sessions = {} # OAuth session storage: {session_token: {userId, userName, displayName, profileUrl, email, created}}
|
|
74
|
-
g_oauth_states = {} # CSRF protection: {state: {created, redirect_uri}}
|
|
75
76
|
g_app = None # ExtensionsContext Singleton
|
|
76
77
|
|
|
77
78
|
|
|
@@ -478,7 +479,7 @@ async def download_file(url):
|
|
|
478
479
|
|
|
479
480
|
async def session_download_file(session, url, default_mimetype="application/octet-stream"):
|
|
480
481
|
try:
|
|
481
|
-
async with session.get(url, timeout=
|
|
482
|
+
async with session.get(url, timeout=get_client_timeout()) as response:
|
|
482
483
|
response.raise_for_status()
|
|
483
484
|
content = await response.read()
|
|
484
485
|
mimetype = response.headers.get("Content-Type")
|
|
@@ -1297,7 +1298,7 @@ class OpenAiCompatible:
|
|
|
1297
1298
|
async with aiohttp.ClientSession() as session:
|
|
1298
1299
|
started_at = time.time()
|
|
1299
1300
|
async with session.post(
|
|
1300
|
-
self.chat_url, headers=self.headers, data=json.dumps(chat), timeout=
|
|
1301
|
+
self.chat_url, headers=self.headers, data=json.dumps(chat), timeout=get_client_timeout()
|
|
1301
1302
|
) as response:
|
|
1302
1303
|
chat["metadata"] = metadata
|
|
1303
1304
|
return self.to_response(await response_json(response), chat, started_at, context=context)
|
|
@@ -1364,7 +1365,7 @@ class OllamaProvider(OpenAiCompatible):
|
|
|
1364
1365
|
async with aiohttp.ClientSession() as session:
|
|
1365
1366
|
_log(f"GET {self.api}/api/tags")
|
|
1366
1367
|
async with session.get(
|
|
1367
|
-
f"{self.api}/api/tags", headers=self.headers, timeout=
|
|
1368
|
+
f"{self.api}/api/tags", headers=self.headers, timeout=get_client_timeout()
|
|
1368
1369
|
) as response:
|
|
1369
1370
|
data = await response_json(response)
|
|
1370
1371
|
for model in data.get("models", []):
|
|
@@ -1425,7 +1426,7 @@ class LMStudioProvider(OllamaProvider):
|
|
|
1425
1426
|
async with aiohttp.ClientSession() as session:
|
|
1426
1427
|
_log(f"GET {self.api}/models")
|
|
1427
1428
|
async with session.get(
|
|
1428
|
-
f"{self.api}/models", headers=self.headers, timeout=
|
|
1429
|
+
f"{self.api}/models", headers=self.headers, timeout=get_client_timeout()
|
|
1429
1430
|
) as response:
|
|
1430
1431
|
data = await response_json(response)
|
|
1431
1432
|
for model in data.get("data", []):
|
|
@@ -1762,6 +1763,15 @@ def convert_tool_args(function_name, function_args):
|
|
|
1762
1763
|
|
|
1763
1764
|
return function_args
|
|
1764
1765
|
|
|
1766
|
+
def get_tool_property(function_name, prop_name):
|
|
1767
|
+
tool_def = g_app.get_tool_definition(function_name)
|
|
1768
|
+
if not tool_def:
|
|
1769
|
+
return None
|
|
1770
|
+
if "function" in tool_def and "parameters" in tool_def["function"]:
|
|
1771
|
+
parameters = tool_def.get("function", {}).get("parameters")
|
|
1772
|
+
properties = parameters.get("properties", {})
|
|
1773
|
+
return properties.get(prop_name)
|
|
1774
|
+
return None
|
|
1765
1775
|
|
|
1766
1776
|
async def g_exec_tool(function_name, function_args):
|
|
1767
1777
|
_log(f"g_exec_tool: {function_name}")
|
|
@@ -1916,6 +1926,8 @@ async def g_chat_completion(chat, context=None):
|
|
|
1916
1926
|
except Exception as e:
|
|
1917
1927
|
tool_result = f"Error: Failed to parse JSON arguments for tool '{function_name}': {to_error_message(e)}"
|
|
1918
1928
|
else:
|
|
1929
|
+
if "user" in context and get_tool_property(function_name, "user"):
|
|
1930
|
+
function_args["user"] = context["user"]
|
|
1919
1931
|
tool_result, resources = await g_exec_tool(function_name, function_args)
|
|
1920
1932
|
|
|
1921
1933
|
# Append tool result to history
|
|
@@ -2790,8 +2802,45 @@ async def watch_config_files(config_path, providers_path, interval=1):
|
|
|
2790
2802
|
pass
|
|
2791
2803
|
|
|
2792
2804
|
|
|
2793
|
-
|
|
2794
|
-
|
|
2805
|
+
class AuthProvider:
|
|
2806
|
+
def __init__(self, app):
|
|
2807
|
+
self.app = app
|
|
2808
|
+
|
|
2809
|
+
def get_session_token(self, request: web.Request):
|
|
2810
|
+
return (
|
|
2811
|
+
request.query.get("session") or request.headers.get("X-Session-Token") or request.cookies.get("llms-token")
|
|
2812
|
+
)
|
|
2813
|
+
|
|
2814
|
+
def get_session(self, request: web.Request) -> Optional[Dict[str, Any]]:
|
|
2815
|
+
session_token = self.get_session_token(request)
|
|
2816
|
+
# _dbg(
|
|
2817
|
+
# f"Session token: {session_token} / {len(self.app.sessions)} sessions = {session_token in self.app.sessions}"
|
|
2818
|
+
# )
|
|
2819
|
+
|
|
2820
|
+
if not session_token or session_token not in self.app.sessions:
|
|
2821
|
+
return None
|
|
2822
|
+
|
|
2823
|
+
session_data = self.app.sessions[session_token]
|
|
2824
|
+
return session_data
|
|
2825
|
+
|
|
2826
|
+
def get_username(self, request: web.Request) -> Optional[str]:
|
|
2827
|
+
session = self.get_session(request)
|
|
2828
|
+
if session:
|
|
2829
|
+
return session.get("userName")
|
|
2830
|
+
return None
|
|
2831
|
+
|
|
2832
|
+
def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
|
2833
|
+
"""Check if request is authenticated. Returns (is_authenticated, user_data)"""
|
|
2834
|
+
session = self.get_session(request)
|
|
2835
|
+
if session:
|
|
2836
|
+
return True, session
|
|
2837
|
+
return False, None
|
|
2838
|
+
|
|
2839
|
+
|
|
2840
|
+
def get_client_timeout(app=None):
|
|
2841
|
+
app = app or g_app
|
|
2842
|
+
timeout = app.limits.get("client_timeout", 120) if app else 120
|
|
2843
|
+
return aiohttp.ClientTimeout(total=timeout)
|
|
2795
2844
|
|
|
2796
2845
|
|
|
2797
2846
|
class AppExtensions:
|
|
@@ -2803,6 +2852,7 @@ class AppExtensions:
|
|
|
2803
2852
|
self.cli_args = cli_args
|
|
2804
2853
|
self.extra_args = extra_args
|
|
2805
2854
|
self.config = None
|
|
2855
|
+
self.limits = DEFAULT_LIMITS
|
|
2806
2856
|
self.error_auth_required = create_error_response("Authentication required", "Unauthorized")
|
|
2807
2857
|
self.ui_extensions = []
|
|
2808
2858
|
self.chat_request_filters = []
|
|
@@ -2824,6 +2874,9 @@ class AppExtensions:
|
|
|
2824
2874
|
self.index_headers = []
|
|
2825
2875
|
self.index_footers = []
|
|
2826
2876
|
self.allowed_directories = []
|
|
2877
|
+
self.auth_providers = []
|
|
2878
|
+
self.sessions = {} # OAuth session storage: {session_token: {userId, userName, displayName, profileUrl, email, created}}
|
|
2879
|
+
self.oauth_states = {} # CSRF protection: {state: {created, redirect_uri}}
|
|
2827
2880
|
self.request_args = {
|
|
2828
2881
|
"image_config": dict, # e.g. { "aspect_ratio": "1:1" }
|
|
2829
2882
|
"temperature": float, # e.g: 0.7
|
|
@@ -2879,7 +2932,12 @@ class AppExtensions:
|
|
|
2879
2932
|
|
|
2880
2933
|
def set_config(self, config: Dict[str, Any]):
|
|
2881
2934
|
self.config = config
|
|
2882
|
-
self.
|
|
2935
|
+
self.limits = self.config.get("limits", DEFAULT_LIMITS)
|
|
2936
|
+
self.limits["client_timeout"] = self.limits.get("client_timeout", 120)
|
|
2937
|
+
self.limits["client_max_size"] = self.limits.get("client_max_size", 20971520)
|
|
2938
|
+
|
|
2939
|
+
def get_client_timeout(self):
|
|
2940
|
+
return get_client_timeout(self)
|
|
2883
2941
|
|
|
2884
2942
|
def set_allowed_directories(
|
|
2885
2943
|
self, directories: List[Annotated[str, "List of absolute paths that are allowed to be accessed."]]
|
|
@@ -2897,40 +2955,46 @@ class AppExtensions:
|
|
|
2897
2955
|
"""Get the list of allowed directories."""
|
|
2898
2956
|
return self.allowed_directories
|
|
2899
2957
|
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
if not self.auth_enabled:
|
|
2904
|
-
return True, None
|
|
2958
|
+
def add_auth_provider(self, auth_provider: AuthProvider) -> None:
|
|
2959
|
+
"""Add an authentication provider."""
|
|
2960
|
+
self.auth_providers.append(auth_provider)
|
|
2905
2961
|
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
if session_token and session_token in g_sessions:
|
|
2909
|
-
return True, g_sessions[session_token]
|
|
2910
|
-
|
|
2911
|
-
# Check for API key
|
|
2912
|
-
auth_header = request.headers.get("Authorization", "")
|
|
2913
|
-
if auth_header.startswith("Bearer "):
|
|
2914
|
-
api_key = auth_header[7:]
|
|
2915
|
-
if api_key:
|
|
2916
|
-
return True, {"authProvider": "apikey"}
|
|
2917
|
-
|
|
2918
|
-
return False, None
|
|
2962
|
+
def is_auth_enabled(self) -> bool:
|
|
2963
|
+
return len(self.auth_providers) > 0
|
|
2919
2964
|
|
|
2920
2965
|
def get_session(self, request: web.Request) -> Optional[Dict[str, Any]]:
|
|
2921
|
-
|
|
2966
|
+
for auth_provider in self.auth_providers:
|
|
2967
|
+
session = auth_provider.get_session(request)
|
|
2968
|
+
if session:
|
|
2969
|
+
return session
|
|
2970
|
+
return None
|
|
2922
2971
|
|
|
2923
|
-
|
|
2972
|
+
def get_username(self, request: web.Request) -> Optional[str]:
|
|
2973
|
+
for auth_provider in self.auth_providers:
|
|
2974
|
+
username = auth_provider.get_username(request)
|
|
2975
|
+
if username:
|
|
2976
|
+
return username
|
|
2924
2977
|
return None
|
|
2925
2978
|
|
|
2926
|
-
|
|
2927
|
-
|
|
2979
|
+
def assert_username(self, request: web.Request) -> str:
|
|
2980
|
+
if not self.is_auth_enabled():
|
|
2981
|
+
return None
|
|
2982
|
+
username = self.get_username(request)
|
|
2983
|
+
if not username:
|
|
2984
|
+
raise Exception("Authentication required")
|
|
2985
|
+
return username
|
|
2928
2986
|
|
|
2929
|
-
def
|
|
2930
|
-
|
|
2931
|
-
if
|
|
2932
|
-
return
|
|
2933
|
-
|
|
2987
|
+
def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
|
2988
|
+
"""Check if request is authenticated. Returns (is_authenticated, user_data)"""
|
|
2989
|
+
if len(self.auth_providers) == 0:
|
|
2990
|
+
return True, None
|
|
2991
|
+
|
|
2992
|
+
for auth_provider in self.auth_providers:
|
|
2993
|
+
is_authenticated, user_data = auth_provider.check_auth(request)
|
|
2994
|
+
if is_authenticated:
|
|
2995
|
+
return True, user_data
|
|
2996
|
+
|
|
2997
|
+
return False, None
|
|
2934
2998
|
|
|
2935
2999
|
def get_user_path(self, username: Optional[str] = None) -> str:
|
|
2936
3000
|
if username:
|
|
@@ -3032,6 +3096,8 @@ def handler_name(handler):
|
|
|
3032
3096
|
class ExtensionContext:
|
|
3033
3097
|
def __init__(self, app: AppExtensions, path: str):
|
|
3034
3098
|
self.app = app
|
|
3099
|
+
self.config = app.config
|
|
3100
|
+
self.limits = app.limits
|
|
3035
3101
|
self.cli_args = app.cli_args
|
|
3036
3102
|
self.extra_args = app.extra_args
|
|
3037
3103
|
self.error_auth_required = app.error_auth_required
|
|
@@ -3046,8 +3112,33 @@ class ExtensionContext:
|
|
|
3046
3112
|
self.verbose = g_verbose
|
|
3047
3113
|
self.aspect_ratios = app.aspect_ratios
|
|
3048
3114
|
self.request_args = app.request_args
|
|
3115
|
+
self.sessions = app.sessions
|
|
3116
|
+
self.oauth_states = app.oauth_states
|
|
3049
3117
|
self.disabled = False
|
|
3050
3118
|
|
|
3119
|
+
def get_client_timeout(self):
|
|
3120
|
+
return self.app.get_client_timeout()
|
|
3121
|
+
|
|
3122
|
+
def add_auth_provider(self, auth_provider: AuthProvider) -> None:
|
|
3123
|
+
"""Add an authentication provider."""
|
|
3124
|
+
self.app.add_auth_provider(auth_provider)
|
|
3125
|
+
self.log(f"Added Auth Provider: {auth_provider.__class__.__name__}, Authentication is now enabled")
|
|
3126
|
+
|
|
3127
|
+
def is_auth_enabled(self) -> bool:
|
|
3128
|
+
return self.app.is_auth_enabled()
|
|
3129
|
+
|
|
3130
|
+
def get_session(self, request: web.Request) -> Optional[Dict[str, Any]]:
|
|
3131
|
+
return self.app.get_session(request)
|
|
3132
|
+
|
|
3133
|
+
def get_username(self, request: web.Request) -> Optional[str]:
|
|
3134
|
+
return self.app.get_username(request)
|
|
3135
|
+
|
|
3136
|
+
def assert_username(self, request: web.Request) -> Optional[str]:
|
|
3137
|
+
return self.app.assert_username(request)
|
|
3138
|
+
|
|
3139
|
+
def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
|
3140
|
+
return self.app.check_auth(request)
|
|
3141
|
+
|
|
3051
3142
|
def set_allowed_directories(
|
|
3052
3143
|
self, directories: List[Annotated[str, "List of absolute paths that are allowed to be accessed."]]
|
|
3053
3144
|
) -> None:
|
|
@@ -3129,6 +3220,9 @@ class ExtensionContext:
|
|
|
3129
3220
|
def error_response(self, e: Exception, stacktrace: bool = False) -> Dict[str, Any]:
|
|
3130
3221
|
return to_error_response(e, stacktrace=stacktrace)
|
|
3131
3222
|
|
|
3223
|
+
def create_error_response(self, message, error_code="Error", stack_trace=None):
|
|
3224
|
+
return create_error_response(message, error_code, stack_trace)
|
|
3225
|
+
|
|
3132
3226
|
def add_provider(self, provider: Any):
|
|
3133
3227
|
self.log(f"Registered provider: {provider.__name__}")
|
|
3134
3228
|
self.app.all_providers.append(provider)
|
|
@@ -3983,33 +4077,11 @@ def cli_exec(cli_args, extra_args):
|
|
|
3983
4077
|
port = int(cli_args.serve)
|
|
3984
4078
|
|
|
3985
4079
|
# Validate auth configuration if enabled
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
# Expand environment variables
|
|
3993
|
-
if client_id.startswith("$"):
|
|
3994
|
-
client_id = client_id[1:]
|
|
3995
|
-
if client_secret.startswith("$"):
|
|
3996
|
-
client_secret = client_secret[1:]
|
|
3997
|
-
|
|
3998
|
-
client_id = os.getenv(client_id, client_id)
|
|
3999
|
-
client_secret = os.getenv(client_secret, client_secret)
|
|
4000
|
-
|
|
4001
|
-
if (
|
|
4002
|
-
not client_id
|
|
4003
|
-
or not client_secret
|
|
4004
|
-
or client_id == "GITHUB_CLIENT_ID"
|
|
4005
|
-
or client_secret == "GITHUB_CLIENT_SECRET"
|
|
4006
|
-
):
|
|
4007
|
-
print("ERROR: Authentication is enabled but GitHub OAuth is not properly configured.")
|
|
4008
|
-
print("Please set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET environment variables,")
|
|
4009
|
-
print("or disable authentication by setting 'auth.enabled' to false in llms.json")
|
|
4010
|
-
return ExitCode.FAILED
|
|
4011
|
-
|
|
4012
|
-
_log("Authentication enabled - GitHub OAuth configured")
|
|
4080
|
+
if g_config.get("auth", {}).get("enabled", False):
|
|
4081
|
+
print("ERROR: GitHub Authentication has moved to the github_auth extension.")
|
|
4082
|
+
print("Please remove the auth configuration from llms.json.")
|
|
4083
|
+
print("Learn more: https://llmspy.org/docs/deployment/github-oauth")
|
|
4084
|
+
return ExitCode.FAILED
|
|
4013
4085
|
|
|
4014
4086
|
client_max_size = g_config.get("limits", {}).get(
|
|
4015
4087
|
"client_max_size", 20 * 1024 * 1024
|
|
@@ -4226,236 +4298,6 @@ def cli_exec(cli_args, extra_args):
|
|
|
4226
4298
|
|
|
4227
4299
|
app.router.add_get("/~cache/{tail:.*}", cache_handler)
|
|
4228
4300
|
|
|
4229
|
-
# OAuth handlers
|
|
4230
|
-
async def github_auth_handler(request):
|
|
4231
|
-
"""Initiate GitHub OAuth flow"""
|
|
4232
|
-
if "auth" not in g_config or "github" not in g_config["auth"]:
|
|
4233
|
-
return web.json_response(create_error_response("GitHub OAuth not configured"), status=500)
|
|
4234
|
-
|
|
4235
|
-
auth_config = g_config["auth"]["github"]
|
|
4236
|
-
client_id = auth_config.get("client_id", "")
|
|
4237
|
-
redirect_uri = auth_config.get("redirect_uri", "")
|
|
4238
|
-
|
|
4239
|
-
# Expand environment variables
|
|
4240
|
-
if client_id.startswith("$"):
|
|
4241
|
-
client_id = client_id[1:]
|
|
4242
|
-
if redirect_uri.startswith("$"):
|
|
4243
|
-
redirect_uri = redirect_uri[1:]
|
|
4244
|
-
|
|
4245
|
-
client_id = os.getenv(client_id, client_id)
|
|
4246
|
-
redirect_uri = os.getenv(redirect_uri, redirect_uri)
|
|
4247
|
-
|
|
4248
|
-
if not client_id:
|
|
4249
|
-
return web.json_response(create_error_response("GitHub client_id not configured"), status=500)
|
|
4250
|
-
|
|
4251
|
-
# Generate CSRF state token
|
|
4252
|
-
state = secrets.token_urlsafe(32)
|
|
4253
|
-
g_oauth_states[state] = {"created": time.time(), "redirect_uri": redirect_uri}
|
|
4254
|
-
|
|
4255
|
-
# Clean up old states (older than 10 minutes)
|
|
4256
|
-
current_time = time.time()
|
|
4257
|
-
expired_states = [s for s, data in g_oauth_states.items() if current_time - data["created"] > 600]
|
|
4258
|
-
for s in expired_states:
|
|
4259
|
-
del g_oauth_states[s]
|
|
4260
|
-
|
|
4261
|
-
# Build GitHub authorization URL
|
|
4262
|
-
params = {
|
|
4263
|
-
"client_id": client_id,
|
|
4264
|
-
"redirect_uri": redirect_uri,
|
|
4265
|
-
"state": state,
|
|
4266
|
-
"scope": "read:user user:email",
|
|
4267
|
-
}
|
|
4268
|
-
auth_url = f"https://github.com/login/oauth/authorize?{urlencode(params)}"
|
|
4269
|
-
|
|
4270
|
-
return web.HTTPFound(auth_url)
|
|
4271
|
-
|
|
4272
|
-
def validate_user(github_username):
|
|
4273
|
-
auth_config = g_config["auth"]["github"]
|
|
4274
|
-
# Check if user is restricted
|
|
4275
|
-
restrict_to = auth_config.get("restrict_to", "")
|
|
4276
|
-
|
|
4277
|
-
# Expand environment variables
|
|
4278
|
-
if restrict_to.startswith("$"):
|
|
4279
|
-
restrict_to = restrict_to[1:]
|
|
4280
|
-
|
|
4281
|
-
restrict_to = os.getenv(restrict_to, None if restrict_to == "GITHUB_USERS" else restrict_to)
|
|
4282
|
-
|
|
4283
|
-
# If restrict_to is configured, validate the user
|
|
4284
|
-
if restrict_to:
|
|
4285
|
-
# Parse allowed users (comma or space delimited)
|
|
4286
|
-
allowed_users = [u.strip() for u in re.split(r"[,\s]+", restrict_to) if u.strip()]
|
|
4287
|
-
|
|
4288
|
-
# Check if user is in the allowed list
|
|
4289
|
-
if not github_username or github_username not in allowed_users:
|
|
4290
|
-
_log(f"Access denied for user: {github_username}. Not in allowed list: {allowed_users}")
|
|
4291
|
-
return web.Response(
|
|
4292
|
-
text=f"Access denied. User '{github_username}' is not authorized to access this application.",
|
|
4293
|
-
status=403,
|
|
4294
|
-
)
|
|
4295
|
-
return None
|
|
4296
|
-
|
|
4297
|
-
async def github_callback_handler(request):
|
|
4298
|
-
"""Handle GitHub OAuth callback"""
|
|
4299
|
-
code = request.query.get("code")
|
|
4300
|
-
state = request.query.get("state")
|
|
4301
|
-
|
|
4302
|
-
# Handle malformed URLs where query params are appended with & instead of ?
|
|
4303
|
-
if not code and "tail" in request.match_info:
|
|
4304
|
-
tail = request.match_info["tail"]
|
|
4305
|
-
if tail.startswith("&"):
|
|
4306
|
-
params = parse_qs(tail[1:])
|
|
4307
|
-
code = params.get("code", [None])[0]
|
|
4308
|
-
state = params.get("state", [None])[0]
|
|
4309
|
-
|
|
4310
|
-
if not code or not state:
|
|
4311
|
-
return web.Response(text="Missing code or state parameter", status=400)
|
|
4312
|
-
|
|
4313
|
-
# Verify state token (CSRF protection)
|
|
4314
|
-
if state not in g_oauth_states:
|
|
4315
|
-
return web.Response(text="Invalid state parameter", status=400)
|
|
4316
|
-
|
|
4317
|
-
g_oauth_states.pop(state)
|
|
4318
|
-
|
|
4319
|
-
if "auth" not in g_config or "github" not in g_config["auth"]:
|
|
4320
|
-
return web.json_response(create_error_response("GitHub OAuth not configured"), status=500)
|
|
4321
|
-
|
|
4322
|
-
auth_config = g_config["auth"]["github"]
|
|
4323
|
-
client_id = auth_config.get("client_id", "")
|
|
4324
|
-
client_secret = auth_config.get("client_secret", "")
|
|
4325
|
-
redirect_uri = auth_config.get("redirect_uri", "")
|
|
4326
|
-
|
|
4327
|
-
# Expand environment variables
|
|
4328
|
-
if client_id.startswith("$"):
|
|
4329
|
-
client_id = client_id[1:]
|
|
4330
|
-
if client_secret.startswith("$"):
|
|
4331
|
-
client_secret = client_secret[1:]
|
|
4332
|
-
if redirect_uri.startswith("$"):
|
|
4333
|
-
redirect_uri = redirect_uri[1:]
|
|
4334
|
-
|
|
4335
|
-
client_id = os.getenv(client_id, client_id)
|
|
4336
|
-
client_secret = os.getenv(client_secret, client_secret)
|
|
4337
|
-
redirect_uri = os.getenv(redirect_uri, redirect_uri)
|
|
4338
|
-
|
|
4339
|
-
if not client_id or not client_secret:
|
|
4340
|
-
return web.json_response(create_error_response("GitHub OAuth credentials not configured"), status=500)
|
|
4341
|
-
|
|
4342
|
-
# Exchange code for access token
|
|
4343
|
-
async with aiohttp.ClientSession() as session:
|
|
4344
|
-
token_url = "https://github.com/login/oauth/access_token"
|
|
4345
|
-
token_data = {
|
|
4346
|
-
"client_id": client_id,
|
|
4347
|
-
"client_secret": client_secret,
|
|
4348
|
-
"code": code,
|
|
4349
|
-
"redirect_uri": redirect_uri,
|
|
4350
|
-
}
|
|
4351
|
-
headers = {"Accept": "application/json"}
|
|
4352
|
-
|
|
4353
|
-
async with session.post(token_url, data=token_data, headers=headers) as resp:
|
|
4354
|
-
token_response = await resp.json()
|
|
4355
|
-
access_token = token_response.get("access_token")
|
|
4356
|
-
|
|
4357
|
-
if not access_token:
|
|
4358
|
-
error = token_response.get("error_description", "Failed to get access token")
|
|
4359
|
-
return web.json_response(create_error_response(f"OAuth error: {error}"), status=400)
|
|
4360
|
-
|
|
4361
|
-
# Fetch user info
|
|
4362
|
-
user_url = "https://api.github.com/user"
|
|
4363
|
-
headers = {"Authorization": f"Bearer {access_token}", "Accept": "application/json"}
|
|
4364
|
-
|
|
4365
|
-
async with session.get(user_url, headers=headers) as resp:
|
|
4366
|
-
user_data = await resp.json()
|
|
4367
|
-
|
|
4368
|
-
# Validate user
|
|
4369
|
-
error_response = validate_user(user_data.get("login", ""))
|
|
4370
|
-
if error_response:
|
|
4371
|
-
return error_response
|
|
4372
|
-
|
|
4373
|
-
# Create session
|
|
4374
|
-
session_token = secrets.token_urlsafe(32)
|
|
4375
|
-
g_sessions[session_token] = {
|
|
4376
|
-
"userId": str(user_data.get("id", "")),
|
|
4377
|
-
"userName": user_data.get("login", ""),
|
|
4378
|
-
"displayName": user_data.get("name", ""),
|
|
4379
|
-
"profileUrl": user_data.get("avatar_url", ""),
|
|
4380
|
-
"email": user_data.get("email", ""),
|
|
4381
|
-
"created": time.time(),
|
|
4382
|
-
}
|
|
4383
|
-
|
|
4384
|
-
# Redirect to UI with session token
|
|
4385
|
-
response = web.HTTPFound(f"/?session={session_token}")
|
|
4386
|
-
response.set_cookie("llms-token", session_token, httponly=True, path="/", max_age=86400)
|
|
4387
|
-
return response
|
|
4388
|
-
|
|
4389
|
-
async def session_handler(request):
|
|
4390
|
-
"""Validate and return session info"""
|
|
4391
|
-
session_token = get_session_token(request)
|
|
4392
|
-
|
|
4393
|
-
if not session_token or session_token not in g_sessions:
|
|
4394
|
-
return web.json_response(create_error_response("Invalid or expired session"), status=401)
|
|
4395
|
-
|
|
4396
|
-
session_data = g_sessions[session_token]
|
|
4397
|
-
|
|
4398
|
-
# Clean up old sessions (older than 24 hours)
|
|
4399
|
-
current_time = time.time()
|
|
4400
|
-
expired_sessions = [token for token, data in g_sessions.items() if current_time - data["created"] > 86400]
|
|
4401
|
-
for token in expired_sessions:
|
|
4402
|
-
del g_sessions[token]
|
|
4403
|
-
|
|
4404
|
-
return web.json_response({**session_data, "sessionToken": session_token})
|
|
4405
|
-
|
|
4406
|
-
async def logout_handler(request):
|
|
4407
|
-
"""End OAuth session"""
|
|
4408
|
-
session_token = get_session_token(request)
|
|
4409
|
-
|
|
4410
|
-
if session_token and session_token in g_sessions:
|
|
4411
|
-
del g_sessions[session_token]
|
|
4412
|
-
|
|
4413
|
-
response = web.json_response({"success": True})
|
|
4414
|
-
response.del_cookie("llms-token")
|
|
4415
|
-
return response
|
|
4416
|
-
|
|
4417
|
-
async def auth_handler(request):
|
|
4418
|
-
"""Check authentication status and return user info"""
|
|
4419
|
-
# Check for OAuth session token
|
|
4420
|
-
session_token = get_session_token(request)
|
|
4421
|
-
|
|
4422
|
-
if session_token and session_token in g_sessions:
|
|
4423
|
-
session_data = g_sessions[session_token]
|
|
4424
|
-
return web.json_response(
|
|
4425
|
-
{
|
|
4426
|
-
"userId": session_data.get("userId", ""),
|
|
4427
|
-
"userName": session_data.get("userName", ""),
|
|
4428
|
-
"displayName": session_data.get("displayName", ""),
|
|
4429
|
-
"profileUrl": session_data.get("profileUrl", ""),
|
|
4430
|
-
"authProvider": "github",
|
|
4431
|
-
}
|
|
4432
|
-
)
|
|
4433
|
-
|
|
4434
|
-
# Check for API key in Authorization header
|
|
4435
|
-
# auth_header = request.headers.get('Authorization', '')
|
|
4436
|
-
# if auth_header.startswith('Bearer '):
|
|
4437
|
-
# # For API key auth, return a basic response
|
|
4438
|
-
# # You can customize this based on your API key validation logic
|
|
4439
|
-
# api_key = auth_header[7:]
|
|
4440
|
-
# if api_key: # Add your API key validation logic here
|
|
4441
|
-
# return web.json_response({
|
|
4442
|
-
# "userId": "1",
|
|
4443
|
-
# "userName": "apiuser",
|
|
4444
|
-
# "displayName": "API User",
|
|
4445
|
-
# "profileUrl": "",
|
|
4446
|
-
# "authProvider": "apikey"
|
|
4447
|
-
# })
|
|
4448
|
-
|
|
4449
|
-
# Not authenticated - return error in expected format
|
|
4450
|
-
return web.json_response(g_app.error_auth_required, status=401)
|
|
4451
|
-
|
|
4452
|
-
app.router.add_get("/auth", auth_handler)
|
|
4453
|
-
app.router.add_get("/auth/github", github_auth_handler)
|
|
4454
|
-
app.router.add_get("/auth/github/callback", github_callback_handler)
|
|
4455
|
-
app.router.add_get("/auth/github/callback{tail:.*}", github_callback_handler)
|
|
4456
|
-
app.router.add_get("/auth/session", session_handler)
|
|
4457
|
-
app.router.add_post("/auth/logout", logout_handler)
|
|
4458
|
-
|
|
4459
4301
|
async def ui_static(request: web.Request) -> web.Response:
|
|
4460
4302
|
path = Path(request.match_info["path"])
|
|
4461
4303
|
|
|
@@ -4494,8 +4336,8 @@ def cli_exec(cli_args, extra_args):
|
|
|
4494
4336
|
enabled, disabled = provider_status()
|
|
4495
4337
|
ret["status"] = {"all": list(g_config["providers"].keys()), "enabled": enabled, "disabled": disabled}
|
|
4496
4338
|
# Add auth configuration
|
|
4497
|
-
ret["requiresAuth"] =
|
|
4498
|
-
ret["
|
|
4339
|
+
ret["requiresAuth"] = g_app.is_auth_enabled()
|
|
4340
|
+
ret["authTypes"] = [provider.__class__.__name__ for provider in g_app.auth_providers]
|
|
4499
4341
|
return web.json_response(ret)
|
|
4500
4342
|
|
|
4501
4343
|
app.router.add_get("/config", config_handler)
|
llms/ui/App.mjs
CHANGED
|
@@ -173,7 +173,10 @@ export default {
|
|
|
173
173
|
<div>
|
|
174
174
|
<ModelSelector :models="$state.models" v-model="$state.selectedModel" />
|
|
175
175
|
</div>
|
|
176
|
-
<
|
|
176
|
+
<div class="flex items-center gap-2">
|
|
177
|
+
<TopBar id="top-bar" />
|
|
178
|
+
<Avatar />
|
|
179
|
+
</div>
|
|
177
180
|
</div>
|
|
178
181
|
<TopPanel v-if="$ai.hasAccess" id="top-panel" :class="$ctx.cls('top-panel', 'shrink-0')" />
|
|
179
182
|
<div id="page" :class="$ctx.cls('page', 'flex-1 overflow-y-auto min-h-0 flex flex-col')">
|
llms/ui/ai.mjs
CHANGED
|
@@ -6,7 +6,7 @@ const headers = { 'Accept': 'application/json' }
|
|
|
6
6
|
const prefsKey = 'llms.prefs'
|
|
7
7
|
|
|
8
8
|
export const o = {
|
|
9
|
-
version: '3.0.
|
|
9
|
+
version: '3.0.26',
|
|
10
10
|
base,
|
|
11
11
|
prefsKey,
|
|
12
12
|
welcome: 'Welcome to llms.py',
|
|
@@ -165,7 +165,6 @@ export const o = {
|
|
|
165
165
|
return { config, models, extensions, auth }
|
|
166
166
|
},
|
|
167
167
|
|
|
168
|
-
|
|
169
168
|
async uploadFile(file) {
|
|
170
169
|
const formData = new FormData()
|
|
171
170
|
formData.append('file', file)
|
|
@@ -643,8 +643,7 @@ export const ChatBody = {
|
|
|
643
643
|
<div class="mx-auto max-w-6xl px-4 py-6">
|
|
644
644
|
|
|
645
645
|
<div v-if="!$ai.hasAccess">
|
|
646
|
-
<
|
|
647
|
-
<SignIn v-else @done="$ai.signIn($event)" />
|
|
646
|
+
<SignIn @done="$ai.signIn($event)" />
|
|
648
647
|
</div>
|
|
649
648
|
<!-- Welcome message when no thread is selected -->
|
|
650
649
|
<div v-else-if="!currentThread" class="text-center py-12">
|