lemonade-sdk 8.1.11__py3-none-any.whl → 8.2.0__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/common/system_info.py +0 -26
- lemonade/tools/bench.py +22 -1
- lemonade/tools/flm/utils.py +70 -22
- lemonade/tools/llamacpp/bench.py +111 -23
- lemonade/tools/llamacpp/load.py +30 -2
- lemonade/tools/llamacpp/utils.py +234 -15
- lemonade/tools/oga/bench.py +0 -26
- lemonade/tools/oga/load.py +38 -142
- lemonade/tools/oga/migration.py +403 -0
- lemonade/tools/report/table.py +6 -0
- lemonade/tools/server/flm.py +2 -6
- lemonade/tools/server/llamacpp.py +20 -1
- lemonade/tools/server/serve.py +335 -17
- lemonade/tools/server/static/js/models.js +416 -18
- lemonade/tools/server/static/js/shared.js +44 -6
- lemonade/tools/server/static/logs.html +29 -19
- lemonade/tools/server/static/styles.css +204 -0
- lemonade/tools/server/static/webapp.html +32 -0
- lemonade/version.py +1 -1
- lemonade_install/install.py +33 -579
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.0.dist-info}/METADATA +5 -3
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.0.dist-info}/RECORD +32 -31
- lemonade_server/cli.py +10 -0
- lemonade_server/model_manager.py +172 -11
- lemonade_server/server_models.json +102 -66
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.0.dist-info}/WHEEL +0 -0
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.0.dist-info}/entry_points.txt +0 -0
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.0.dist-info}/licenses/LICENSE +0 -0
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.0.dist-info}/licenses/NOTICE.md +0 -0
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.0.dist-info}/top_level.txt +0 -0
|
@@ -166,6 +166,29 @@ async function unloadModel() {
|
|
|
166
166
|
|
|
167
167
|
// === Model Browser Management ===
|
|
168
168
|
|
|
169
|
+
// Update visibility of categories/subcategories based on available models
|
|
170
|
+
function updateCategoryVisibility() {
|
|
171
|
+
const allModels = window.SERVER_MODELS || {};
|
|
172
|
+
|
|
173
|
+
// Count models for each recipe
|
|
174
|
+
const recipeCounts = {};
|
|
175
|
+
const recipes = ['llamacpp', 'oga-hybrid', 'oga-npu', 'oga-cpu', 'flm'];
|
|
176
|
+
recipes.forEach(recipe => {
|
|
177
|
+
recipeCounts[recipe] = 0;
|
|
178
|
+
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
179
|
+
if (modelData.recipe === recipe && (modelData.suggested || installedModels.has(modelId))) {
|
|
180
|
+
recipeCounts[recipe]++;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Show/hide recipe subcategory
|
|
185
|
+
const subcategory = document.querySelector(`[data-recipe="${recipe}"]`);
|
|
186
|
+
if (subcategory) {
|
|
187
|
+
subcategory.style.display = recipeCounts[recipe] > 0 ? 'block' : 'none';
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
169
192
|
// Toggle category in model browser (only for Hot Models now)
|
|
170
193
|
function toggleCategory(categoryName) {
|
|
171
194
|
const header = document.querySelector(`[data-category="${categoryName}"] .category-header`);
|
|
@@ -283,7 +306,7 @@ function displayHotModels() {
|
|
|
283
306
|
modelList.innerHTML = '';
|
|
284
307
|
|
|
285
308
|
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
286
|
-
if (modelData.labels && modelData.labels.includes('hot')) {
|
|
309
|
+
if (modelData.labels && modelData.labels.includes('hot') && (modelData.suggested || installedModels.has(modelId))) {
|
|
287
310
|
createModelItem(modelId, modelData, modelList);
|
|
288
311
|
}
|
|
289
312
|
});
|
|
@@ -317,7 +340,7 @@ function displayModelsByRecipe(recipe) {
|
|
|
317
340
|
}
|
|
318
341
|
|
|
319
342
|
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
320
|
-
if (modelData.recipe === recipe) {
|
|
343
|
+
if (modelData.recipe === recipe && (modelData.suggested || installedModels.has(modelId))) {
|
|
321
344
|
createModelItem(modelId, modelData, modelList);
|
|
322
345
|
}
|
|
323
346
|
});
|
|
@@ -341,7 +364,7 @@ function displayModelsByLabel(label) {
|
|
|
341
364
|
if (modelId.startsWith('user.')) {
|
|
342
365
|
createModelItem(modelId, modelData, modelList);
|
|
343
366
|
}
|
|
344
|
-
} else if (modelData.labels && modelData.labels.includes(label)) {
|
|
367
|
+
} else if (modelData.labels && modelData.labels.includes(label) && (modelData.suggested || installedModels.has(modelId))) {
|
|
345
368
|
createModelItem(modelId, modelData, modelList);
|
|
346
369
|
}
|
|
347
370
|
});
|
|
@@ -505,7 +528,11 @@ async function deleteModel(modelId) {
|
|
|
505
528
|
headers: { 'Content-Type': 'application/json' },
|
|
506
529
|
body: JSON.stringify({ model_name: modelId })
|
|
507
530
|
});
|
|
508
|
-
|
|
531
|
+
installedModels.delete(modelId);
|
|
532
|
+
// Remove custom models from SERVER_MODELS to prevent them from reappearing without having to do a manual refresh
|
|
533
|
+
if (modelId.startsWith('user.')) {
|
|
534
|
+
delete window.SERVER_MODELS[modelId];
|
|
535
|
+
}
|
|
509
536
|
// Refresh installed models and model status
|
|
510
537
|
await fetchInstalledModels();
|
|
511
538
|
await updateModelStatusIndicator();
|
|
@@ -618,8 +645,8 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|
|
618
645
|
// Initial fetch of model data - this will populate installedModels
|
|
619
646
|
await updateModelStatusIndicator();
|
|
620
647
|
|
|
621
|
-
//
|
|
622
|
-
|
|
648
|
+
// Update category visibility on initial load
|
|
649
|
+
updateCategoryVisibility();
|
|
623
650
|
|
|
624
651
|
// Initialize model browser with hot models
|
|
625
652
|
displayHotModels();
|
|
@@ -635,6 +662,45 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|
|
635
662
|
|
|
636
663
|
// Set up register model form
|
|
637
664
|
setupRegisterModelForm();
|
|
665
|
+
setupFolderSelection();
|
|
666
|
+
|
|
667
|
+
// Set up smart periodic refresh to detect external model changes
|
|
668
|
+
// Poll every 15 seconds (much less aggressive than 1 second)
|
|
669
|
+
// Only poll when page is visible to save resources
|
|
670
|
+
let pollInterval = null;
|
|
671
|
+
|
|
672
|
+
function startPolling() {
|
|
673
|
+
if (!pollInterval) {
|
|
674
|
+
pollInterval = setInterval(async () => {
|
|
675
|
+
// Only update if page is visible
|
|
676
|
+
if (document.visibilityState === 'visible') {
|
|
677
|
+
await updateModelStatusIndicator();
|
|
678
|
+
}
|
|
679
|
+
}, 15000); // Check every 15 seconds
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function stopPolling() {
|
|
684
|
+
if (pollInterval) {
|
|
685
|
+
clearInterval(pollInterval);
|
|
686
|
+
pollInterval = null;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Start polling when page is visible, stop when hidden
|
|
691
|
+
document.addEventListener('visibilitychange', () => {
|
|
692
|
+
if (document.visibilityState === 'visible') {
|
|
693
|
+
// Page became visible - update immediately and resume polling
|
|
694
|
+
updateModelStatusIndicator();
|
|
695
|
+
startPolling();
|
|
696
|
+
} else {
|
|
697
|
+
// Page hidden - stop polling to save resources
|
|
698
|
+
stopPolling();
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// Start polling initially
|
|
703
|
+
startPolling();
|
|
638
704
|
});
|
|
639
705
|
|
|
640
706
|
// Toggle Add Model form
|
|
@@ -928,7 +994,17 @@ function setupRegisterModelForm() {
|
|
|
928
994
|
if (!name.startsWith('user.')) {
|
|
929
995
|
name = 'user.' + name;
|
|
930
996
|
}
|
|
931
|
-
|
|
997
|
+
|
|
998
|
+
// Check if model name already exists
|
|
999
|
+
const allModels = window.SERVER_MODELS || {};
|
|
1000
|
+
if (allModels[name] || installedModels.has(name)) {
|
|
1001
|
+
showErrorBanner('Model name already exists. Please enter a different name.');
|
|
1002
|
+
registerStatus.textContent = 'Model name already exists';
|
|
1003
|
+
registerStatus.style.color = '#b10819ff';
|
|
1004
|
+
registerStatus.className = 'register-status error';
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
932
1008
|
const checkpoint = document.getElementById('register-checkpoint').value.trim();
|
|
933
1009
|
const recipe = document.getElementById('register-recipe').value;
|
|
934
1010
|
const reasoning = document.getElementById('register-reasoning').checked;
|
|
@@ -939,24 +1015,70 @@ function setupRegisterModelForm() {
|
|
|
939
1015
|
return;
|
|
940
1016
|
}
|
|
941
1017
|
|
|
942
|
-
const payload = { model_name: name, recipe, reasoning, vision };
|
|
943
|
-
if (checkpoint) payload.checkpoint = checkpoint;
|
|
944
|
-
if (mmproj) payload.mmproj = mmproj;
|
|
945
|
-
|
|
946
1018
|
const btn = document.getElementById('register-submit');
|
|
947
1019
|
btn.disabled = true;
|
|
948
1020
|
btn.textContent = 'Installing...';
|
|
949
1021
|
|
|
950
1022
|
try {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1023
|
+
if (isLocalModel && selectedModelFiles) {
|
|
1024
|
+
if (recipe === 'llamacpp' && !Array.from(selectedModelFiles).some(file => file.name.toLowerCase().endsWith('.gguf'))) {
|
|
1025
|
+
throw new Error('No .gguf files found in the selected folder for llamacpp');
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const formData = new FormData();
|
|
1029
|
+
formData.append('model_name', name);
|
|
1030
|
+
formData.append('checkpoint', checkpoint);
|
|
1031
|
+
formData.append('recipe', recipe);
|
|
1032
|
+
formData.append('reasoning', reasoning);
|
|
1033
|
+
formData.append('vision', vision);
|
|
1034
|
+
if (mmproj) formData.append('mmproj', mmproj);
|
|
1035
|
+
Array.from(selectedModelFiles).forEach(file => {
|
|
1036
|
+
formData.append('model_files', file, file.webkitRelativePath);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/add-local-model', {
|
|
1040
|
+
method: 'POST',
|
|
1041
|
+
body: formData
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
if (!checkpoint) {
|
|
1046
|
+
throw new Error('Checkpoint is required for remote models');
|
|
1047
|
+
}
|
|
1048
|
+
const payload = { model_name: name, recipe, reasoning, vision };
|
|
1049
|
+
if (checkpoint) payload.checkpoint = checkpoint;
|
|
1050
|
+
if (mmproj) payload.mmproj = mmproj;
|
|
1051
|
+
|
|
1052
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
|
|
1053
|
+
method: 'POST',
|
|
1054
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1055
|
+
body: JSON.stringify(payload)
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
956
1059
|
registerStatus.textContent = 'Model installed!';
|
|
957
|
-
registerStatus.style.color = '#
|
|
1060
|
+
registerStatus.style.color = '#0eaf51ff';
|
|
958
1061
|
registerStatus.className = 'register-status success';
|
|
1062
|
+
|
|
1063
|
+
// Add custom model to SERVER_MODELS so it appears in the UI without having to do a manual refresh
|
|
1064
|
+
if (name.startsWith('user.')) {
|
|
1065
|
+
const labels = ['custom'];
|
|
1066
|
+
if (vision) labels.push('vision');
|
|
1067
|
+
if (reasoning) labels.push('reasoning');
|
|
1068
|
+
|
|
1069
|
+
window.SERVER_MODELS[name] = {
|
|
1070
|
+
recipe: recipe,
|
|
1071
|
+
labels: labels
|
|
1072
|
+
};
|
|
1073
|
+
if (checkpoint) window.SERVER_MODELS[name].checkpoint = checkpoint;
|
|
1074
|
+
if (mmproj) window.SERVER_MODELS[name].mmproj = mmproj;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
959
1077
|
registerForm.reset();
|
|
1078
|
+
isLocalModel = false;
|
|
1079
|
+
selectedModelFiles = null;
|
|
1080
|
+
document.getElementById('folder-input').value = '';
|
|
1081
|
+
|
|
960
1082
|
await refreshModelMgmtUI();
|
|
961
1083
|
// Update chat dropdown too if loadModels function exists
|
|
962
1084
|
if (typeof loadModels === 'function') {
|
|
@@ -975,6 +1097,278 @@ function setupRegisterModelForm() {
|
|
|
975
1097
|
};
|
|
976
1098
|
}
|
|
977
1099
|
}
|
|
1100
|
+
let isLocalModel = false;
|
|
1101
|
+
let selectedModelFiles = null;
|
|
1102
|
+
// Helper function to find mmproj file in selected folder
|
|
1103
|
+
function findMmprojFile(files) {
|
|
1104
|
+
for (let i = 0; i < files.length; i++) {
|
|
1105
|
+
const file = files[i];
|
|
1106
|
+
const fileName = file.name.toLowerCase();
|
|
1107
|
+
const relativePath = file.webkitRelativePath;
|
|
1108
|
+
|
|
1109
|
+
// Check if file contains 'mmproj' and has .gguf extension
|
|
1110
|
+
if (fileName.includes('mmproj') && fileName.endsWith('.gguf')) {
|
|
1111
|
+
// Return just the filename (last part of the path)
|
|
1112
|
+
return relativePath.split('/').pop();
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Helper function to find all non-mmproj GGUF files in selected folder
|
|
1119
|
+
function findGgufFiles(files) {
|
|
1120
|
+
const ggufFiles = [];
|
|
1121
|
+
for (let i = 0; i < files.length; i++) {
|
|
1122
|
+
const file = files[i];
|
|
1123
|
+
const fileName = file.name.toLowerCase();
|
|
1124
|
+
const relativePath = file.webkitRelativePath;
|
|
1125
|
+
|
|
1126
|
+
// Check if file has .gguf extension but is NOT an mmproj file
|
|
1127
|
+
if (fileName.endsWith('.gguf') && !fileName.includes('mmproj')) {
|
|
1128
|
+
// Store just the filename (last part of the path)
|
|
1129
|
+
ggufFiles.push(relativePath.split('/').pop());
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return ggufFiles;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Helper function to check GGUF files and show appropriate banners
|
|
1136
|
+
function checkGgufFilesAndShowBanner(files) {
|
|
1137
|
+
const recipeSelect = document.getElementById('register-recipe');
|
|
1138
|
+
|
|
1139
|
+
// Only check if llamacpp is selected
|
|
1140
|
+
if (!recipeSelect || recipeSelect.value !== 'llamacpp') {
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const mmprojFile = findMmprojFile(files);
|
|
1145
|
+
const ggufFiles = findGgufFiles(files);
|
|
1146
|
+
|
|
1147
|
+
// Hide any existing banners first
|
|
1148
|
+
hideErrorBanner();
|
|
1149
|
+
|
|
1150
|
+
if (ggufFiles.length > 1) {
|
|
1151
|
+
// Multiple GGUF files detected
|
|
1152
|
+
const folderPath = files[0].webkitRelativePath.split('/')[0];
|
|
1153
|
+
let bannerMsg = `More than one variant detected. Please clarify them at the end of the checkpoint name like:\n<folder_name>:<variant>\nExample: ${folderPath}:${ggufFiles[0]}`;
|
|
1154
|
+
|
|
1155
|
+
if (mmprojFile) {
|
|
1156
|
+
bannerMsg += `\n\nDon't forget to enter the mmproj file name and check the 'vision' checkbox if it is a vision model.`;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
showBanner(bannerMsg, 'warning');
|
|
1160
|
+
} else if (mmprojFile) {
|
|
1161
|
+
// MMproj detected
|
|
1162
|
+
showBanner("MMproj detected and populated. Please validate the file name and check the 'vision' checkbox if it is a vision model.", 'success');
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
// Helper function to auto-fill mmproj field if llamacpp is selected
|
|
1166
|
+
function autoFillMmproj() {
|
|
1167
|
+
const recipeSelect = document.getElementById('register-recipe');
|
|
1168
|
+
const mmprojInput = document.getElementById('register-mmproj');
|
|
1169
|
+
|
|
1170
|
+
if (recipeSelect && mmprojInput && isLocalModel && selectedModelFiles) {
|
|
1171
|
+
const selectedRecipe = recipeSelect.value;
|
|
1172
|
+
|
|
1173
|
+
if (selectedRecipe === 'llamacpp') {
|
|
1174
|
+
const mmprojFile = findMmprojFile(selectedModelFiles);
|
|
1175
|
+
if (mmprojFile) {
|
|
1176
|
+
mmprojInput.value = mmprojFile;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Check GGUF files and show appropriate banner
|
|
1180
|
+
checkGgufFilesAndShowBanner(selectedModelFiles);
|
|
1181
|
+
} else {
|
|
1182
|
+
// Hide banners if not llamacpp
|
|
1183
|
+
hideErrorBanner();
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
function setupFolderSelection() {
|
|
1188
|
+
const selectFolderBtn = document.getElementById('select-folder-btn');
|
|
1189
|
+
const folderInput = document.getElementById('folder-input');
|
|
1190
|
+
const checkpointInput = document.getElementById('register-checkpoint');
|
|
1191
|
+
const recipeSelect = document.getElementById('register-recipe');
|
|
1192
|
+
|
|
1193
|
+
if (selectFolderBtn && folderInput && checkpointInput) {
|
|
1194
|
+
selectFolderBtn.addEventListener('click', () => {
|
|
1195
|
+
folderInput.click();
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
folderInput.addEventListener('change', (event) => {
|
|
1199
|
+
const files = event.target.files;
|
|
1200
|
+
if (files.length > 0) {
|
|
1201
|
+
const firstFile = files[0];
|
|
1202
|
+
const folderPath = firstFile.webkitRelativePath.split('/')[0];
|
|
1203
|
+
checkpointInput.value = folderPath;
|
|
1204
|
+
isLocalModel = true;
|
|
1205
|
+
selectedModelFiles = files;
|
|
1206
|
+
|
|
1207
|
+
// Auto-fill mmproj if llamacpp is already selected
|
|
1208
|
+
autoFillMmproj();
|
|
1209
|
+
}
|
|
1210
|
+
else {
|
|
1211
|
+
isLocalModel = false;
|
|
1212
|
+
selectedModelFiles = null;
|
|
1213
|
+
checkpointInput.value = '';
|
|
1214
|
+
hideErrorBanner();
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
// Add listener to recipe dropdown to auto-fill mmproj when changed to llamacpp
|
|
1219
|
+
if (recipeSelect) {
|
|
1220
|
+
recipeSelect.addEventListener('change', () => {
|
|
1221
|
+
autoFillMmproj();
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
// === Migration/Cleanup Functions ===
|
|
1227
|
+
|
|
1228
|
+
// Store incompatible models data globally
|
|
1229
|
+
let incompatibleModelsData = null;
|
|
1230
|
+
|
|
1231
|
+
// Check for incompatible models on page load
|
|
1232
|
+
async function checkIncompatibleModels() {
|
|
1233
|
+
try {
|
|
1234
|
+
const response = await httpJson(getServerBaseUrl() + '/api/v1/migration/incompatible-models');
|
|
1235
|
+
incompatibleModelsData = response;
|
|
1236
|
+
|
|
1237
|
+
if (response.count > 0) {
|
|
1238
|
+
showMigrationBanner(response.count, response.total_size);
|
|
1239
|
+
}
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
console.error('Error checking for incompatible models:', error);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Show migration banner
|
|
1246
|
+
function showMigrationBanner(count, totalSize) {
|
|
1247
|
+
const banner = document.getElementById('migration-banner');
|
|
1248
|
+
const msg = document.getElementById('migration-banner-msg');
|
|
1249
|
+
|
|
1250
|
+
const sizeGB = (totalSize / (1024 * 1024 * 1024)).toFixed(1);
|
|
1251
|
+
msg.textContent = `Found ${count} incompatible RyzenAI model${count > 1 ? 's' : ''} (${sizeGB} GB). Clean up to free disk space.`;
|
|
1252
|
+
banner.style.display = 'flex';
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Hide migration banner
|
|
1256
|
+
function hideMigrationBanner() {
|
|
1257
|
+
const banner = document.getElementById('migration-banner');
|
|
1258
|
+
banner.style.display = 'none';
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Show migration modal with model list
|
|
1262
|
+
function showMigrationModal() {
|
|
1263
|
+
if (!incompatibleModelsData || incompatibleModelsData.count === 0) {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const modal = document.getElementById('migration-modal');
|
|
1268
|
+
const modelList = document.getElementById('migration-model-list');
|
|
1269
|
+
const totalSize = document.getElementById('migration-total-size');
|
|
1270
|
+
|
|
1271
|
+
// Populate model list
|
|
1272
|
+
modelList.innerHTML = '';
|
|
1273
|
+
incompatibleModelsData.models.forEach(model => {
|
|
1274
|
+
const item = document.createElement('div');
|
|
1275
|
+
item.className = 'migration-model-item';
|
|
1276
|
+
|
|
1277
|
+
const nameSpan = document.createElement('span');
|
|
1278
|
+
nameSpan.className = 'migration-model-name';
|
|
1279
|
+
nameSpan.textContent = model.name;
|
|
1280
|
+
|
|
1281
|
+
const sizeSpan = document.createElement('span');
|
|
1282
|
+
sizeSpan.className = 'migration-model-size';
|
|
1283
|
+
sizeSpan.textContent = model.size_formatted;
|
|
1284
|
+
|
|
1285
|
+
item.appendChild(nameSpan);
|
|
1286
|
+
item.appendChild(sizeSpan);
|
|
1287
|
+
modelList.appendChild(item);
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
// Set total size
|
|
1291
|
+
const sizeGB = (incompatibleModelsData.total_size / (1024 * 1024 * 1024)).toFixed(1);
|
|
1292
|
+
totalSize.textContent = `${sizeGB} GB`;
|
|
1293
|
+
|
|
1294
|
+
modal.style.display = 'flex';
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Hide migration modal
|
|
1298
|
+
function hideMigrationModal() {
|
|
1299
|
+
const modal = document.getElementById('migration-modal');
|
|
1300
|
+
modal.style.display = 'none';
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// Delete incompatible models
|
|
1304
|
+
async function deleteIncompatibleModels() {
|
|
1305
|
+
if (!incompatibleModelsData || incompatibleModelsData.count === 0) {
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
const modelPaths = incompatibleModelsData.models.map(m => m.path);
|
|
1310
|
+
|
|
1311
|
+
try {
|
|
1312
|
+
// Disable buttons during deletion
|
|
1313
|
+
const deleteBtn = document.querySelector('.delete-btn');
|
|
1314
|
+
const cancelBtn = document.querySelector('.cancel-btn');
|
|
1315
|
+
deleteBtn.disabled = true;
|
|
1316
|
+
cancelBtn.disabled = true;
|
|
1317
|
+
deleteBtn.textContent = 'Deleting...';
|
|
1318
|
+
|
|
1319
|
+
const response = await httpRequest(getServerBaseUrl() + '/api/v1/migration/cleanup', {
|
|
1320
|
+
method: 'POST',
|
|
1321
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1322
|
+
body: JSON.stringify({ model_paths: modelPaths })
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
const result = await response.json();
|
|
1326
|
+
|
|
1327
|
+
// Close modal
|
|
1328
|
+
hideMigrationModal();
|
|
1329
|
+
|
|
1330
|
+
// Hide banner
|
|
1331
|
+
hideMigrationBanner();
|
|
1332
|
+
|
|
1333
|
+
// Show success message
|
|
1334
|
+
showSuccessMessage(`Successfully deleted ${result.success_count} model${result.success_count > 1 ? 's' : ''}, freed ${result.freed_size_formatted}`);
|
|
1335
|
+
|
|
1336
|
+
// Clear cached data
|
|
1337
|
+
incompatibleModelsData = null;
|
|
1338
|
+
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
console.error('Error deleting incompatible models:', error);
|
|
1341
|
+
showErrorBanner('Failed to delete models: ' + error.message);
|
|
1342
|
+
|
|
1343
|
+
// Re-enable buttons
|
|
1344
|
+
const deleteBtn = document.querySelector('.delete-btn');
|
|
1345
|
+
const cancelBtn = document.querySelector('.cancel-btn');
|
|
1346
|
+
deleteBtn.disabled = false;
|
|
1347
|
+
cancelBtn.disabled = false;
|
|
1348
|
+
deleteBtn.textContent = 'Delete All';
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// Show success message (reuse error banner with green color)
|
|
1353
|
+
function showSuccessMessage(message) {
|
|
1354
|
+
const banner = document.getElementById('error-banner');
|
|
1355
|
+
const msg = document.getElementById('error-banner-msg');
|
|
1356
|
+
msg.textContent = message;
|
|
1357
|
+
banner.style.backgroundColor = '#2d7f47';
|
|
1358
|
+
banner.style.display = 'flex';
|
|
1359
|
+
|
|
1360
|
+
// Auto-hide after 5 seconds
|
|
1361
|
+
setTimeout(() => {
|
|
1362
|
+
banner.style.display = 'none';
|
|
1363
|
+
banner.style.backgroundColor = ''; // Reset to default
|
|
1364
|
+
}, 5000);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// Check for incompatible models when page loads
|
|
1368
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1369
|
+
// Run check after a short delay to let the page load
|
|
1370
|
+
setTimeout(checkIncompatibleModels, 1000);
|
|
1371
|
+
});
|
|
978
1372
|
|
|
979
1373
|
// Make functions globally available for HTML onclick handlers and other components
|
|
980
1374
|
window.toggleCategory = toggleCategory;
|
|
@@ -983,4 +1377,8 @@ window.selectLabel = selectLabel;
|
|
|
983
1377
|
window.showAddModelForm = showAddModelForm;
|
|
984
1378
|
window.unloadModel = unloadModel;
|
|
985
1379
|
window.installModel = installModel;
|
|
986
|
-
window.deleteModel = deleteModel;
|
|
1380
|
+
window.deleteModel = deleteModel;
|
|
1381
|
+
window.showMigrationModal = showMigrationModal;
|
|
1382
|
+
window.hideMigrationModal = hideMigrationModal;
|
|
1383
|
+
window.hideMigrationBanner = hideMigrationBanner;
|
|
1384
|
+
window.deleteIncompatibleModels = deleteIncompatibleModels;
|
|
@@ -54,23 +54,53 @@ function renderMarkdown(text) {
|
|
|
54
54
|
|
|
55
55
|
// Display an error message in the banner
|
|
56
56
|
function showErrorBanner(msg) {
|
|
57
|
+
showBanner(msg, 'error');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Display a banner with a specific type (error, warning, success)
|
|
61
|
+
function showBanner(msg, type = 'error') {
|
|
57
62
|
// If DOM isn't ready, wait for it
|
|
58
63
|
if (document.readyState === 'loading') {
|
|
59
64
|
document.addEventListener('DOMContentLoaded', () => {
|
|
60
|
-
|
|
65
|
+
showBanner(msg, type);
|
|
61
66
|
});
|
|
62
67
|
return;
|
|
63
68
|
}
|
|
64
|
-
|
|
69
|
+
|
|
65
70
|
const banner = document.getElementById('error-banner');
|
|
66
71
|
if (!banner) return;
|
|
67
72
|
const msgEl = document.getElementById('error-banner-msg');
|
|
68
|
-
const
|
|
73
|
+
const logsUrl = window.location.origin + '/static/logs.html';
|
|
74
|
+
|
|
75
|
+
// Determine the full message and styling based on type
|
|
76
|
+
let fullMsg = msg;
|
|
77
|
+
let backgroundColor, color;
|
|
78
|
+
|
|
79
|
+
switch(type) {
|
|
80
|
+
case 'success':
|
|
81
|
+
backgroundColor = '#27ae60'; // green
|
|
82
|
+
color = '#ffffff';
|
|
83
|
+
break;
|
|
84
|
+
case 'warning':
|
|
85
|
+
backgroundColor = '#8d5803ff'; // yellow/orange
|
|
86
|
+
color = '#ffffff';
|
|
87
|
+
break;
|
|
88
|
+
case 'error':
|
|
89
|
+
default:
|
|
90
|
+
backgroundColor = '#b10819ff'; // red
|
|
91
|
+
color = '#ffffff';
|
|
92
|
+
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.`;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
69
96
|
if (msgEl) {
|
|
70
|
-
msgEl.
|
|
97
|
+
msgEl.innerHTML = fullMsg;
|
|
71
98
|
} else {
|
|
72
|
-
banner.
|
|
99
|
+
banner.innerHTML = fullMsg;
|
|
73
100
|
}
|
|
101
|
+
|
|
102
|
+
banner.style.backgroundColor = backgroundColor;
|
|
103
|
+
banner.style.color = color;
|
|
74
104
|
banner.style.display = 'flex';
|
|
75
105
|
}
|
|
76
106
|
|
|
@@ -222,10 +252,18 @@ async function loadModelStandardized(modelId, options = {}) {
|
|
|
222
252
|
}
|
|
223
253
|
|
|
224
254
|
// Make the API call to load the model
|
|
255
|
+
// Include mmproj if the model has it defined
|
|
256
|
+
const loadPayload = { model_name: modelId };
|
|
257
|
+
const allModels = window.SERVER_MODELS || {};
|
|
258
|
+
const modelData = allModels[modelId];
|
|
259
|
+
if (modelData && modelData.mmproj) {
|
|
260
|
+
loadPayload.mmproj = modelData.mmproj;
|
|
261
|
+
}
|
|
262
|
+
|
|
225
263
|
await httpRequest(getServerBaseUrl() + '/api/v1/load', {
|
|
226
264
|
method: 'POST',
|
|
227
265
|
headers: { 'Content-Type': 'application/json' },
|
|
228
|
-
body: JSON.stringify(
|
|
266
|
+
body: JSON.stringify(loadPayload)
|
|
229
267
|
});
|
|
230
268
|
|
|
231
269
|
// Update model status indicator after successful load
|
|
@@ -22,26 +22,36 @@
|
|
|
22
22
|
<body>
|
|
23
23
|
<div id="log-container"></div>
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
}
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
};
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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>
|
|
46
56
|
</body>
|
|
47
57
|
</html>
|