llms-py 3.0.24__py3-none-any.whl → 3.0.25__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.
@@ -0,0 +1,169 @@
1
+ # GitHub Auth Extension
2
+
3
+ The GitHub Auth extension enables OAuth 2.0 authentication via GitHub for your llms application. When enabled, users must sign in with their GitHub account before accessing the application.
4
+
5
+ ## Features
6
+
7
+ - **GitHub OAuth 2.0** - Standard OAuth flow with CSRF protection
8
+ - **User Restrictions** - Optionally restrict access to specific GitHub users
9
+ - **Session Management** - Automatic session handling with 24-hour expiry
10
+ - **Environment Variables** - Credentials can use env vars for secure deployment
11
+
12
+ ## Configuration
13
+
14
+ Create a config file at `~/.llms/users/default/github_auth/config.json`:
15
+
16
+ ```json
17
+ {
18
+ "enabled": true,
19
+ "client_id": "$GITHUB_CLIENT_ID",
20
+ "client_secret": "$GITHUB_CLIENT_SECRET",
21
+ "redirect_uri": "http://localhost:8000/auth/github/callback",
22
+ "restrict_to": "$GITHUB_USERS"
23
+ }
24
+ ```
25
+
26
+ | Property | Description |
27
+ |-----------------|-------------|
28
+ | `client_id` | GitHub OAuth App client ID |
29
+ | `client_secret` | GitHub OAuth App client secret |
30
+ | `redirect_uri` | Callback URL registered with GitHub |
31
+ | `restrict_to` | Optional comma/space-delimited list of allowed GitHub usernames |
32
+ | `enabled` | Set to `false` to disable the extension |
33
+
34
+ Values prefixed with `$` are resolved from environment variables.
35
+
36
+ ## Creating a GitHub OAuth App
37
+
38
+ 1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
39
+ 2. Click **New OAuth App**
40
+ 3. Fill in the application details:
41
+ - **Application name**: Your app name
42
+ - **Homepage URL**: Your app's homepage (e.g., `http://localhost:8000`)
43
+ - **Authorization callback URL**: Must match your `redirect_uri` (e.g., `http://localhost:8000/auth/github/callback`)
44
+ 4. Click **Register application**
45
+ 5. Copy the **Client ID** and generate a **Client Secret**
46
+
47
+ ## API Endpoints
48
+
49
+ The extension registers these routes:
50
+
51
+ | Method | Endpoint | Description |
52
+ |--------|-------------------------|-------------|
53
+ | GET | `/auth` | Check authentication status |
54
+ | GET | `/auth/github` | Initiate GitHub OAuth flow |
55
+ | GET | `/auth/github/callback` | OAuth callback handler |
56
+ | GET | `/auth/session` | Get current session info |
57
+ | POST | `/auth/logout` | End the current session |
58
+
59
+ ### GET /auth
60
+
61
+ Returns the authenticated user's info or a 401 error:
62
+
63
+ ```json
64
+ {
65
+ "userId": "12345",
66
+ "userName": "octocat",
67
+ "displayName": "The Octocat",
68
+ "profileUrl": "https://avatars.githubusercontent.com/u/12345",
69
+ "authProvider": "github"
70
+ }
71
+ ```
72
+
73
+ ### GET /auth/session
74
+
75
+ Returns full session details including the session token:
76
+
77
+ ```json
78
+ {
79
+ "userId": "12345",
80
+ "userName": "octocat",
81
+ "displayName": "The Octocat",
82
+ "profileUrl": "https://avatars.githubusercontent.com/u/12345",
83
+ "email": "octocat@github.com",
84
+ "created": 1706600000.123,
85
+ "sessionToken": "..."
86
+ }
87
+ ```
88
+
89
+ ## OAuth Flow
90
+
91
+ ```
92
+ ┌─────────┐ ┌─────────┐ ┌────────┐
93
+ │ Browser │ │ llms │ │ GitHub │
94
+ └────┬────┘ └────┬────┘ └───┬────┘
95
+ │ │ │
96
+ │ GET /auth/github │ │
97
+ ├───────────────────►│ │
98
+ │ │ │
99
+ │ 302 Redirect │ │
100
+ │◄───────────────────┤ │
101
+ │ │ │
102
+ │ /login/oauth/authorize?... │
103
+ ├────────────────────────────────────────►
104
+ │ │ │
105
+ │ User grants access │
106
+ │◄────────────────────────────────────────
107
+ │ │ │
108
+ │ GET /auth/github/callback?code=... │
109
+ ├───────────────────►│ │
110
+ │ │ │
111
+ │ │ POST /access_token │
112
+ │ ├──────────────────►│
113
+ │ │ │
114
+ │ │ access_token │
115
+ │ │◄──────────────────┤
116
+ │ │ │
117
+ │ │ GET /user │
118
+ │ ├──────────────────►│
119
+ │ │ │
120
+ │ │ user info │
121
+ │ │◄──────────────────┤
122
+ │ │ │
123
+ │ 302 /?session=... │ │
124
+ │ Set-Cookie: token │ │
125
+ │◄───────────────────┤ │
126
+ │ │ │
127
+ ```
128
+
129
+ 1. User clicks "Sign in with GitHub" → redirects to `/auth/github`
130
+ 2. Server generates CSRF state token and redirects to GitHub
131
+ 3. User authorizes the app on GitHub
132
+ 4. GitHub redirects back with authorization code
133
+ 5. Server exchanges code for access token
134
+ 6. Server fetches user info from GitHub API
135
+ 7. Server creates session and sets cookie
136
+
137
+ ## Restricting Access
138
+
139
+ To limit access to specific GitHub users, set `restrict_to` in your config:
140
+
141
+ ```json
142
+ {
143
+ "client_id": "...",
144
+ "client_secret": "...",
145
+ "redirect_uri": "...",
146
+ "restrict_to": "alice bob charlie"
147
+ }
148
+ ```
149
+
150
+ Users not in this list receive a `403 Forbidden` response.
151
+
152
+ ## UI Component
153
+
154
+ The extension provides a custom `SignIn` component that displays a "Sign in with GitHub" button. This component automatically overrides the default sign-in UI when the extension is loaded.
155
+
156
+ ## Session Storage
157
+
158
+ Sessions are stored in memory with:
159
+ - **Token**: Cryptographically secure random string
160
+ - **User data**: GitHub user ID, username, display name, avatar URL, email
161
+ - **Expiry**: Automatic cleanup after 24 hours
162
+ - **Cookie**: `llms-token` with `httponly` flag for security
163
+
164
+ ## Security Notes
165
+
166
+ - **CSRF Protection**: OAuth state tokens prevent cross-site request forgery
167
+ - **State Cleanup**: Expired state tokens (>10 min) are automatically removed
168
+ - **Session Cleanup**: Sessions older than 24 hours are pruned
169
+ - **HttpOnly Cookie**: Session token is not accessible via JavaScript
@@ -0,0 +1,254 @@
1
+ import json
2
+ import os
3
+ import re
4
+ import secrets
5
+ import time
6
+ from urllib.parse import parse_qs, urlencode
7
+
8
+ import aiohttp
9
+ from aiohttp import web
10
+
11
+
12
+ def install(ctx):
13
+ g_app = ctx.app
14
+
15
+ auth_config_file = os.path.join(ctx.get_user_path(), "github_auth", "config.json")
16
+
17
+ auth_config = None
18
+ if os.path.exists(auth_config_file):
19
+ try:
20
+ with open(auth_config_file, encoding="utf-8") as f:
21
+ auth_config = json.load(f)
22
+ if "enabled" in auth_config and not auth_config["enabled"]:
23
+ ctx.log("GitHub Auth is disabled in config")
24
+ auth_config = None
25
+ except Exception as e:
26
+ ctx.err("Failed to load GitHub auth config", e)
27
+ else:
28
+ ctx.dbg(f"GitHub Auth config file '{auth_config_file}' not found")
29
+
30
+ if not auth_config:
31
+ # don't load extension if auth_config is not found or is disabled
32
+ ctx.disabled = True
33
+ return
34
+
35
+ client_id = auth_config.get("client_id", "")
36
+ client_secret = auth_config.get("client_secret", "")
37
+ redirect_uri = auth_config.get("redirect_uri", "")
38
+ restrict_to = auth_config.get("restrict_to", "")
39
+
40
+ # Expand environment variables
41
+ if client_id.startswith("$"):
42
+ client_id = client_id[1:]
43
+ if client_secret.startswith("$"):
44
+ client_secret = client_secret[1:]
45
+ client_secret = os.getenv(client_secret)
46
+ if redirect_uri.startswith("$"):
47
+ redirect_uri = redirect_uri[1:]
48
+ redirect_uri = os.getenv(redirect_uri)
49
+ if restrict_to.startswith("$"):
50
+ restrict_to = restrict_to[1:]
51
+ restrict_to = os.getenv(restrict_to)
52
+
53
+ # check if client_id is set
54
+ if client_id == "GITHUB_CLIENT_ID":
55
+ client_id = os.getenv(client_id)
56
+ if client_secret == "GITHUB_CLIENT_SECRET":
57
+ client_secret = os.getenv(client_secret)
58
+ if restrict_to == "GITHUB_USERS":
59
+ restrict_to = os.getenv(restrict_to)
60
+
61
+ if not client_id or not redirect_uri or not client_secret:
62
+ ctx.disabled = True
63
+ ctx.log("GitHub OAuth client_id, client_secret and redirect_uri are not configured")
64
+ return
65
+
66
+ from llms.main import AuthProvider
67
+
68
+ class GitHubAuthProvider(AuthProvider):
69
+ def __init__(self, app):
70
+ super().__init__(app)
71
+
72
+ # Adding an Auth Provider forces Authentication to be enabled
73
+ auth_provider = GitHubAuthProvider(g_app)
74
+ g_app.auth_providers.append(auth_provider)
75
+
76
+ # OAuth handlers
77
+ async def github_auth_handler(request):
78
+ # Generate CSRF state token
79
+ state = secrets.token_urlsafe(32)
80
+ ctx.oauth_states[state] = {"created": time.time(), "redirect_uri": redirect_uri}
81
+
82
+ # Clean up old states (older than 10 minutes)
83
+ current_time = time.time()
84
+ expired_states = [s for s, data in ctx.oauth_states.items() if current_time - data["created"] > 600]
85
+ for s in expired_states:
86
+ del ctx.oauth_states[s]
87
+
88
+ # Build GitHub authorization URL
89
+ params = {
90
+ "client_id": client_id,
91
+ "redirect_uri": redirect_uri,
92
+ "state": state,
93
+ "scope": "read:user user:email",
94
+ }
95
+ auth_url = f"https://github.com/login/oauth/authorize?{urlencode(params)}"
96
+
97
+ return web.HTTPFound(auth_url)
98
+
99
+ def validate_user(github_username):
100
+ # If restrict_to is configured, validate the user
101
+ if restrict_to:
102
+ # Parse allowed users (comma or space delimited)
103
+ allowed_users = [u.strip() for u in re.split(r"[,\s]+", restrict_to) if u.strip()]
104
+
105
+ # Check if user is in the allowed list
106
+ if not github_username or github_username not in allowed_users:
107
+ ctx.log(f"Access denied for user: {github_username}. Not in allowed list: {allowed_users}")
108
+ return web.Response(
109
+ text=f"Access denied. User '{github_username}' is not authorized to access this application.",
110
+ status=403,
111
+ )
112
+ return None
113
+
114
+ async def github_callback_handler(request):
115
+ """Handle GitHub OAuth callback"""
116
+ code = request.query.get("code")
117
+ state = request.query.get("state")
118
+
119
+ # Handle malformed URLs where query params are appended with & instead of ?
120
+ if not code and "tail" in request.match_info:
121
+ tail = request.match_info["tail"]
122
+ if tail.startswith("&"):
123
+ params = parse_qs(tail[1:])
124
+ code = params.get("code", [None])[0]
125
+ state = params.get("state", [None])[0]
126
+
127
+ if not code or not state:
128
+ return web.Response(text="Missing code or state parameter", status=400)
129
+
130
+ # Verify state token (CSRF protection)
131
+ if state not in ctx.oauth_states:
132
+ return web.Response(text="Invalid state parameter", status=400)
133
+
134
+ ctx.oauth_states.pop(state)
135
+
136
+ # Exchange code for access token
137
+ async with aiohttp.ClientSession() as session:
138
+ token_url = "https://github.com/login/oauth/access_token"
139
+ token_data = {
140
+ "client_id": client_id,
141
+ "client_secret": client_secret,
142
+ "code": code,
143
+ "redirect_uri": redirect_uri,
144
+ }
145
+ headers = {"Accept": "application/json"}
146
+
147
+ async with session.post(token_url, data=token_data, headers=headers) as resp:
148
+ token_response = await resp.json()
149
+ access_token = token_response.get("access_token")
150
+
151
+ if not access_token:
152
+ error = token_response.get("error_description", "Failed to get access token")
153
+ return web.json_response(ctx.create_error_response(f"OAuth error: {error}"), status=400)
154
+
155
+ # Fetch user info
156
+ user_url = "https://api.github.com/user"
157
+ headers = {"Authorization": f"Bearer {access_token}", "Accept": "application/json"}
158
+
159
+ async with session.get(user_url, headers=headers) as resp:
160
+ user_data = await resp.json()
161
+
162
+ # Validate user
163
+ error_response = validate_user(user_data.get("login", ""))
164
+ if error_response:
165
+ return error_response
166
+
167
+ # Create session
168
+ session_token = secrets.token_urlsafe(32)
169
+ ctx.sessions[session_token] = {
170
+ "userId": str(user_data.get("id", "")),
171
+ "userName": user_data.get("login", ""),
172
+ "displayName": user_data.get("name", ""),
173
+ "profileUrl": user_data.get("avatar_url", ""),
174
+ "email": user_data.get("email", ""),
175
+ "created": time.time(),
176
+ }
177
+
178
+ # Redirect to UI with session token
179
+ response = web.HTTPFound(f"/?session={session_token}")
180
+ response.set_cookie("llms-token", session_token, httponly=True, path="/", max_age=86400)
181
+ return response
182
+
183
+ async def session_handler(request):
184
+ """Validate and return session info"""
185
+ session_token = auth_provider.get_session_token(request)
186
+
187
+ if not session_token or session_token not in ctx.sessions:
188
+ return web.json_response(ctx.create_error_response("Invalid or expired session"), status=401)
189
+
190
+ session_data = ctx.sessions[session_token]
191
+
192
+ # Clean up old sessions (older than 24 hours)
193
+ current_time = time.time()
194
+ expired_sessions = [token for token, data in ctx.sessions.items() if current_time - data["created"] > 86400]
195
+ for token in expired_sessions:
196
+ del ctx.sessions[token]
197
+
198
+ return web.json_response({**session_data, "sessionToken": session_token})
199
+
200
+ async def logout_handler(request):
201
+ """End OAuth session"""
202
+ session_token = auth_provider.get_session_token(request)
203
+
204
+ if session_token and session_token in g_app.sessions:
205
+ del g_app.sessions[session_token]
206
+
207
+ response = web.json_response({"success": True})
208
+ response.del_cookie("llms-token")
209
+ return response
210
+
211
+ async def auth_handler(request):
212
+ """Check authentication status and return user info"""
213
+ # Check for OAuth session token
214
+ session_token = auth_provider.get_session_token(request)
215
+
216
+ if session_token and session_token in g_app.sessions:
217
+ session_data = g_app.sessions[session_token]
218
+ return web.json_response(
219
+ {
220
+ "userId": session_data.get("userId", ""),
221
+ "userName": session_data.get("userName", ""),
222
+ "displayName": session_data.get("displayName", ""),
223
+ "profileUrl": session_data.get("profileUrl", ""),
224
+ "authProvider": "github",
225
+ }
226
+ )
227
+
228
+ # Check for API key in Authorization header
229
+ # auth_header = request.headers.get('Authorization', '')
230
+ # if auth_header.startswith('Bearer '):
231
+ # # For API key auth, return a basic response
232
+ # # You can customize this based on your API key validation logic
233
+ # api_key = auth_header[7:]
234
+ # if api_key: # Add your API key validation logic here
235
+ # return web.json_response({
236
+ # "userId": "1",
237
+ # "userName": "apiuser",
238
+ # "displayName": "API User",
239
+ # "profileUrl": "",
240
+ # "authProvider": "apikey"
241
+ # })
242
+
243
+ # Not authenticated - return error in expected format
244
+ return web.json_response(g_app.error_auth_required, status=401)
245
+
246
+ ctx.add_get("/auth", auth_handler)
247
+ ctx.add_get("/auth/github", github_auth_handler)
248
+ ctx.add_get("/auth/github/callback", github_callback_handler)
249
+ ctx.add_get("/auth/github/callback{tail:.*}", github_callback_handler)
250
+ ctx.add_get("/auth/session", session_handler)
251
+ ctx.add_post("/auth/logout", logout_handler)
252
+
253
+
254
+ __install__ = install
@@ -0,0 +1,66 @@
1
+ import { ref } from "vue"
2
+
3
+ const SignIn = {
4
+ template: `
5
+ <div class="min-h-full -mt-36 flex flex-col justify-center sm:px-6 lg:px-8">
6
+ <div class="sm:mx-auto sm:w-full sm:max-w-md text-center">
7
+ <Welcome />
8
+ </div>
9
+ <div class="sm:mx-auto sm:w-full sm:max-w-md">
10
+ <div v-if="errorMessage" class="mb-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 rounded-lg px-4 py-3">
11
+ <div class="flex items-start space-x-2">
12
+ <div class="flex-1">
13
+ <div class="text-base font-medium">{{ errorMessage }}</div>
14
+ </div>
15
+ <button type="button"
16
+ @click="errorMessage = null"
17
+ class="text-red-400 dark:text-red-300 hover:text-red-600 dark:hover:text-red-100 flex-shrink-0"
18
+ >
19
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
20
+ <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
21
+ </svg>
22
+ </button>
23
+ </div>
24
+ </div>
25
+ <div class="py-8 px-4 sm:px-10">
26
+ <div class="space-y-4">
27
+ <button
28
+ type="button"
29
+ @click="signInWithGitHub"
30
+ class="w-full inline-flex items-center justify-center px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-base font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors"
31
+ >
32
+ <svg class="w-6 h-6 mr-3" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
33
+ <path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd" />
34
+ </svg>
35
+ Sign in with GitHub
36
+ </button>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ `,
42
+ emits: ['done'],
43
+ setup(props, { emit }) {
44
+ const errorMessage = ref(null)
45
+
46
+ function signInWithGitHub() {
47
+ // Redirect to GitHub OAuth endpoint
48
+ window.location.href = '/auth/github'
49
+ }
50
+
51
+ return {
52
+ signInWithGitHub,
53
+ errorMessage,
54
+ }
55
+ }
56
+ }
57
+
58
+
59
+ export default {
60
+ install(ctx) {
61
+ // Override SignIn component
62
+ ctx.components({
63
+ SignIn,
64
+ })
65
+ }
66
+ }
llms/llms.json CHANGED
@@ -1,14 +1,5 @@
1
1
  {
2
2
  "version": 3,
3
- "auth": {
4
- "enabled": false,
5
- "github": {
6
- "client_id": "GITHUB_CLIENT_ID",
7
- "client_secret": "GITHUB_CLIENT_SECRET",
8
- "redirect_uri": "http://localhost:8000/auth/github/callback",
9
- "restrict_to": "GITHUB_USERS"
10
- }
11
- },
12
3
  "disable_extensions": [],
13
4
  "defaults": {
14
5
  "headers": {
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, urlencode, urljoin
47
+ from urllib.parse import parse_qs, urljoin
49
48
 
50
49
  import aiohttp
51
50
  from aiohttp import web
@@ -57,8 +56,8 @@ 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.25"
62
61
  DEBUG = os.getenv("DEBUG") == "1"
63
62
  MOCK = os.getenv("MOCK") == "1"
64
63
  MOCK_DIR = os.getenv("MOCK_DIR")
@@ -70,8 +69,6 @@ g_handlers = {}
70
69
  g_verbose = False
71
70
  g_logprefix = ""
72
71
  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
72
  g_app = None # ExtensionsContext Singleton
76
73
 
77
74
 
@@ -2790,8 +2787,36 @@ async def watch_config_files(config_path, providers_path, interval=1):
2790
2787
  pass
2791
2788
 
2792
2789
 
2793
- def get_session_token(request):
2794
- return request.query.get("session") or request.headers.get("X-Session-Token") or request.cookies.get("llms-token")
2790
+ class AuthProvider:
2791
+ def __init__(self, app):
2792
+ self.app = app
2793
+
2794
+ def get_session_token(self, request: web.Request):
2795
+ return (
2796
+ request.query.get("session") or request.headers.get("X-Session-Token") or request.cookies.get("llms-token")
2797
+ )
2798
+
2799
+ def get_session(self, request: web.Request) -> Optional[Dict[str, Any]]:
2800
+ session_token = self.get_session_token(request)
2801
+
2802
+ if not session_token or session_token not in self.app.sessions:
2803
+ return None
2804
+
2805
+ session_data = self.app.sessions[session_token]
2806
+ return session_data
2807
+
2808
+ def get_username(self, request: web.Request) -> Optional[str]:
2809
+ session = self.get_session(request)
2810
+ if session:
2811
+ return session.get("userName")
2812
+ return None
2813
+
2814
+ def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
2815
+ """Check if request is authenticated. Returns (is_authenticated, user_data)"""
2816
+ session = self.get_session(request)
2817
+ if session:
2818
+ return True, session
2819
+ return False, None
2795
2820
 
2796
2821
 
2797
2822
  class AppExtensions:
@@ -2824,6 +2849,9 @@ class AppExtensions:
2824
2849
  self.index_headers = []
2825
2850
  self.index_footers = []
2826
2851
  self.allowed_directories = []
2852
+ self.auth_providers = []
2853
+ self.sessions = {} # OAuth session storage: {session_token: {userId, userName, displayName, profileUrl, email, created}}
2854
+ self.oauth_states = {} # CSRF protection: {state: {created, redirect_uri}}
2827
2855
  self.request_args = {
2828
2856
  "image_config": dict, # e.g. { "aspect_ratio": "1:1" }
2829
2857
  "temperature": float, # e.g: 0.7
@@ -2879,7 +2907,6 @@ class AppExtensions:
2879
2907
 
2880
2908
  def set_config(self, config: Dict[str, Any]):
2881
2909
  self.config = config
2882
- self.auth_enabled = self.config.get("auth", {}).get("enabled", False)
2883
2910
 
2884
2911
  def set_allowed_directories(
2885
2912
  self, directories: List[Annotated[str, "List of absolute paths that are allowed to be accessed."]]
@@ -2897,40 +2924,38 @@ class AppExtensions:
2897
2924
  """Get the list of allowed directories."""
2898
2925
  return self.allowed_directories
2899
2926
 
2900
- # Authentication middleware helper
2901
- def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
2902
- """Check if request is authenticated. Returns (is_authenticated, user_data)"""
2903
- if not self.auth_enabled:
2904
- return True, None
2905
-
2906
- # Check for OAuth session token
2907
- session_token = get_session_token(request)
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"}
2927
+ def add_auth_provider(self, auth_provider: AuthProvider) -> None:
2928
+ """Add an authentication provider."""
2929
+ self.auth_providers.append(auth_provider)
2917
2930
 
2918
- return False, None
2931
+ def is_auth_enabled(self) -> bool:
2932
+ return len(self.auth_providers) > 0
2919
2933
 
2920
2934
  def get_session(self, request: web.Request) -> Optional[Dict[str, Any]]:
2921
- session_token = get_session_token(request)
2935
+ for auth_provider in self.auth_providers:
2936
+ session = auth_provider.get_session(request)
2937
+ if session:
2938
+ return session
2939
+ return None
2922
2940
 
2923
- if not session_token or session_token not in g_sessions:
2941
+ def get_username(self, request: web.Request) -> Optional[str]:
2942
+ for auth_provider in self.auth_providers:
2943
+ username = auth_provider.get_username(request)
2944
+ if username:
2945
+ return username
2924
2946
  return None
2925
2947
 
2926
- session_data = g_sessions[session_token]
2927
- return session_data
2948
+ def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
2949
+ """Check if request is authenticated. Returns (is_authenticated, user_data)"""
2950
+ if len(self.auth_providers) == 0:
2951
+ return True, None
2928
2952
 
2929
- def get_username(self, request: web.Request) -> Optional[str]:
2930
- session = self.get_session(request)
2931
- if session:
2932
- return session.get("userName")
2933
- return None
2953
+ for auth_provider in self.auth_providers:
2954
+ is_authenticated, user_data = auth_provider.check_auth(request)
2955
+ if is_authenticated:
2956
+ return True, user_data
2957
+
2958
+ return False, None
2934
2959
 
2935
2960
  def get_user_path(self, username: Optional[str] = None) -> str:
2936
2961
  if username:
@@ -3032,6 +3057,7 @@ def handler_name(handler):
3032
3057
  class ExtensionContext:
3033
3058
  def __init__(self, app: AppExtensions, path: str):
3034
3059
  self.app = app
3060
+ self.config = app.config
3035
3061
  self.cli_args = app.cli_args
3036
3062
  self.extra_args = app.extra_args
3037
3063
  self.error_auth_required = app.error_auth_required
@@ -3046,8 +3072,24 @@ class ExtensionContext:
3046
3072
  self.verbose = g_verbose
3047
3073
  self.aspect_ratios = app.aspect_ratios
3048
3074
  self.request_args = app.request_args
3075
+ self.sessions = app.sessions
3076
+ self.oauth_states = app.oauth_states
3049
3077
  self.disabled = False
3050
3078
 
3079
+ def add_auth_provider(self, auth_provider: AuthProvider) -> None:
3080
+ """Add an authentication provider."""
3081
+ self.app.add_auth_provider(auth_provider)
3082
+ self.log(f"Added Auth Provider: {auth_provider.__class__.__name__}, Authentication is now enabled")
3083
+
3084
+ def get_session(self, request: web.Request) -> Optional[Dict[str, Any]]:
3085
+ return self.app.get_session(request)
3086
+
3087
+ def get_username(self, request: web.Request) -> Optional[str]:
3088
+ return self.app.get_username(request)
3089
+
3090
+ def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
3091
+ return self.app.check_auth(request)
3092
+
3051
3093
  def set_allowed_directories(
3052
3094
  self, directories: List[Annotated[str, "List of absolute paths that are allowed to be accessed."]]
3053
3095
  ) -> None:
@@ -3129,6 +3171,9 @@ class ExtensionContext:
3129
3171
  def error_response(self, e: Exception, stacktrace: bool = False) -> Dict[str, Any]:
3130
3172
  return to_error_response(e, stacktrace=stacktrace)
3131
3173
 
3174
+ def create_error_response(self, message, error_code="Error", stack_trace=None):
3175
+ return create_error_response(message, error_code, stack_trace)
3176
+
3132
3177
  def add_provider(self, provider: Any):
3133
3178
  self.log(f"Registered provider: {provider.__name__}")
3134
3179
  self.app.all_providers.append(provider)
@@ -3983,33 +4028,11 @@ def cli_exec(cli_args, extra_args):
3983
4028
  port = int(cli_args.serve)
3984
4029
 
3985
4030
  # Validate auth configuration if enabled
3986
- auth_enabled = g_config.get("auth", {}).get("enabled", False)
3987
- if auth_enabled:
3988
- github_config = g_config.get("auth", {}).get("github", {})
3989
- client_id = github_config.get("client_id", "")
3990
- client_secret = github_config.get("client_secret", "")
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")
4031
+ if g_config.get("auth", {}).get("enabled", False):
4032
+ print("ERROR: GitHub Authentication has moved to the github_auth extension.")
4033
+ print("Please remove the auth configuration from llms.json.")
4034
+ print("Learn more: https://llmspy.org/docs/deployment/github-oauth")
4035
+ return ExitCode.FAILED
4013
4036
 
4014
4037
  client_max_size = g_config.get("limits", {}).get(
4015
4038
  "client_max_size", 20 * 1024 * 1024
@@ -4226,236 +4249,6 @@ def cli_exec(cli_args, extra_args):
4226
4249
 
4227
4250
  app.router.add_get("/~cache/{tail:.*}", cache_handler)
4228
4251
 
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
4252
  async def ui_static(request: web.Request) -> web.Response:
4460
4253
  path = Path(request.match_info["path"])
4461
4254
 
@@ -4494,8 +4287,8 @@ def cli_exec(cli_args, extra_args):
4494
4287
  enabled, disabled = provider_status()
4495
4288
  ret["status"] = {"all": list(g_config["providers"].keys()), "enabled": enabled, "disabled": disabled}
4496
4289
  # Add auth configuration
4497
- ret["requiresAuth"] = auth_enabled
4498
- ret["authType"] = "oauth" if auth_enabled else "apikey"
4290
+ ret["requiresAuth"] = g_app.is_auth_enabled()
4291
+ ret["authTypes"] = [provider.__class__.__name__ for provider in g_app.auth_providers]
4499
4292
  return web.json_response(ret)
4500
4293
 
4501
4294
  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
- <TopBar id="top-bar" />
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.24',
9
+ version: '3.0.25',
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
- <OAuthSignIn v-if="$ai.authType === 'oauth'" @done="$ai.signIn($event)" />
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">
@@ -34,7 +34,7 @@ const Avatar = {
34
34
  @click.stop="toggleMenu"
35
35
  :src="$ai.auth.profileUrl"
36
36
  :title="authTitle"
37
- class="size-8 rounded-full cursor-pointer hover:ring-2 hover:ring-gray-300"
37
+ class="mr-1 size-6 rounded-full cursor-pointer hover:ring-2 hover:ring-gray-300"
38
38
  />
39
39
  <div
40
40
  v-if="showMenu"
@@ -112,61 +112,6 @@ const Avatar = {
112
112
  }
113
113
  }
114
114
 
115
- const OAuthSignIn = {
116
- template: `
117
- <div class="min-h-full -mt-36 flex flex-col justify-center sm:px-6 lg:px-8">
118
- <div class="sm:mx-auto sm:w-full sm:max-w-md text-center">
119
- <Welcome />
120
- </div>
121
- <div class="sm:mx-auto sm:w-full sm:max-w-md">
122
- <div v-if="errorMessage" class="mb-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 rounded-lg px-4 py-3">
123
- <div class="flex items-start space-x-2">
124
- <div class="flex-1">
125
- <div class="text-base font-medium">{{ errorMessage }}</div>
126
- </div>
127
- <button type="button"
128
- @click="errorMessage = null"
129
- class="text-red-400 dark:text-red-300 hover:text-red-600 dark:hover:text-red-100 flex-shrink-0"
130
- >
131
- <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
132
- <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
133
- </svg>
134
- </button>
135
- </div>
136
- </div>
137
- <div class="py-8 px-4 sm:px-10">
138
- <div class="space-y-4">
139
- <button
140
- type="button"
141
- @click="signInWithGitHub"
142
- class="w-full inline-flex items-center justify-center px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-base font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors"
143
- >
144
- <svg class="w-6 h-6 mr-3" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
145
- <path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd" />
146
- </svg>
147
- Sign in with GitHub
148
- </button>
149
- </div>
150
- </div>
151
- </div>
152
- </div>
153
- `,
154
- emits: ['done'],
155
- setup(props, { emit }) {
156
- const errorMessage = ref(null)
157
-
158
- function signInWithGitHub() {
159
- // Redirect to GitHub OAuth endpoint
160
- window.location.href = '/auth/github'
161
- }
162
-
163
- return {
164
- signInWithGitHub,
165
- errorMessage,
166
- }
167
- }
168
- }
169
-
170
115
  const SignIn = {
171
116
  template: `
172
117
  <div class="min-h-full -mt-12 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
@@ -263,7 +208,6 @@ export default {
263
208
  Brand,
264
209
  Welcome,
265
210
  Avatar,
266
- OAuthSignIn,
267
211
  SignIn,
268
212
  ErrorViewer,
269
213
  })
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llms-py
3
- Version: 3.0.24
3
+ Version: 3.0.25
4
4
  Summary: A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers
5
5
  Home-page: https://github.com/ServiceStack/llms
6
6
  Author: ServiceStack
@@ -2,8 +2,8 @@ llms/__init__.py,sha256=DKwTZDsyYL_wHe7yvLw49Nf8PSgPSyWaeVdotUqSvrQ,84
2
2
  llms/__main__.py,sha256=hrBulHIt3lmPm1BCyAEVtB6DQ0Hvc3gnIddhHCmJasg,151
3
3
  llms/db.py,sha256=oozp5I5lECVO8oZEFwcZl3ES5mARqWeR1BkoqG5kSqM,11687
4
4
  llms/index.html,sha256=nGk1Djtn9p7l6LuKp4Kg0JIB9fCzxtTWXFfmDb4ggpc,1658
5
- llms/llms.json,sha256=xeIOuraZKs4CwSJ8R2aDAOClLqx6rdEN68O6s51_7cA,11824
6
- llms/main.py,sha256=MBRi98pE7MZ70Iw45vMVEHqeRn6WH4oJFAY5gvjnhFM,184270
5
+ llms/llms.json,sha256=u6qFL_0d6dyQOvHLUbk_8-rkzgkmznOLm4ca_m4faCw,11533
6
+ llms/main.py,sha256=EbmW1JJxrudnx2g6imi3mmiOoWCEJx0bMfQmgNAnjkA,174804
7
7
  llms/providers-extra.json,sha256=_6DmGBiQY9LM6_Y0zOiObYn7ba4g3akSNQfmHcYlENc,11101
8
8
  llms/providers.json,sha256=yls3OUqPIBLSf2rk0xgwUHKkvd-8drGq4JW7w49rEws,299324
9
9
  llms/extensions/analytics/ui/index.mjs,sha256=m1XwaqYCLwK267JAUCAltkN_nOXep0GxfpvGNS5i4_w,69547
@@ -49,6 +49,9 @@ llms/extensions/gallery/README.md,sha256=zif27qiMef1dBboMEPvTJqPDnLvrqntraVSw8oQ
49
49
  llms/extensions/gallery/__init__.py,sha256=24cABEIyqD0zLi32Jrqxoe-qdsS_q4mdJK7NgS0SiEY,1756
50
50
  llms/extensions/gallery/db.py,sha256=IgiwBnqdGf0GYWCltphLD0NWS_MnbPDpRUJszh2_DW0,9032
51
51
  llms/extensions/gallery/ui/index.mjs,sha256=2a6dsZqCoey3So3QLCaHMqYSdzOcLR7ZBi60auFx-24,28356
52
+ llms/extensions/github_auth/README.md,sha256=3WQbs93lw4CXohRgBQCnXJ8I5nDZmYgCwv0HXWPHv7Q,6800
53
+ llms/extensions/github_auth/__init__.py,sha256=7tItmIEuvs-Ev5I0yEVVRqVAPVUxQbjcQxwFXemy30E,9926
54
+ llms/extensions/github_auth/ui/index.mjs,sha256=TR-FJh_g475DcLUzsyAbKbsnTNJoQy6XHNJl5EVG2T4,3544
52
55
  llms/extensions/katex/README.md,sha256=fD_0riNXrKW0SfehifIc3jnXKg9diQdlXVq5X_e1zTc,1473
53
56
  llms/extensions/katex/__init__.py,sha256=CvEe9CPtzoANc4AFuxZTbrJZdggoWLuKnENKfEdncPQ,206
54
57
  llms/extensions/katex/ui/README.md,sha256=Xkjgncuvv4WT4TzNX4muWHYKJOwZ-kSpNnyveLlJDNU,7268
@@ -169,8 +172,8 @@ llms/extensions/system_prompts/ui/index.mjs,sha256=Ec2dXSzEj6RnEuW2U3HxeXK3LTl_l
169
172
  llms/extensions/system_prompts/ui/prompts.json,sha256=t5DD3bird-87wFa4OlW-bC2wdoYDrVzfyc8TO5OaotI,128489
170
173
  llms/extensions/tools/__init__.py,sha256=PRZe0QMfsOymJ3jTqO0VFppNEWI4f2bYSOImK_YrGQM,2036
171
174
  llms/extensions/tools/ui/index.mjs,sha256=1TgCn74oX_rUAhxO8w54HlIgNkHnI5ma-GCqXp-qYVY,39434
172
- llms/ui/App.mjs,sha256=CoUzO9mV__-jV19NKHYIbwHsjWMnO11jyNSbnJhe1gQ,7486
173
- llms/ui/ai.mjs,sha256=SMAvpE5QzcoQvCNaoOptjZX8k8P3kH4L4yEkDTMfP-E,6541
175
+ llms/ui/App.mjs,sha256=8yljf7M7LUp4q7XPEHTCUKpJB3X2d8ePnatRamwTM00,7622
176
+ llms/ui/ai.mjs,sha256=heyruTHUvMpDbSwBJx1sZRtzaQtRQ-ma_u1Cv5g0U1k,6540
174
177
  llms/ui/app.css,sha256=SVVzmFhTd0chuGq5yhv3FjgNudgo6WXlM2fnb-csK4c,190220
175
178
  llms/ui/ctx.mjs,sha256=4x-LTmofhf6OvLThSlDSTQOsLkzyBFOEMRGIOLHszqs,14974
176
179
  llms/ui/fav.svg,sha256=_R6MFeXl6wBFT0lqcUxYQIDWgm246YH_3hSTW0oO8qw,734
@@ -191,14 +194,14 @@ llms/ui/lib/vue-router.min.mjs,sha256=fR30GHoXI1u81zyZ26YEU105pZgbbAKSXbpnzFKIxl
191
194
  llms/ui/lib/vue.min.mjs,sha256=T1TVu9SFEoLArPLKKEp6gsj_eScmHl5TkUebxGK6ldk,166282
192
195
  llms/ui/lib/vue.mjs,sha256=75FuLhUTPk19sncwNIrm0BGEL0_Qw298-_v01fPWYoI,542872
193
196
  llms/ui/modules/icons.mjs,sha256=LGcH0ys0QLS2ZKCO42qHpwPYbBV_EssoWLezU4XZEzU,27751
194
- llms/ui/modules/layout.mjs,sha256=8pAxs8bedQI3b3eRA9nrfpLZznLmrpp4BZvigYAQjpQ,12572
197
+ llms/ui/modules/layout.mjs,sha256=83XCQ0Kzm1lq885PBeB0xDPUAOiTkeVktZlH1KN_XzA,9167
195
198
  llms/ui/modules/model-selector.mjs,sha256=6U4rAZ7vmQELFRQGWk4YEtq02v3lyHdMq6yUOp-ArXg,43184
196
- llms/ui/modules/chat/ChatBody.mjs,sha256=jusiYUxz8NoN6iYCrEayYW5t2wdOx0So79aLxjk7J9c,58187
199
+ llms/ui/modules/chat/ChatBody.mjs,sha256=OyjAQPHNIbdqEhQq01ysbx7Cbt1CezUERbgFpcbnrNI,58081
197
200
  llms/ui/modules/chat/SettingsDialog.mjs,sha256=HMBJTwrapKrRIAstIIqp0QlJL5O-ho4hzgvfagPfsX8,19930
198
201
  llms/ui/modules/chat/index.mjs,sha256=nS_L6G1RSuCybgnA6n-q8Sn3OeSbQWL2iW3-zCIFqJk,39548
199
- llms_py-3.0.24.dist-info/licenses/LICENSE,sha256=bus9cuAOWeYqBk2OuhSABVV1P4z7hgrEFISpyda_H5w,1532
200
- llms_py-3.0.24.dist-info/METADATA,sha256=DIZBWhv5icDwZzyHFqY9up_pq-9Om3JiNpW4GNYpjfA,2195
201
- llms_py-3.0.24.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
202
- llms_py-3.0.24.dist-info/entry_points.txt,sha256=WswyE7PfnkZMIxboC-MS6flBD6wm-CYU7JSUnMhqMfM,40
203
- llms_py-3.0.24.dist-info/top_level.txt,sha256=gC7hk9BKSeog8gyg-EM_g2gxm1mKHwFRfK-10BxOsa4,5
204
- llms_py-3.0.24.dist-info/RECORD,,
202
+ llms_py-3.0.25.dist-info/licenses/LICENSE,sha256=bus9cuAOWeYqBk2OuhSABVV1P4z7hgrEFISpyda_H5w,1532
203
+ llms_py-3.0.25.dist-info/METADATA,sha256=Bbn39CQjmUUq-bdheQOOW3cPBKZc2G9ogdNOnzPmK5w,2195
204
+ llms_py-3.0.25.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
205
+ llms_py-3.0.25.dist-info/entry_points.txt,sha256=WswyE7PfnkZMIxboC-MS6flBD6wm-CYU7JSUnMhqMfM,40
206
+ llms_py-3.0.25.dist-info/top_level.txt,sha256=gC7hk9BKSeog8gyg-EM_g2gxm1mKHwFRfK-10BxOsa4,5
207
+ llms_py-3.0.25.dist-info/RECORD,,