lemonade-sdk 8.1.10__py3-none-any.whl → 8.1.12__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.
Potentially problematic release.
This version of lemonade-sdk might be problematic. Click here for more details.
- lemonade/cache.py +6 -1
- lemonade/common/status.py +4 -4
- lemonade/tools/bench.py +22 -1
- lemonade/tools/flm/__init__.py +1 -0
- lemonade/tools/flm/utils.py +255 -0
- lemonade/tools/llamacpp/bench.py +111 -23
- lemonade/tools/llamacpp/load.py +20 -1
- lemonade/tools/llamacpp/utils.py +210 -17
- lemonade/tools/oga/bench.py +0 -26
- lemonade/tools/report/table.py +6 -0
- lemonade/tools/server/flm.py +133 -0
- lemonade/tools/server/llamacpp.py +23 -5
- lemonade/tools/server/serve.py +260 -135
- lemonade/tools/server/static/js/chat.js +165 -82
- lemonade/tools/server/static/js/models.js +87 -54
- lemonade/tools/server/static/js/shared.js +9 -6
- lemonade/tools/server/static/logs.html +57 -0
- lemonade/tools/server/static/styles.css +159 -8
- lemonade/tools/server/static/webapp.html +28 -10
- lemonade/tools/server/tray.py +94 -38
- lemonade/tools/server/utils/macos_tray.py +226 -0
- lemonade/tools/server/utils/{system_tray.py → windows_tray.py} +13 -0
- lemonade/tools/server/webapp.py +4 -1
- lemonade/tools/server/wrapped_server.py +91 -25
- lemonade/version.py +1 -1
- lemonade_install/install.py +25 -2
- {lemonade_sdk-8.1.10.dist-info → lemonade_sdk-8.1.12.dist-info}/METADATA +10 -6
- {lemonade_sdk-8.1.10.dist-info → lemonade_sdk-8.1.12.dist-info}/RECORD +37 -32
- lemonade_server/cli.py +103 -14
- lemonade_server/model_manager.py +186 -45
- lemonade_server/pydantic_models.py +25 -1
- lemonade_server/server_models.json +175 -62
- {lemonade_sdk-8.1.10.dist-info → lemonade_sdk-8.1.12.dist-info}/WHEEL +0 -0
- {lemonade_sdk-8.1.10.dist-info → lemonade_sdk-8.1.12.dist-info}/entry_points.txt +0 -0
- {lemonade_sdk-8.1.10.dist-info → lemonade_sdk-8.1.12.dist-info}/licenses/LICENSE +0 -0
- {lemonade_sdk-8.1.10.dist-info → lemonade_sdk-8.1.12.dist-info}/licenses/NOTICE.md +0 -0
- {lemonade_sdk-8.1.10.dist-info → lemonade_sdk-8.1.12.dist-info}/top_level.txt +0 -0
|
@@ -65,11 +65,12 @@ function showErrorBanner(msg) {
|
|
|
65
65
|
const banner = document.getElementById('error-banner');
|
|
66
66
|
if (!banner) return;
|
|
67
67
|
const msgEl = document.getElementById('error-banner-msg');
|
|
68
|
-
const
|
|
68
|
+
const logsUrl = window.location.origin + '/static/logs.html';
|
|
69
|
+
const fullMsg = `${msg} <br>Check the Lemonade Server logs <a href="${logsUrl}" target="_blank" rel="noopener noreferrer">on the browser</a> or via the system tray app for more information.`;
|
|
69
70
|
if (msgEl) {
|
|
70
|
-
msgEl.
|
|
71
|
+
msgEl.innerHTML = fullMsg;
|
|
71
72
|
} else {
|
|
72
|
-
banner.
|
|
73
|
+
banner.innerHTML = fullMsg;
|
|
73
74
|
}
|
|
74
75
|
banner.style.display = 'flex';
|
|
75
76
|
}
|
|
@@ -181,7 +182,7 @@ async function loadModelStandardized(modelId, options = {}) {
|
|
|
181
182
|
// Update load button if provided
|
|
182
183
|
if (loadButton) {
|
|
183
184
|
loadButton.disabled = true;
|
|
184
|
-
loadButton.textContent = '
|
|
185
|
+
loadButton.textContent = '⏳';
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
// Update status indicator to show loading state
|
|
@@ -246,7 +247,8 @@ async function loadModelStandardized(modelId, options = {}) {
|
|
|
246
247
|
// Reset load button if provided
|
|
247
248
|
if (loadButton) {
|
|
248
249
|
loadButton.disabled = false;
|
|
249
|
-
loadButton.textContent = '
|
|
250
|
+
loadButton.textContent = '🚀';
|
|
251
|
+
loadButton.classList.remove('loading');
|
|
250
252
|
}
|
|
251
253
|
|
|
252
254
|
// Reset chat controls
|
|
@@ -280,7 +282,8 @@ async function loadModelStandardized(modelId, options = {}) {
|
|
|
280
282
|
// Reset load button if provided
|
|
281
283
|
if (loadButton) {
|
|
282
284
|
loadButton.disabled = false;
|
|
283
|
-
loadButton.textContent = '
|
|
285
|
+
loadButton.textContent = '🚀';
|
|
286
|
+
loadButton.classList.remove('loading');
|
|
284
287
|
}
|
|
285
288
|
|
|
286
289
|
// Reset status indicator on error
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Lemonade Server Logs</title>
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
font-family: monospace;
|
|
9
|
+
background: #1e1e1e;
|
|
10
|
+
color: #d4d4d4;
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
}
|
|
14
|
+
#log-container {
|
|
15
|
+
padding: 10px;
|
|
16
|
+
height: 100vh;
|
|
17
|
+
overflow-y: auto;
|
|
18
|
+
white-space: pre-wrap;
|
|
19
|
+
}
|
|
20
|
+
</style>
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
<div id="log-container"></div>
|
|
24
|
+
|
|
25
|
+
<script>
|
|
26
|
+
function stripAnsi(str) {
|
|
27
|
+
return str.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const logContainer = document.getElementById("log-container");
|
|
31
|
+
const ws = new WebSocket(`ws://${location.host}/api/v1/logs/ws`);
|
|
32
|
+
|
|
33
|
+
function isNearBottom() {
|
|
34
|
+
const threshold = 50; // px from bottom
|
|
35
|
+
return logContainer.scrollTop + logContainer.clientHeight >= logContainer.scrollHeight - threshold;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ws.onmessage = (event) => {
|
|
39
|
+
const line = document.createElement("div");
|
|
40
|
+
line.textContent = stripAnsi(event.data);
|
|
41
|
+
logContainer.appendChild(line);
|
|
42
|
+
|
|
43
|
+
// Only autoscroll if the user is already at (or near) the bottom
|
|
44
|
+
if (isNearBottom()) {
|
|
45
|
+
logContainer.scrollTop = logContainer.scrollHeight;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
ws.onclose = () => {
|
|
50
|
+
const msg = document.createElement("div");
|
|
51
|
+
msg.textContent = "[Disconnected from log stream]";
|
|
52
|
+
msg.style.color = "red";
|
|
53
|
+
logContainer.appendChild(msg);
|
|
54
|
+
};
|
|
55
|
+
</script>
|
|
56
|
+
</body>
|
|
57
|
+
</html>
|
|
@@ -99,6 +99,15 @@ body::before {
|
|
|
99
99
|
.brand-title a {
|
|
100
100
|
color: inherit;
|
|
101
101
|
text-decoration: none;
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
gap: 0.5rem;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.brand-icon {
|
|
108
|
+
width: 1.5rem;
|
|
109
|
+
height: 1.5rem;
|
|
110
|
+
vertical-align: middle;
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
.navbar-links {
|
|
@@ -167,6 +176,9 @@ body::before {
|
|
|
167
176
|
width: 100%;
|
|
168
177
|
margin-left: 1rem;
|
|
169
178
|
margin-right: 1rem;
|
|
179
|
+
/* Removing only the bottom border and shadow for the content tab gap to look nicer */
|
|
180
|
+
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0);
|
|
181
|
+
border-bottom: none;
|
|
170
182
|
}
|
|
171
183
|
|
|
172
184
|
.tabs {
|
|
@@ -213,14 +225,24 @@ body::before {
|
|
|
213
225
|
position: relative;
|
|
214
226
|
}
|
|
215
227
|
|
|
228
|
+
/* Wrapper for the select element with embedded status light */
|
|
229
|
+
.model-select-wrapper {
|
|
230
|
+
position: relative;
|
|
231
|
+
display: flex;
|
|
232
|
+
align-items: center;
|
|
233
|
+
}
|
|
234
|
+
|
|
216
235
|
.status-light {
|
|
217
236
|
width: 8px;
|
|
218
237
|
height: 8px;
|
|
219
238
|
border-radius: 50%;
|
|
220
|
-
position:
|
|
239
|
+
position: absolute;
|
|
240
|
+
left: 8px;
|
|
241
|
+
top: 50%;
|
|
242
|
+
transform: translateY(-50%);
|
|
221
243
|
transition: all var(--transition-fast);
|
|
222
244
|
flex-shrink: 0;
|
|
223
|
-
|
|
245
|
+
z-index: 10;
|
|
224
246
|
}
|
|
225
247
|
|
|
226
248
|
.status-light::before {
|
|
@@ -288,7 +310,7 @@ body::before {
|
|
|
288
310
|
|
|
289
311
|
/* Base styles for the select element */
|
|
290
312
|
.model-select {
|
|
291
|
-
padding: 0.5rem 0.75rem;
|
|
313
|
+
padding: 0.5rem 0.75rem 0.5rem 1.5rem;
|
|
292
314
|
border: 1px solid #ddd;
|
|
293
315
|
border-radius: 6px;
|
|
294
316
|
background: #fafafa;
|
|
@@ -391,11 +413,18 @@ button:disabled {
|
|
|
391
413
|
display: flex;
|
|
392
414
|
}
|
|
393
415
|
|
|
416
|
+
.tab-content-wrapper {
|
|
417
|
+
width: 85%;
|
|
418
|
+
}
|
|
419
|
+
|
|
394
420
|
.tab-content {
|
|
395
421
|
display: none;
|
|
396
422
|
padding: 2em;
|
|
397
423
|
background: #fafafa;
|
|
398
424
|
border-radius: 0 0 8px 8px;
|
|
425
|
+
/* adding border and shadow that was removed for the gap look from higher div */
|
|
426
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
427
|
+
border: 1px solid #e0e0e0;
|
|
399
428
|
}
|
|
400
429
|
|
|
401
430
|
.tab-content.active {
|
|
@@ -406,7 +435,9 @@ button:disabled {
|
|
|
406
435
|
.chat-container {
|
|
407
436
|
display: flex;
|
|
408
437
|
flex-direction: column;
|
|
409
|
-
|
|
438
|
+
/* Max space available in viewport
|
|
439
|
+
This also prevents the chat-history section to resize lower than a certain point */
|
|
440
|
+
min-height: calc(100vh - 550px);
|
|
410
441
|
min-width: 300px;
|
|
411
442
|
max-width: 100%;
|
|
412
443
|
width: 100%;
|
|
@@ -414,18 +445,37 @@ button:disabled {
|
|
|
414
445
|
border: 1px solid #e0e0e0;
|
|
415
446
|
border-radius: 8px;
|
|
416
447
|
background: #fff;
|
|
417
|
-
|
|
418
|
-
|
|
448
|
+
/* Use a semi-fixed chat 'window' height so streaming content never expands the page.
|
|
449
|
+
The chat-history area inside will scroll. The CSS variable allows easy tuning. */
|
|
450
|
+
--chat-height: 520px;
|
|
451
|
+
/* Allow vertical resizing by the user while keeping horizontal size fixed.
|
|
452
|
+
Constrain the resize with min/max heights so it stays usable and doesn't overflow the viewport. */
|
|
453
|
+
resize: vertical;
|
|
454
|
+
height: var(--chat-height);
|
|
455
|
+
max-height: calc(100vh - 120px);
|
|
456
|
+
overflow: hidden; /* hide overflow at container level; chat-history will scroll */
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/* Responsive fallback: if the viewport height is small, cap the chat window to fit */
|
|
460
|
+
@media (max-height: 700px) {
|
|
461
|
+
.chat-container {
|
|
462
|
+
height: calc(100vh - 180px); /* leave space for navbar/footer */
|
|
463
|
+
}
|
|
419
464
|
}
|
|
420
465
|
|
|
421
466
|
.chat-history {
|
|
422
|
-
|
|
467
|
+
/* Make chat history take remaining space in the chat container and scroll when content
|
|
468
|
+
exceeds this space. This prevents streaming text from expanding the overall layout. */
|
|
469
|
+
flex: 1 1 auto;
|
|
423
470
|
overflow-y: auto;
|
|
471
|
+
-webkit-overflow-scrolling: touch;
|
|
424
472
|
padding: 1em;
|
|
425
473
|
border-bottom: 1px solid #e0e0e0;
|
|
426
474
|
display: flex;
|
|
427
475
|
flex-direction: column;
|
|
428
476
|
gap: 0.5em;
|
|
477
|
+
/* Optional visual hint for scrollable content */
|
|
478
|
+
scrollbar-width: thin;
|
|
429
479
|
}
|
|
430
480
|
|
|
431
481
|
.chat-message {
|
|
@@ -1492,6 +1542,11 @@ button:disabled {
|
|
|
1492
1542
|
font-size: 1.3rem;
|
|
1493
1543
|
}
|
|
1494
1544
|
|
|
1545
|
+
.brand-icon {
|
|
1546
|
+
width: 1.3rem;
|
|
1547
|
+
height: 1.3rem;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1495
1550
|
.navbar-links {
|
|
1496
1551
|
gap: 1.5rem;
|
|
1497
1552
|
font-size: 1rem;
|
|
@@ -1507,6 +1562,11 @@ button:disabled {
|
|
|
1507
1562
|
font-size: 1.2rem;
|
|
1508
1563
|
}
|
|
1509
1564
|
|
|
1565
|
+
.brand-icon {
|
|
1566
|
+
width: 1.2rem;
|
|
1567
|
+
height: 1.2rem;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1510
1570
|
.navbar-links {
|
|
1511
1571
|
gap: 1rem;
|
|
1512
1572
|
font-size: 0.9rem;
|
|
@@ -1838,6 +1898,53 @@ button:disabled {
|
|
|
1838
1898
|
align-items: center;
|
|
1839
1899
|
}
|
|
1840
1900
|
|
|
1901
|
+
/* FastFlowLM notice styles */
|
|
1902
|
+
.flm-notice {
|
|
1903
|
+
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
|
|
1904
|
+
border: 1px solid #ffeaa7;
|
|
1905
|
+
border-left: 4px solid #f39c12;
|
|
1906
|
+
border-radius: 8px;
|
|
1907
|
+
margin-bottom: 1.5rem;
|
|
1908
|
+
box-shadow: 0 2px 8px rgba(243, 156, 18, 0.1);
|
|
1909
|
+
animation: fadeIn 0.3s ease;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
.flm-notice-content {
|
|
1913
|
+
display: flex;
|
|
1914
|
+
align-items: flex-start;
|
|
1915
|
+
gap: 0.75rem;
|
|
1916
|
+
padding: 1rem 1.25rem;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
.flm-notice-icon {
|
|
1920
|
+
font-size: 1.2rem;
|
|
1921
|
+
flex-shrink: 0;
|
|
1922
|
+
margin-top: 0.1rem;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
.flm-notice-text {
|
|
1926
|
+
flex: 1;
|
|
1927
|
+
font-size: 0.95rem;
|
|
1928
|
+
line-height: 1.5;
|
|
1929
|
+
color: #856404;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
.flm-notice-text strong {
|
|
1933
|
+
color: #6c5ce7;
|
|
1934
|
+
font-weight: 600;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
.flm-notice-text a {
|
|
1938
|
+
color: var(--info-primary);
|
|
1939
|
+
text-decoration: none;
|
|
1940
|
+
font-weight: 500;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
.flm-notice-text a:hover {
|
|
1944
|
+
text-decoration: underline;
|
|
1945
|
+
color: var(--info-hover);
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1841
1948
|
.error-banner .close-btn {
|
|
1842
1949
|
background: none;
|
|
1843
1950
|
border: none;
|
|
@@ -2275,4 +2382,48 @@ button:disabled {
|
|
|
2275
2382
|
0 8px 25px rgba(200, 88, 108, 0.2),
|
|
2276
2383
|
0 3px 10px rgba(0, 0, 0, 0.1),
|
|
2277
2384
|
inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
|
2278
|
-
}
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
|
|
2388
|
+
/* Dropdown styling */
|
|
2389
|
+
.dropdown {
|
|
2390
|
+
position: relative;
|
|
2391
|
+
display: inline-block;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
.dropbtn {
|
|
2395
|
+
background-color: transparent;
|
|
2396
|
+
border: none;
|
|
2397
|
+
font-size: 16px;
|
|
2398
|
+
cursor: pointer;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
.dropdown-content {
|
|
2402
|
+
display: none;
|
|
2403
|
+
position: absolute;
|
|
2404
|
+
right: 0; /* align to the right edge of the parent */
|
|
2405
|
+
left: auto; /* prevent left alignment */
|
|
2406
|
+
top: calc(100% + 1px); /* opens 8px below the button */
|
|
2407
|
+
background-color: #ffe76c;
|
|
2408
|
+
border-radius: 8px;
|
|
2409
|
+
min-width: 140px;
|
|
2410
|
+
box-shadow: 0px 8px 16px rgba(0,0,0,0.2);
|
|
2411
|
+
z-index: 1000;
|
|
2412
|
+
overflow: hidden;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
.dropdown-content a {
|
|
2416
|
+
color: black;
|
|
2417
|
+
font-size: 12px; /* smaller font */
|
|
2418
|
+
padding: 8px 12px;
|
|
2419
|
+
text-decoration: none;
|
|
2420
|
+
display: block;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
.dropdown-content a:hover {
|
|
2424
|
+
background-color: #f6c146;
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
.dropdown:hover .dropdown-content {
|
|
2428
|
+
display: block;
|
|
2429
|
+
}
|
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
window.SERVER_PORT = {{SERVER_PORT}};
|
|
11
11
|
</script>
|
|
12
12
|
{{SERVER_MODELS_JS}}
|
|
13
|
+
{{PLATFORM_JS}}
|
|
13
14
|
</head>
|
|
14
15
|
<body>
|
|
15
16
|
<nav class="navbar" id="navbar">
|
|
16
17
|
<div class="navbar-brand">
|
|
17
|
-
<span class="brand-title"><a href="https://lemonade-server.ai"
|
|
18
|
-
</div>
|
|
18
|
+
<span class="brand-title"><a href="https://lemonade-server.ai"><img src="/static/favicon.ico" alt="🍋" class="brand-icon"> Lemonade Server</a></span>
|
|
19
|
+
</div>
|
|
19
20
|
<div class="navbar-links">
|
|
20
21
|
<a href="https://github.com/lemonade-sdk/lemonade" target="_blank">GitHub</a>
|
|
21
22
|
<a href="https://lemonade-server.ai/docs/" target="_blank">Docs</a>
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
<button class="close-btn" onclick="hideErrorBanner()">×</button>
|
|
30
31
|
</div>
|
|
31
32
|
<main class="main">
|
|
33
|
+
<div class="tab-content-wrapper">
|
|
32
34
|
<div class="tab-container">
|
|
33
35
|
<div class="tabs">
|
|
34
36
|
<div class="tab-group">
|
|
@@ -38,23 +40,32 @@
|
|
|
38
40
|
</div>
|
|
39
41
|
|
|
40
42
|
<div class="model-status-indicator" id="model-status-indicator">
|
|
41
|
-
<div class="
|
|
42
|
-
|
|
43
|
-
<
|
|
44
|
-
|
|
43
|
+
<div class="model-select-wrapper">
|
|
44
|
+
<div class="status-light" id="status-light"></div>
|
|
45
|
+
<select id="model-select" class="model-select">
|
|
46
|
+
<option value="">Pick a model</option>
|
|
47
|
+
</select>
|
|
48
|
+
</div>
|
|
45
49
|
<button class="model-action-btn" id="model-unload-btn" title="Unload model" style="display: flex;">⏏</button>
|
|
50
|
+
<!-- Dropdown -->
|
|
51
|
+
<div class="dropdown">
|
|
52
|
+
<button class="dropbtn" aria-label="Dropdown" style="display: flex;">🛠️</button>
|
|
53
|
+
<div class="dropdown-content">
|
|
54
|
+
<a href="/static/logs.html" target="_blank">View Logs</a>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
46
57
|
</div>
|
|
47
|
-
</div>
|
|
58
|
+
</div>
|
|
48
59
|
<div class="tab-content active" id="content-chat">
|
|
49
60
|
<div class="chat-container">
|
|
50
|
-
<div class="chat-history" id="chat-history"></div>
|
|
61
|
+
<div class="chat-history" id="chat-history" style="overflow-y: auto;"></div>
|
|
51
62
|
<div class="chat-input-row">
|
|
52
63
|
<div class="input-with-indicator">
|
|
53
64
|
<textarea id="chat-input" placeholder="Type your message..." rows="1"></textarea>
|
|
54
65
|
</div>
|
|
55
66
|
<input type="file" id="file-attachment" style="display: none;" multiple accept="image/*">
|
|
56
67
|
<button id="attachment-btn" title="Attach files">📎</button>
|
|
57
|
-
<button id="
|
|
68
|
+
<button id="toggle-btn" title="Start">Start</button>
|
|
58
69
|
</div>
|
|
59
70
|
<div class="attachments-preview-container" id="attachments-preview-container">
|
|
60
71
|
<div class="attachments-preview-row" id="attachments-preview-row"></div>
|
|
@@ -150,6 +161,7 @@
|
|
|
150
161
|
<div class="subcategory" data-recipe="oga-hybrid" onclick="selectRecipe('oga-hybrid')">OGA Hybrid</div>
|
|
151
162
|
<div class="subcategory" data-recipe="oga-npu" onclick="selectRecipe('oga-npu')">OGA NPU</div>
|
|
152
163
|
<div class="subcategory" data-recipe="oga-cpu" onclick="selectRecipe('oga-cpu')">OGA CPU</div>
|
|
164
|
+
<div class="subcategory" data-recipe="flm" onclick="selectRecipe('flm')">FastFlowLM NPU</div>
|
|
153
165
|
</div>
|
|
154
166
|
</div>
|
|
155
167
|
<div class="model-category-section">
|
|
@@ -204,6 +216,7 @@
|
|
|
204
216
|
</label>
|
|
205
217
|
<select id="register-recipe" name="recipe" required>
|
|
206
218
|
<option value="llamacpp">llamacpp</option>
|
|
219
|
+
<option value="flm">flm</option>
|
|
207
220
|
<option value="oga-npu">oga-npu</option>
|
|
208
221
|
<option value="oga-hybrid">oga-hybrid</option>
|
|
209
222
|
<option value="oga-cpu">oga-cpu</option>
|
|
@@ -221,6 +234,11 @@
|
|
|
221
234
|
Reasoning
|
|
222
235
|
<span class="tooltip-icon" data-tooltip="Enable to inform Lemonade Server that the model has reasoning capabilities that will use thinking tokens.">ⓘ</span>
|
|
223
236
|
</label>
|
|
237
|
+
<label class="register-label reasoning-inline">
|
|
238
|
+
<input type="checkbox" id="register-vision" name="vision">
|
|
239
|
+
Vision
|
|
240
|
+
<span class="tooltip-icon" data-tooltip="Enable to inform Lemonade Server that the model has vision capabilities for processing images.">ⓘ</span>
|
|
241
|
+
</label>
|
|
224
242
|
</div>
|
|
225
243
|
<div class="register-form-row register-form-row-tight">
|
|
226
244
|
<button type="submit" id="register-submit">Install</button>
|
|
@@ -231,6 +249,7 @@
|
|
|
231
249
|
</div>
|
|
232
250
|
</div>
|
|
233
251
|
</div>
|
|
252
|
+
</div>
|
|
234
253
|
</div>
|
|
235
254
|
</main>
|
|
236
255
|
<footer class="site-footer">
|
|
@@ -250,4 +269,3 @@
|
|
|
250
269
|
<script src="/static/js/chat.js"></script>
|
|
251
270
|
</body>
|
|
252
271
|
</html>
|
|
253
|
-
</html>
|
lemonade/tools/server/tray.py
CHANGED
|
@@ -7,6 +7,7 @@ import webbrowser
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
import logging
|
|
9
9
|
import tempfile
|
|
10
|
+
import platform
|
|
10
11
|
|
|
11
12
|
import requests
|
|
12
13
|
from packaging.version import parse as parse_version
|
|
@@ -14,7 +15,20 @@ from packaging.version import parse as parse_version
|
|
|
14
15
|
from lemonade_server.pydantic_models import DEFAULT_CTX_SIZE
|
|
15
16
|
|
|
16
17
|
from lemonade.version import __version__
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
# Import the appropriate tray implementation based on platform
|
|
20
|
+
if platform.system() == "Darwin": # macOS
|
|
21
|
+
from lemonade.tools.server.utils.macos_tray import (
|
|
22
|
+
MacOSSystemTray as SystemTray,
|
|
23
|
+
Menu,
|
|
24
|
+
MenuItem,
|
|
25
|
+
)
|
|
26
|
+
else: # Windows/Linux
|
|
27
|
+
from lemonade.tools.server.utils.windows_tray import (
|
|
28
|
+
SystemTray,
|
|
29
|
+
Menu,
|
|
30
|
+
MenuItem,
|
|
31
|
+
)
|
|
18
32
|
|
|
19
33
|
|
|
20
34
|
class OutputDuplicator:
|
|
@@ -87,6 +101,9 @@ class LemonadeTray(SystemTray):
|
|
|
87
101
|
self.version_check_thread = None
|
|
88
102
|
self.stop_version_check = threading.Event()
|
|
89
103
|
|
|
104
|
+
# Hook function for platform-specific initialization callback
|
|
105
|
+
self.on_ready = None
|
|
106
|
+
|
|
90
107
|
def get_latest_version(self):
|
|
91
108
|
"""
|
|
92
109
|
Update the latest version information.
|
|
@@ -191,15 +208,38 @@ class LemonadeTray(SystemTray):
|
|
|
191
208
|
Show the log file in a new window.
|
|
192
209
|
"""
|
|
193
210
|
try:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
211
|
+
system = platform.system().lower()
|
|
212
|
+
if system == "darwin":
|
|
213
|
+
# Use Terminal.app to show live logs on macOS
|
|
214
|
+
try:
|
|
215
|
+
subprocess.Popen(
|
|
216
|
+
[
|
|
217
|
+
"osascript",
|
|
218
|
+
"-e",
|
|
219
|
+
f'tell application "Terminal" to do script "tail -f {self.log_file}"',
|
|
220
|
+
]
|
|
221
|
+
)
|
|
222
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
223
|
+
self.logger.error(f"Failed to open Terminal for logs: {e}")
|
|
224
|
+
self.show_balloon_notification(
|
|
225
|
+
"Error",
|
|
226
|
+
f"Failed to open logs in Terminal. Log file: {self.log_file}",
|
|
227
|
+
)
|
|
228
|
+
elif system == "windows":
|
|
229
|
+
# Use PowerShell on Windows
|
|
230
|
+
subprocess.Popen(
|
|
231
|
+
[
|
|
232
|
+
"powershell",
|
|
233
|
+
"Start-Process",
|
|
234
|
+
"powershell",
|
|
235
|
+
"-ArgumentList",
|
|
236
|
+
f'"-NoExit", "Get-Content -Wait {self.log_file}"',
|
|
237
|
+
]
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
# Unsupported platform
|
|
241
|
+
self.logger.error(f"Log viewing not supported on platform: {system}")
|
|
242
|
+
|
|
203
243
|
except Exception as e: # pylint: disable=broad-exception-caught
|
|
204
244
|
self.logger.error(f"Error opening logs: {str(e)}")
|
|
205
245
|
|
|
@@ -228,7 +268,7 @@ class LemonadeTray(SystemTray):
|
|
|
228
268
|
try:
|
|
229
269
|
response = requests.get(
|
|
230
270
|
f"http://localhost:{self.port}/api/v0/health",
|
|
231
|
-
timeout=0.1,
|
|
271
|
+
timeout=0.1,
|
|
232
272
|
)
|
|
233
273
|
response.raise_for_status()
|
|
234
274
|
response_data = response.json()
|
|
@@ -257,7 +297,9 @@ class LemonadeTray(SystemTray):
|
|
|
257
297
|
"""
|
|
258
298
|
Change the server port and restart the server.
|
|
259
299
|
"""
|
|
300
|
+
|
|
260
301
|
try:
|
|
302
|
+
|
|
261
303
|
# Stop the current server
|
|
262
304
|
if self.server_thread and self.server_thread.is_alive():
|
|
263
305
|
# Set should_exit flag on the uvicorn server instance
|
|
@@ -270,16 +312,17 @@ class LemonadeTray(SystemTray):
|
|
|
270
312
|
|
|
271
313
|
# Update the port in both the tray and the server instance
|
|
272
314
|
self.port = new_port
|
|
273
|
-
if self.server:
|
|
274
|
-
self.server.port = new_port
|
|
275
315
|
|
|
276
|
-
#
|
|
316
|
+
# Clear the old server instance to ensure a fresh start
|
|
317
|
+
# This prevents middleware conflicts when restarting
|
|
318
|
+
self.server = None
|
|
319
|
+
|
|
277
320
|
self.server_thread = threading.Thread(target=self.start_server, daemon=True)
|
|
278
321
|
self.server_thread.start()
|
|
279
322
|
|
|
280
|
-
# Show notification
|
|
281
323
|
self.show_balloon_notification(
|
|
282
|
-
"Port Changed",
|
|
324
|
+
"Port Changed",
|
|
325
|
+
f"Lemonade Server is now running on port {self.port}",
|
|
283
326
|
)
|
|
284
327
|
|
|
285
328
|
except Exception as e: # pylint: disable=broad-exception-caught
|
|
@@ -539,6 +582,11 @@ class LemonadeTray(SystemTray):
|
|
|
539
582
|
Start the uvicorn server.
|
|
540
583
|
"""
|
|
541
584
|
self.server = self.server_factory()
|
|
585
|
+
|
|
586
|
+
# Ensure the server uses the current port from the tray
|
|
587
|
+
# This is important when changing ports
|
|
588
|
+
self.server.port = self.port
|
|
589
|
+
|
|
542
590
|
self.server.uvicorn_server = self.server.run_in_thread(self.server.host)
|
|
543
591
|
self.server.uvicorn_server.run()
|
|
544
592
|
|
|
@@ -547,16 +595,6 @@ class LemonadeTray(SystemTray):
|
|
|
547
595
|
Run the Lemonade tray application.
|
|
548
596
|
"""
|
|
549
597
|
|
|
550
|
-
# Register window class and create window
|
|
551
|
-
self.register_window_class()
|
|
552
|
-
self.create_window()
|
|
553
|
-
|
|
554
|
-
# Set up Windows console control handler for CTRL+C
|
|
555
|
-
self.console_handler = self.setup_console_control_handler(self.logger)
|
|
556
|
-
|
|
557
|
-
# Add tray icon
|
|
558
|
-
self.add_tray_icon()
|
|
559
|
-
|
|
560
598
|
# Start the background model mapping update thread
|
|
561
599
|
self.model_update_thread = threading.Thread(
|
|
562
600
|
target=self.update_downloaded_models_background, daemon=True
|
|
@@ -573,17 +611,27 @@ class LemonadeTray(SystemTray):
|
|
|
573
611
|
self.server_thread = threading.Thread(target=self.start_server, daemon=True)
|
|
574
612
|
self.server_thread.start()
|
|
575
613
|
|
|
576
|
-
#
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
(
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
614
|
+
# Provide an on_ready hook that Windows base tray will call after
|
|
615
|
+
# the HWND/icon are created. macOS will call it immediately after run.
|
|
616
|
+
def _on_ready():
|
|
617
|
+
system = platform.system().lower()
|
|
618
|
+
if system == "darwin":
|
|
619
|
+
message = (
|
|
620
|
+
"Lemonade Server is running! "
|
|
621
|
+
"Click the tray icon above to access options."
|
|
622
|
+
)
|
|
623
|
+
else: # Windows/Linux
|
|
624
|
+
message = (
|
|
625
|
+
"Lemonade Server is running! "
|
|
626
|
+
"Right-click the tray icon below to access options."
|
|
627
|
+
)
|
|
628
|
+
self.show_balloon_notification("Woohoo!", message)
|
|
584
629
|
|
|
585
|
-
#
|
|
586
|
-
self.
|
|
630
|
+
# Attach hook for both implementations to invoke after init
|
|
631
|
+
self.on_ready = _on_ready
|
|
632
|
+
|
|
633
|
+
# Call the parent run method which handles platform-specific initialization
|
|
634
|
+
super().run()
|
|
587
635
|
|
|
588
636
|
def exit_app(self, icon, item):
|
|
589
637
|
"""
|
|
@@ -598,8 +646,16 @@ class LemonadeTray(SystemTray):
|
|
|
598
646
|
if self.version_check_thread and self.version_check_thread.is_alive():
|
|
599
647
|
self.version_check_thread.join(timeout=1)
|
|
600
648
|
|
|
601
|
-
#
|
|
602
|
-
|
|
649
|
+
# Platform-specific exit handling
|
|
650
|
+
system = platform.system().lower()
|
|
651
|
+
if system == "darwin": # macOS
|
|
652
|
+
# For macOS, quit the rumps application
|
|
653
|
+
import rumps
|
|
654
|
+
|
|
655
|
+
rumps.quit_application()
|
|
656
|
+
else:
|
|
657
|
+
# Call parent exit method for Windows
|
|
658
|
+
super().exit_app(icon, item)
|
|
603
659
|
|
|
604
660
|
# Stop the server using the CLI stop command to ensure a rigorous cleanup
|
|
605
661
|
# This must be a subprocess to ensure the cleanup doesnt kill itself
|