llms-py 2.0.27__py3-none-any.whl → 2.0.28__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llms/llms.json +11 -1
- llms/main.py +125 -4
- llms/ui/Analytics.mjs +12 -4
- llms/ui/Main.mjs +4 -2
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +0 -70
- llms/ui/typography.css +4 -2
- {llms_py-2.0.27.dist-info → llms_py-2.0.28.dist-info}/METADATA +32 -9
- {llms_py-2.0.27.dist-info → llms_py-2.0.28.dist-info}/RECORD +13 -13
- {llms_py-2.0.27.dist-info → llms_py-2.0.28.dist-info}/WHEEL +0 -0
- {llms_py-2.0.27.dist-info → llms_py-2.0.28.dist-info}/entry_points.txt +0 -0
- {llms_py-2.0.27.dist-info → llms_py-2.0.28.dist-info}/licenses/LICENSE +0 -0
- {llms_py-2.0.27.dist-info → llms_py-2.0.28.dist-info}/top_level.txt +0 -0
llms/llms.json
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
"github": {
|
|
5
5
|
"client_id": "$GITHUB_CLIENT_ID",
|
|
6
6
|
"client_secret": "$GITHUB_CLIENT_SECRET",
|
|
7
|
-
"redirect_uri": "http://localhost:8000/auth/github/callback"
|
|
7
|
+
"redirect_uri": "http://localhost:8000/auth/github/callback",
|
|
8
|
+
"restrict_to": "$GITHUB_USERS"
|
|
8
9
|
}
|
|
9
10
|
},
|
|
10
11
|
"defaults": {
|
|
@@ -104,6 +105,15 @@
|
|
|
104
105
|
"stream": false
|
|
105
106
|
}
|
|
106
107
|
},
|
|
108
|
+
"limits": {
|
|
109
|
+
"client_max_size": 20971520
|
|
110
|
+
},
|
|
111
|
+
"convert": {
|
|
112
|
+
"image": {
|
|
113
|
+
"max_size": "1536x1024",
|
|
114
|
+
"max_length": 1572864
|
|
115
|
+
}
|
|
116
|
+
},
|
|
107
117
|
"providers": {
|
|
108
118
|
"openrouter_free": {
|
|
109
119
|
"enabled": true,
|
llms/main.py
CHANGED
|
@@ -15,6 +15,8 @@ import traceback
|
|
|
15
15
|
import sys
|
|
16
16
|
import site
|
|
17
17
|
import secrets
|
|
18
|
+
import re
|
|
19
|
+
from io import BytesIO
|
|
18
20
|
from urllib.parse import parse_qs, urlencode
|
|
19
21
|
|
|
20
22
|
import aiohttp
|
|
@@ -23,7 +25,13 @@ from aiohttp import web
|
|
|
23
25
|
from pathlib import Path
|
|
24
26
|
from importlib import resources # Py≥3.9 (pip install importlib_resources for 3.7/3.8)
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
try:
|
|
29
|
+
from PIL import Image
|
|
30
|
+
HAS_PIL = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
HAS_PIL = False
|
|
33
|
+
|
|
34
|
+
VERSION = "2.0.28"
|
|
27
35
|
_ROOT = None
|
|
28
36
|
g_config_path = None
|
|
29
37
|
g_ui_path = None
|
|
@@ -200,6 +208,77 @@ def price_to_string(price: float | int | str | None) -> str | None:
|
|
|
200
208
|
except (ValueError, TypeError):
|
|
201
209
|
return None
|
|
202
210
|
|
|
211
|
+
def convert_image_if_needed(image_bytes, mimetype='image/png'):
|
|
212
|
+
"""
|
|
213
|
+
Convert and resize image to WebP if it exceeds configured limits.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
image_bytes: Raw image bytes
|
|
217
|
+
mimetype: Original image MIME type
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
tuple: (converted_bytes, new_mimetype) or (original_bytes, original_mimetype) if no conversion needed
|
|
221
|
+
"""
|
|
222
|
+
if not HAS_PIL:
|
|
223
|
+
return image_bytes, mimetype
|
|
224
|
+
|
|
225
|
+
# Get conversion config
|
|
226
|
+
convert_config = g_config.get('convert', {}).get('image', {}) if g_config else {}
|
|
227
|
+
if not convert_config:
|
|
228
|
+
return image_bytes, mimetype
|
|
229
|
+
|
|
230
|
+
max_size_str = convert_config.get('max_size', '1536x1024')
|
|
231
|
+
max_length = convert_config.get('max_length', 1.5*1024*1024) # 1.5MB
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
# Parse max_size (e.g., "1536x1024")
|
|
235
|
+
max_width, max_height = map(int, max_size_str.split('x'))
|
|
236
|
+
|
|
237
|
+
# Open image
|
|
238
|
+
with Image.open(BytesIO(image_bytes)) as img:
|
|
239
|
+
original_width, original_height = img.size
|
|
240
|
+
|
|
241
|
+
# Check if image exceeds limits
|
|
242
|
+
needs_resize = original_width > max_width or original_height > max_height
|
|
243
|
+
|
|
244
|
+
# Check if base64 length would exceed max_length (in KB)
|
|
245
|
+
# Base64 encoding increases size by ~33%, so check raw bytes * 1.33 / 1024
|
|
246
|
+
estimated_kb = (len(image_bytes) * 1.33) / 1024
|
|
247
|
+
needs_conversion = estimated_kb > max_length
|
|
248
|
+
|
|
249
|
+
if not needs_resize and not needs_conversion:
|
|
250
|
+
return image_bytes, mimetype
|
|
251
|
+
|
|
252
|
+
# Convert RGBA to RGB if necessary (WebP doesn't support transparency in RGB mode)
|
|
253
|
+
if img.mode in ('RGBA', 'LA', 'P'):
|
|
254
|
+
# Create a white background
|
|
255
|
+
background = Image.new('RGB', img.size, (255, 255, 255))
|
|
256
|
+
if img.mode == 'P':
|
|
257
|
+
img = img.convert('RGBA')
|
|
258
|
+
background.paste(img, mask=img.split()[-1] if img.mode in ('RGBA', 'LA') else None)
|
|
259
|
+
img = background
|
|
260
|
+
elif img.mode != 'RGB':
|
|
261
|
+
img = img.convert('RGB')
|
|
262
|
+
|
|
263
|
+
# Resize if needed (preserve aspect ratio)
|
|
264
|
+
if needs_resize:
|
|
265
|
+
img.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
|
|
266
|
+
_log(f"Resized image from {original_width}x{original_height} to {img.size[0]}x{img.size[1]}")
|
|
267
|
+
|
|
268
|
+
# Convert to WebP
|
|
269
|
+
output = BytesIO()
|
|
270
|
+
img.save(output, format='WEBP', quality=85, method=6)
|
|
271
|
+
converted_bytes = output.getvalue()
|
|
272
|
+
|
|
273
|
+
_log(f"Converted image to WebP: {len(image_bytes)} bytes -> {len(converted_bytes)} bytes ({len(converted_bytes)*100//len(image_bytes)}%)")
|
|
274
|
+
|
|
275
|
+
return converted_bytes, 'image/webp'
|
|
276
|
+
|
|
277
|
+
except Exception as e:
|
|
278
|
+
_log(f"Error converting image: {e}")
|
|
279
|
+
# Return original if conversion fails
|
|
280
|
+
return image_bytes, mimetype
|
|
281
|
+
|
|
203
282
|
async def process_chat(chat):
|
|
204
283
|
if not chat:
|
|
205
284
|
raise Exception("No chat provided")
|
|
@@ -230,19 +309,31 @@ async def process_chat(chat):
|
|
|
230
309
|
mimetype = get_file_mime_type(get_filename(url))
|
|
231
310
|
if 'Content-Type' in response.headers:
|
|
232
311
|
mimetype = response.headers['Content-Type']
|
|
312
|
+
# convert/resize image if needed
|
|
313
|
+
content, mimetype = convert_image_if_needed(content, mimetype)
|
|
233
314
|
# convert to data uri
|
|
234
315
|
image_url['url'] = f"data:{mimetype};base64,{base64.b64encode(content).decode('utf-8')}"
|
|
235
316
|
elif is_file_path(url):
|
|
236
317
|
_log(f"Reading image: {url}")
|
|
237
318
|
with open(url, "rb") as f:
|
|
238
319
|
content = f.read()
|
|
239
|
-
ext = os.path.splitext(url)[1].lower().lstrip('.') if '.' in url else 'png'
|
|
240
320
|
# get mimetype from file extension
|
|
241
321
|
mimetype = get_file_mime_type(get_filename(url))
|
|
322
|
+
# convert/resize image if needed
|
|
323
|
+
content, mimetype = convert_image_if_needed(content, mimetype)
|
|
242
324
|
# convert to data uri
|
|
243
325
|
image_url['url'] = f"data:{mimetype};base64,{base64.b64encode(content).decode('utf-8')}"
|
|
244
326
|
elif url.startswith('data:'):
|
|
245
|
-
|
|
327
|
+
# Extract existing data URI and process it
|
|
328
|
+
if ';base64,' in url:
|
|
329
|
+
prefix = url.split(';base64,')[0]
|
|
330
|
+
mimetype = prefix.split(':')[1] if ':' in prefix else 'image/png'
|
|
331
|
+
base64_data = url.split(';base64,')[1]
|
|
332
|
+
content = base64.b64decode(base64_data)
|
|
333
|
+
# convert/resize image if needed
|
|
334
|
+
content, mimetype = convert_image_if_needed(content, mimetype)
|
|
335
|
+
# update data uri with potentially converted image
|
|
336
|
+
image_url['url'] = f"data:{mimetype};base64,{base64.b64encode(content).decode('utf-8')}"
|
|
246
337
|
else:
|
|
247
338
|
raise Exception(f"Invalid image: {url}")
|
|
248
339
|
elif item['type'] == 'input_audio' and 'input_audio' in item:
|
|
@@ -1480,7 +1571,9 @@ def main():
|
|
|
1480
1571
|
|
|
1481
1572
|
_log("Authentication enabled - GitHub OAuth configured")
|
|
1482
1573
|
|
|
1483
|
-
|
|
1574
|
+
client_max_size = g_config.get('limits', {}).get('client_max_size', 20*1024*1024) # 20MB max request size (to handle base64 encoding overhead)
|
|
1575
|
+
_log(f"client_max_size set to {client_max_size} bytes ({client_max_size/1024/1024:.1f}MB)")
|
|
1576
|
+
app = web.Application(client_max_size=client_max_size)
|
|
1484
1577
|
|
|
1485
1578
|
# Authentication middleware helper
|
|
1486
1579
|
def check_auth(request):
|
|
@@ -1601,6 +1694,29 @@ def main():
|
|
|
1601
1694
|
auth_url = f"https://github.com/login/oauth/authorize?{urlencode(params)}"
|
|
1602
1695
|
|
|
1603
1696
|
return web.HTTPFound(auth_url)
|
|
1697
|
+
|
|
1698
|
+
def validate_user(github_username):
|
|
1699
|
+
auth_config = g_config['auth']['github']
|
|
1700
|
+
# Check if user is restricted
|
|
1701
|
+
restrict_to = auth_config.get('restrict_to', '')
|
|
1702
|
+
|
|
1703
|
+
# Expand environment variables
|
|
1704
|
+
if restrict_to.startswith('$'):
|
|
1705
|
+
restrict_to = os.environ.get(restrict_to[1:], '')
|
|
1706
|
+
|
|
1707
|
+
# If restrict_to is configured, validate the user
|
|
1708
|
+
if restrict_to:
|
|
1709
|
+
# Parse allowed users (comma or space delimited)
|
|
1710
|
+
allowed_users = [u.strip() for u in re.split(r'[,\s]+', restrict_to) if u.strip()]
|
|
1711
|
+
|
|
1712
|
+
# Check if user is in the allowed list
|
|
1713
|
+
if not github_username or github_username not in allowed_users:
|
|
1714
|
+
_log(f"Access denied for user: {github_username}. Not in allowed list: {allowed_users}")
|
|
1715
|
+
return web.Response(
|
|
1716
|
+
text=f"Access denied. User '{github_username}' is not authorized to access this application.",
|
|
1717
|
+
status=403
|
|
1718
|
+
)
|
|
1719
|
+
return None
|
|
1604
1720
|
|
|
1605
1721
|
async def github_callback_handler(request):
|
|
1606
1722
|
"""Handle GitHub OAuth callback"""
|
|
@@ -1664,6 +1780,11 @@ def main():
|
|
|
1664
1780
|
async with session.get(user_url, headers=headers) as resp:
|
|
1665
1781
|
user_data = await resp.json()
|
|
1666
1782
|
|
|
1783
|
+
# Validate user
|
|
1784
|
+
error_response = validate_user(user_data.get('login', ''))
|
|
1785
|
+
if error_response:
|
|
1786
|
+
return error_response
|
|
1787
|
+
|
|
1667
1788
|
# Create session
|
|
1668
1789
|
session_token = secrets.token_urlsafe(32)
|
|
1669
1790
|
g_sessions[session_token] = {
|
llms/ui/Analytics.mjs
CHANGED
|
@@ -1021,7 +1021,9 @@ export default {
|
|
|
1021
1021
|
|
|
1022
1022
|
// Only display label if percentage > 1%
|
|
1023
1023
|
if (parseFloat(percentage) > 1) {
|
|
1024
|
-
|
|
1024
|
+
// Use white color in dark mode, black in light mode
|
|
1025
|
+
const isDarkMode = document.documentElement.classList.contains('dark')
|
|
1026
|
+
chartCtx.fillStyle = isDarkMode ? '#fff' : '#000'
|
|
1025
1027
|
chartCtx.font = 'bold 12px Arial'
|
|
1026
1028
|
chartCtx.textAlign = 'center'
|
|
1027
1029
|
chartCtx.textBaseline = 'middle'
|
|
@@ -1078,7 +1080,9 @@ export default {
|
|
|
1078
1080
|
|
|
1079
1081
|
// Only display label if percentage > 1%
|
|
1080
1082
|
if (parseFloat(percentage) > 1) {
|
|
1081
|
-
|
|
1083
|
+
// Use white color in dark mode, black in light mode
|
|
1084
|
+
const isDarkMode = document.documentElement.classList.contains('dark')
|
|
1085
|
+
chartCtx.fillStyle = isDarkMode ? '#fff' : '#000'
|
|
1082
1086
|
chartCtx.font = 'bold 12px Arial'
|
|
1083
1087
|
chartCtx.textAlign = 'center'
|
|
1084
1088
|
chartCtx.textBaseline = 'middle'
|
|
@@ -1135,7 +1139,9 @@ export default {
|
|
|
1135
1139
|
|
|
1136
1140
|
// Only display label if percentage > 1%
|
|
1137
1141
|
if (parseFloat(percentage) > 1) {
|
|
1138
|
-
|
|
1142
|
+
// Use white color in dark mode, black in light mode
|
|
1143
|
+
const isDarkMode = document.documentElement.classList.contains('dark')
|
|
1144
|
+
chartCtx.fillStyle = isDarkMode ? '#fff' : '#000'
|
|
1139
1145
|
chartCtx.font = 'bold 12px Arial'
|
|
1140
1146
|
chartCtx.textAlign = 'center'
|
|
1141
1147
|
chartCtx.textBaseline = 'middle'
|
|
@@ -1192,7 +1198,9 @@ export default {
|
|
|
1192
1198
|
|
|
1193
1199
|
// Only display label if percentage > 1%
|
|
1194
1200
|
if (parseFloat(percentage) > 1) {
|
|
1195
|
-
|
|
1201
|
+
// Use white color in dark mode, black in light mode
|
|
1202
|
+
const isDarkMode = document.documentElement.classList.contains('dark')
|
|
1203
|
+
chartCtx.fillStyle = isDarkMode ? '#fff' : '#000'
|
|
1196
1204
|
chartCtx.font = 'bold 12px Arial'
|
|
1197
1205
|
chartCtx.textAlign = 'center'
|
|
1198
1206
|
chartCtx.textBaseline = 'middle'
|
llms/ui/Main.mjs
CHANGED
|
@@ -38,7 +38,6 @@ export default {
|
|
|
38
38
|
<SystemPromptSelector :prompts="prompts" v-model="selectedPrompt"
|
|
39
39
|
:show="showSystemPrompt" @toggle="showSystemPrompt = !showSystemPrompt" />
|
|
40
40
|
<Avatar />
|
|
41
|
-
<DarkModeToggle />
|
|
42
41
|
</div>
|
|
43
42
|
</div>
|
|
44
43
|
</div>
|
|
@@ -63,7 +62,7 @@ export default {
|
|
|
63
62
|
</div>
|
|
64
63
|
|
|
65
64
|
<!-- Export/Import buttons -->
|
|
66
|
-
<div class="mt-2 flex space-x-3 justify-center">
|
|
65
|
+
<div class="mt-2 flex space-x-3 justify-center items-center">
|
|
67
66
|
<button type="button"
|
|
68
67
|
@click="(e) => e.altKey ? exportRequests() : exportThreads()"
|
|
69
68
|
:disabled="isExporting"
|
|
@@ -104,6 +103,9 @@ export default {
|
|
|
104
103
|
@change="handleFileImport"
|
|
105
104
|
class="hidden"
|
|
106
105
|
/>
|
|
106
|
+
|
|
107
|
+
<DarkModeToggle />
|
|
108
|
+
|
|
107
109
|
</div>
|
|
108
110
|
|
|
109
111
|
</div>
|
llms/ui/ai.mjs
CHANGED
llms/ui/app.css
CHANGED
|
@@ -82,7 +82,6 @@
|
|
|
82
82
|
--color-purple-900: oklch(38.1% 0.176 304.987);
|
|
83
83
|
--color-slate-50: oklch(98.4% 0.003 247.858);
|
|
84
84
|
--color-slate-200: oklch(92.9% 0.013 255.508);
|
|
85
|
-
--color-slate-300: oklch(86.9% 0.022 252.894);
|
|
86
85
|
--color-slate-400: oklch(70.4% 0.04 256.788);
|
|
87
86
|
--color-slate-500: oklch(55.4% 0.046 257.417);
|
|
88
87
|
--color-slate-700: oklch(37.2% 0.044 257.287);
|
|
@@ -399,15 +398,9 @@
|
|
|
399
398
|
max-width: 96rem;
|
|
400
399
|
}
|
|
401
400
|
}
|
|
402
|
-
.-m-2 {
|
|
403
|
-
margin: calc(var(--spacing) * -2);
|
|
404
|
-
}
|
|
405
401
|
.-m-2\.5 {
|
|
406
402
|
margin: calc(var(--spacing) * -2.5);
|
|
407
403
|
}
|
|
408
|
-
.-mx-1 {
|
|
409
|
-
margin-inline: calc(var(--spacing) * -1);
|
|
410
|
-
}
|
|
411
404
|
.-mx-1\.5 {
|
|
412
405
|
margin-inline: calc(var(--spacing) * -1.5);
|
|
413
406
|
}
|
|
@@ -420,9 +413,6 @@
|
|
|
420
413
|
.mx-auto {
|
|
421
414
|
margin-inline: auto;
|
|
422
415
|
}
|
|
423
|
-
.-my-1 {
|
|
424
|
-
margin-block: calc(var(--spacing) * -1);
|
|
425
|
-
}
|
|
426
416
|
.-my-1\.5 {
|
|
427
417
|
margin-block: calc(var(--spacing) * -1.5);
|
|
428
418
|
}
|
|
@@ -498,9 +488,6 @@
|
|
|
498
488
|
.-ml-px {
|
|
499
489
|
margin-left: -1px;
|
|
500
490
|
}
|
|
501
|
-
.ml-0 {
|
|
502
|
-
margin-left: calc(var(--spacing) * 0);
|
|
503
|
-
}
|
|
504
491
|
.ml-0\.5 {
|
|
505
492
|
margin-left: calc(var(--spacing) * 0.5);
|
|
506
493
|
}
|
|
@@ -1091,9 +1078,6 @@
|
|
|
1091
1078
|
.border-yellow-400 {
|
|
1092
1079
|
border-color: var(--color-yellow-400);
|
|
1093
1080
|
}
|
|
1094
|
-
.bg-black {
|
|
1095
|
-
background-color: var(--color-black);
|
|
1096
|
-
}
|
|
1097
1081
|
.bg-black\/40 {
|
|
1098
1082
|
background-color: color-mix(in srgb, #000 40%, transparent);
|
|
1099
1083
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -1134,9 +1118,6 @@
|
|
|
1134
1118
|
.bg-gray-400 {
|
|
1135
1119
|
background-color: var(--color-gray-400);
|
|
1136
1120
|
}
|
|
1137
|
-
.bg-gray-500 {
|
|
1138
|
-
background-color: var(--color-gray-500);
|
|
1139
|
-
}
|
|
1140
1121
|
.bg-gray-500\/75 {
|
|
1141
1122
|
background-color: color-mix(in srgb, oklch(55.1% 0.027 264.364) 75%, transparent);
|
|
1142
1123
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -1151,9 +1132,6 @@
|
|
|
1151
1132
|
.bg-gray-700 {
|
|
1152
1133
|
background-color: var(--color-gray-700);
|
|
1153
1134
|
}
|
|
1154
|
-
.bg-gray-900 {
|
|
1155
|
-
background-color: var(--color-gray-900);
|
|
1156
|
-
}
|
|
1157
1135
|
.bg-gray-900\/80 {
|
|
1158
1136
|
background-color: color-mix(in srgb, oklch(21% 0.034 264.665) 80%, transparent);
|
|
1159
1137
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -1204,9 +1182,6 @@
|
|
|
1204
1182
|
.bg-sky-600 {
|
|
1205
1183
|
background-color: var(--color-sky-600);
|
|
1206
1184
|
}
|
|
1207
|
-
.bg-slate-400 {
|
|
1208
|
-
background-color: var(--color-slate-400);
|
|
1209
|
-
}
|
|
1210
1185
|
.bg-slate-400\/10 {
|
|
1211
1186
|
background-color: color-mix(in srgb, oklch(70.4% 0.04 256.788) 10%, transparent);
|
|
1212
1187
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -1269,9 +1244,6 @@
|
|
|
1269
1244
|
.px-6 {
|
|
1270
1245
|
padding-inline: calc(var(--spacing) * 6);
|
|
1271
1246
|
}
|
|
1272
|
-
.py-0 {
|
|
1273
|
-
padding-block: calc(var(--spacing) * 0);
|
|
1274
|
-
}
|
|
1275
1247
|
.py-0\.5 {
|
|
1276
1248
|
padding-block: calc(var(--spacing) * 0.5);
|
|
1277
1249
|
}
|
|
@@ -1302,9 +1274,6 @@
|
|
|
1302
1274
|
.py-12 {
|
|
1303
1275
|
padding-block: calc(var(--spacing) * 12);
|
|
1304
1276
|
}
|
|
1305
|
-
.pt-0 {
|
|
1306
|
-
padding-top: calc(var(--spacing) * 0);
|
|
1307
|
-
}
|
|
1308
1277
|
.pt-0\.5 {
|
|
1309
1278
|
padding-top: calc(var(--spacing) * 0.5);
|
|
1310
1279
|
}
|
|
@@ -1596,23 +1565,12 @@
|
|
|
1596
1565
|
.text-sky-600 {
|
|
1597
1566
|
color: var(--color-sky-600);
|
|
1598
1567
|
}
|
|
1599
|
-
.text-slate-300 {
|
|
1600
|
-
color: var(--color-slate-300);
|
|
1601
|
-
}
|
|
1602
1568
|
.text-slate-500 {
|
|
1603
1569
|
color: var(--color-slate-500);
|
|
1604
1570
|
}
|
|
1605
1571
|
.text-white {
|
|
1606
1572
|
color: var(--color-white);
|
|
1607
1573
|
}
|
|
1608
|
-
.text-white\/70 {
|
|
1609
|
-
color: color-mix(in srgb, #fff 70%, transparent);
|
|
1610
|
-
@supports (color: color-mix(in lab, red, red)) {
|
|
1611
|
-
& {
|
|
1612
|
-
color: color-mix(in oklab, var(--color-white) 70%, transparent);
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
1574
|
.text-yellow-400 {
|
|
1617
1575
|
color: var(--color-yellow-400);
|
|
1618
1576
|
}
|
|
@@ -1631,9 +1589,6 @@
|
|
|
1631
1589
|
.uppercase {
|
|
1632
1590
|
text-transform: uppercase;
|
|
1633
1591
|
}
|
|
1634
|
-
.underline {
|
|
1635
|
-
text-decoration-line: underline;
|
|
1636
|
-
}
|
|
1637
1592
|
.placeholder-gray-500 {
|
|
1638
1593
|
&::placeholder {
|
|
1639
1594
|
color: var(--color-gray-500);
|
|
@@ -1695,9 +1650,6 @@
|
|
|
1695
1650
|
--tw-inset-ring-shadow: inset 0 0 0 1px var(--tw-inset-ring-color, currentcolor);
|
|
1696
1651
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
1697
1652
|
}
|
|
1698
|
-
.ring-black {
|
|
1699
|
-
--tw-ring-color: var(--color-black);
|
|
1700
|
-
}
|
|
1701
1653
|
.ring-black\/5 {
|
|
1702
1654
|
--tw-ring-color: color-mix(in srgb, #000 5%, transparent);
|
|
1703
1655
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -1712,9 +1664,6 @@
|
|
|
1712
1664
|
.ring-indigo-500 {
|
|
1713
1665
|
--tw-ring-color: var(--color-indigo-500);
|
|
1714
1666
|
}
|
|
1715
|
-
.inset-ring-gray-900 {
|
|
1716
|
-
--tw-inset-ring-color: var(--color-gray-900);
|
|
1717
|
-
}
|
|
1718
1667
|
.inset-ring-gray-900\/5 {
|
|
1719
1668
|
--tw-inset-ring-color: color-mix(in srgb, oklch(21% 0.034 264.665) 5%, transparent);
|
|
1720
1669
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -2103,18 +2052,6 @@
|
|
|
2103
2052
|
}
|
|
2104
2053
|
}
|
|
2105
2054
|
}
|
|
2106
|
-
.hover\:bg-white\/20 {
|
|
2107
|
-
&:hover {
|
|
2108
|
-
@media (hover: hover) {
|
|
2109
|
-
background-color: color-mix(in srgb, #fff 20%, transparent);
|
|
2110
|
-
@supports (color: color-mix(in lab, red, red)) {
|
|
2111
|
-
& {
|
|
2112
|
-
background-color: color-mix(in oklab, var(--color-white) 20%, transparent);
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
2055
|
.hover\:bg-yellow-50 {
|
|
2119
2056
|
&:hover {
|
|
2120
2057
|
@media (hover: hover) {
|
|
@@ -2269,13 +2206,6 @@
|
|
|
2269
2206
|
}
|
|
2270
2207
|
}
|
|
2271
2208
|
}
|
|
2272
|
-
.hover\:text-white {
|
|
2273
|
-
&:hover {
|
|
2274
|
-
@media (hover: hover) {
|
|
2275
|
-
color: var(--color-white);
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
2209
|
.hover\:shadow {
|
|
2280
2210
|
&:hover {
|
|
2281
2211
|
@media (hover: hover) {
|
llms/ui/typography.css
CHANGED
|
@@ -94,16 +94,18 @@
|
|
|
94
94
|
border-left-style: solid;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
.dark .prose :
|
|
97
|
+
.dark .prose :where(td):not(:where([class~="not-prose"] *)) {
|
|
98
98
|
color: rgb(209 213 219); /*text-gray-300*/
|
|
99
99
|
}
|
|
100
100
|
.dark .prose :where(h1,h2,h3,h4,h5,h6,th):not(:where([class~="not-prose"] *)) {
|
|
101
101
|
color: rgb(243 244 246); /*text-gray-100*/
|
|
102
102
|
}
|
|
103
|
-
.dark .prose :where(code):not(:where([class~="not-prose"] *))
|
|
103
|
+
.dark .prose :where(code):not(:where([class~="not-prose"] *)),
|
|
104
|
+
.dark .message em {
|
|
104
105
|
background-color: rgb(30 58 138); /*text-blue-900*/
|
|
105
106
|
color: rgb(243 244 246); /*text-gray-100*/
|
|
106
107
|
}
|
|
108
|
+
|
|
107
109
|
.dark .prose :where(pre code):not(:where([class~="not-prose"] *)) {
|
|
108
110
|
background-color: unset;
|
|
109
111
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: llms-py
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.28
|
|
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
|
|
@@ -50,7 +50,7 @@ Configure additional providers and models in [llms.json](llms/llms.json)
|
|
|
50
50
|
|
|
51
51
|
## Features
|
|
52
52
|
|
|
53
|
-
- **Lightweight**: Single [llms.py](https://github.com/ServiceStack/llms/blob/main/llms/main.py) Python file with single `aiohttp` dependency
|
|
53
|
+
- **Lightweight**: Single [llms.py](https://github.com/ServiceStack/llms/blob/main/llms/main.py) Python file with single `aiohttp` dependency (Pillow optional)
|
|
54
54
|
- **Multi-Provider Support**: OpenRouter, Ollama, Anthropic, Google, OpenAI, Grok, Groq, Qwen, Z.ai, Mistral
|
|
55
55
|
- **OpenAI-Compatible API**: Works with any client that supports OpenAI's chat completion API
|
|
56
56
|
- **Built-in Analytics**: Built-in analytics UI to visualize costs, requests, and token usage
|
|
@@ -58,6 +58,7 @@ Configure additional providers and models in [llms.json](llms/llms.json)
|
|
|
58
58
|
- **CLI Interface**: Simple command-line interface for quick interactions
|
|
59
59
|
- **Server Mode**: Run an OpenAI-compatible HTTP server at `http://localhost:{PORT}/v1/chat/completions`
|
|
60
60
|
- **Image Support**: Process images through vision-capable models
|
|
61
|
+
- Auto resizes and converts to webp if exceeds configured limits
|
|
61
62
|
- **Audio Support**: Process audio through audio-capable models
|
|
62
63
|
- **Custom Chat Templates**: Configurable chat completion request templates for different modalities
|
|
63
64
|
- **Auto-Discovery**: Automatically discover available Ollama models
|
|
@@ -68,23 +69,27 @@ Configure additional providers and models in [llms.json](llms/llms.json)
|
|
|
68
69
|
|
|
69
70
|
Access all your local all remote LLMs with a single ChatGPT-like UI:
|
|
70
71
|
|
|
71
|
-
[](https://servicestack.net/posts/llms-py-ui)
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
#### Dark Mode Support
|
|
75
|
+
|
|
76
|
+
[](https://servicestack.net/posts/llms-py-ui)
|
|
77
|
+
|
|
78
|
+
#### Monthly Costs Analysis
|
|
74
79
|
|
|
75
80
|
[](https://servicestack.net/posts/llms-py-ui)
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
#### Monthly Token Usage (Dark Mode)
|
|
78
83
|
|
|
79
|
-
[](https://servicestack.net/posts/llms-py-ui)
|
|
84
|
+
[](https://servicestack.net/posts/llms-py-ui)
|
|
80
85
|
|
|
81
|
-
|
|
86
|
+
#### Monthly Activity Log
|
|
82
87
|
|
|
83
88
|
[](https://servicestack.net/posts/llms-py-ui)
|
|
84
89
|
|
|
85
90
|
[More Features and Screenshots](https://servicestack.net/posts/llms-py-ui).
|
|
86
91
|
|
|
87
|
-
|
|
92
|
+
#### Check Provider Reliability and Response Times
|
|
88
93
|
|
|
89
94
|
Check the status of configured providers to test if they're configured correctly, reachable and what their response times is for the simplest `1+1=` request:
|
|
90
95
|
|
|
@@ -230,6 +235,22 @@ See [DOCKER.md](DOCKER.md) for detailed instructions on customizing configuratio
|
|
|
230
235
|
|
|
231
236
|
llms.py supports optional GitHub OAuth authentication to secure your web UI and API endpoints. When enabled, users must sign in with their GitHub account before accessing the application.
|
|
232
237
|
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"auth": {
|
|
241
|
+
"enabled": true,
|
|
242
|
+
"github": {
|
|
243
|
+
"client_id": "$GITHUB_CLIENT_ID",
|
|
244
|
+
"client_secret": "$GITHUB_CLIENT_SECRET",
|
|
245
|
+
"redirect_uri": "http://localhost:8000/auth/github/callback",
|
|
246
|
+
"restrict_to": "$GITHUB_USERS"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`GITHUB_USERS` is optional but if set will only allow access to the specified users.
|
|
253
|
+
|
|
233
254
|
See [GITHUB_OAUTH_SETUP.md](GITHUB_OAUTH_SETUP.md) for detailed setup instructions.
|
|
234
255
|
|
|
235
256
|
## Configuration
|
|
@@ -243,6 +264,8 @@ The configuration file [llms.json](llms/llms.json) is saved to `~/.llms/llms.jso
|
|
|
243
264
|
- `audio`: Default chat completion request template for audio prompts
|
|
244
265
|
- `file`: Default chat completion request template for file prompts
|
|
245
266
|
- `check`: Check request template for testing provider connectivity
|
|
267
|
+
- `limits`: Override Request size limits
|
|
268
|
+
- `convert`: Max image size and length limits and auto conversion settings
|
|
246
269
|
|
|
247
270
|
### Providers
|
|
248
271
|
|
|
@@ -1211,7 +1234,7 @@ This shows:
|
|
|
1211
1234
|
- `llms/main.py` - Main script with CLI and server functionality
|
|
1212
1235
|
- `llms/llms.json` - Default configuration file
|
|
1213
1236
|
- `llms/ui.json` - UI configuration file
|
|
1214
|
-
- `requirements.txt` - Python dependencies
|
|
1237
|
+
- `requirements.txt` - Python dependencies, required: `aiohttp`, optional: `Pillow`
|
|
1215
1238
|
|
|
1216
1239
|
### Provider Classes
|
|
1217
1240
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
llms/__init__.py,sha256=Mk6eHi13yoUxLlzhwfZ6A1IjsfSQt9ShhOdbLXTvffU,53
|
|
2
2
|
llms/__main__.py,sha256=hrBulHIt3lmPm1BCyAEVtB6DQ0Hvc3gnIddhHCmJasg,151
|
|
3
3
|
llms/index.html,sha256=_pkjdzCX95HTf19LgE4gMh6tLittcnf7M_jL2hSEbbM,3250
|
|
4
|
-
llms/llms.json,sha256=
|
|
5
|
-
llms/main.py,sha256=
|
|
4
|
+
llms/llms.json,sha256=oTMlVM3nYeooQgsPbIGN2LQ-1aq0u1v38sjmxHPiGAc,41331
|
|
5
|
+
llms/main.py,sha256=yAnDzq4qPQ9QqMdK3jnj_2l5GaU6SdofQmfhX6zHRys,88018
|
|
6
6
|
llms/ui.json,sha256=iBOmpNeD5-o8AgUa51ymS-KemovJ7bm9J1fnL0nf8jk,134025
|
|
7
|
-
llms/ui/Analytics.mjs,sha256=
|
|
7
|
+
llms/ui/Analytics.mjs,sha256=r5il9Yvh2lje23e4zbGVUqDu2CG75mtyx84MeMVmHpU,72055
|
|
8
8
|
llms/ui/App.mjs,sha256=95pBXexTt3tgaX-Toh5oS0PdFxrVt3ZU8wu8Ywoz6FI,648
|
|
9
9
|
llms/ui/Avatar.mjs,sha256=TgouwV9bN-Ou1Tf2zCDtVaRiUB21TXZZPFCTlFL-xxQ,3387
|
|
10
10
|
llms/ui/Brand.mjs,sha256=kJn7O4Oo3tbZi87Ifbbcq7AZvhfUqbn_ZcXcyv_WI1A,2075
|
|
11
11
|
llms/ui/ChatPrompt.mjs,sha256=gVKBbxMmvIZWq70LWYBWUgWGemY_pnyMUelI9jRQbsI,25417
|
|
12
|
-
llms/ui/Main.mjs,sha256=
|
|
12
|
+
llms/ui/Main.mjs,sha256=pgJOi7Snllf4nHk95HxwFB38cce5jostwwSJyAvynHQ,42249
|
|
13
13
|
llms/ui/ModelSelector.mjs,sha256=iAFp6ZW2pFfS9L1fn07x8a0rJaqqh8w8Tkxej-10pPw,2520
|
|
14
14
|
llms/ui/OAuthSignIn.mjs,sha256=IdA9Tbswlh74a_-9e9YulOpqLfRpodRLGfCZ9sTZ5jU,4879
|
|
15
15
|
llms/ui/ProviderIcon.mjs,sha256=HTjlgtXEpekn8iNN_S0uswbbvL0iGb20N15-_lXdojk,9054
|
|
@@ -21,13 +21,13 @@ llms/ui/SignIn.mjs,sha256=df3b-7L3ZIneDGbJWUk93K9RGo40gVeuR5StzT1ZH9g,2324
|
|
|
21
21
|
llms/ui/SystemPromptEditor.mjs,sha256=PffkNPV6hGbm1QZBKPI7yvWPZSBL7qla0d-JEJ4mxYo,1466
|
|
22
22
|
llms/ui/SystemPromptSelector.mjs,sha256=bxbJc_Ekqr2d7fO_4fOvMyjL3r60T5B-jQDEuwHBVVo,3151
|
|
23
23
|
llms/ui/Welcome.mjs,sha256=r9j7unF9CF3k7gEQBMRMVsa2oSjgHGNn46Oa5l5BwlY,950
|
|
24
|
-
llms/ui/ai.mjs,sha256=
|
|
25
|
-
llms/ui/app.css,sha256=
|
|
24
|
+
llms/ui/ai.mjs,sha256=Gb72n4mBGdXWKac-thKsVo4gj71CYgmcCZCWWCppxgE,4768
|
|
25
|
+
llms/ui/app.css,sha256=agY_1rQHTaph-9YxlfC6zpQaC-HOI984URQH64hg6Ag,108008
|
|
26
26
|
llms/ui/fav.svg,sha256=_R6MFeXl6wBFT0lqcUxYQIDWgm246YH_3hSTW0oO8qw,734
|
|
27
27
|
llms/ui/markdown.mjs,sha256=uWSyBZZ8a76Dkt53q6CJzxg7Gkx7uayX089td3Srv8w,6388
|
|
28
28
|
llms/ui/tailwind.input.css,sha256=QInTVDpCR89OTzRo9AePdAa-MX3i66RkhNOfa4_7UAg,12086
|
|
29
29
|
llms/ui/threadStore.mjs,sha256=VeGXAuUlA9-Ie9ZzOsay6InKBK_ewWFK6aTRmLTporg,16543
|
|
30
|
-
llms/ui/typography.css,sha256=
|
|
30
|
+
llms/ui/typography.css,sha256=6o7pbMIamRVlm2GfzSStpcOG4T5eFCK_WcQ3RIHKAsU,19587
|
|
31
31
|
llms/ui/utils.mjs,sha256=cYrP17JwpQk7lLqTWNgVTOD_ZZAovbWnx2QSvKzeB24,5333
|
|
32
32
|
llms/ui/lib/chart.js,sha256=dx8FdDX0Rv6OZtZjr9FQh5h-twFsKjfnb-FvFlQ--cU,196176
|
|
33
33
|
llms/ui/lib/charts.mjs,sha256=MNym9qE_2eoH6M7_8Gj9i6e6-Y3b7zw9UQWCUHRF6x0,1088
|
|
@@ -40,9 +40,9 @@ llms/ui/lib/servicestack-vue.mjs,sha256=r_-khYokisXJAIPDLh8Wq6YtcLAY6HNjtJlCZJjL
|
|
|
40
40
|
llms/ui/lib/vue-router.min.mjs,sha256=fR30GHoXI1u81zyZ26YEU105pZgbbAKSXbpnzFKIxls,30418
|
|
41
41
|
llms/ui/lib/vue.min.mjs,sha256=iXh97m5hotl0eFllb3aoasQTImvp7mQoRJ_0HoxmZkw,163811
|
|
42
42
|
llms/ui/lib/vue.mjs,sha256=dS8LKOG01t9CvZ04i0tbFXHqFXOO_Ha4NmM3BytjQAs,537071
|
|
43
|
-
llms_py-2.0.
|
|
44
|
-
llms_py-2.0.
|
|
45
|
-
llms_py-2.0.
|
|
46
|
-
llms_py-2.0.
|
|
47
|
-
llms_py-2.0.
|
|
48
|
-
llms_py-2.0.
|
|
43
|
+
llms_py-2.0.28.dist-info/licenses/LICENSE,sha256=bus9cuAOWeYqBk2OuhSABVV1P4z7hgrEFISpyda_H5w,1532
|
|
44
|
+
llms_py-2.0.28.dist-info/METADATA,sha256=kfED8zRRxV4mDAddg20-0rShqkz4wz5vpsTMmyfvlik,37074
|
|
45
|
+
llms_py-2.0.28.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
46
|
+
llms_py-2.0.28.dist-info/entry_points.txt,sha256=WswyE7PfnkZMIxboC-MS6flBD6wm-CYU7JSUnMhqMfM,40
|
|
47
|
+
llms_py-2.0.28.dist-info/top_level.txt,sha256=gC7hk9BKSeog8gyg-EM_g2gxm1mKHwFRfK-10BxOsa4,5
|
|
48
|
+
llms_py-2.0.28.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|