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.
- llms/extensions/github_auth/README.md +169 -0
- llms/extensions/github_auth/__init__.py +254 -0
- llms/extensions/github_auth/ui/index.mjs +66 -0
- llms/llms.json +0 -9
- llms/main.py +87 -294
- 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.25.dist-info}/METADATA +1 -1
- {llms_py-3.0.24.dist-info → llms_py-3.0.25.dist-info}/RECORD +15 -12
- {llms_py-3.0.24.dist-info → llms_py-3.0.25.dist-info}/WHEEL +0 -0
- {llms_py-3.0.24.dist-info → llms_py-3.0.25.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.24.dist-info → llms_py-3.0.25.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.24.dist-info → llms_py-3.0.25.dist-info}/top_level.txt +0 -0
|
@@ -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,
|
|
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
|
-
|
|
2794
|
-
|
|
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
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2927
|
-
|
|
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
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
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
|
-
|
|
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")
|
|
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"] =
|
|
4498
|
-
ret["
|
|
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
|
-
<
|
|
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.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
|
-
<
|
|
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">
|
llms/ui/modules/layout.mjs
CHANGED
|
@@ -34,7 +34,7 @@ const Avatar = {
|
|
|
34
34
|
@click.stop="toggleMenu"
|
|
35
35
|
:src="$ai.auth.profileUrl"
|
|
36
36
|
:title="authTitle"
|
|
37
|
-
class="size-
|
|
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.
|
|
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=
|
|
6
|
-
llms/main.py,sha256=
|
|
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=
|
|
173
|
-
llms/ui/ai.mjs,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
200
|
-
llms_py-3.0.
|
|
201
|
-
llms_py-3.0.
|
|
202
|
-
llms_py-3.0.
|
|
203
|
-
llms_py-3.0.
|
|
204
|
-
llms_py-3.0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|