lemonade-sdk 8.1.11__py3-none-any.whl → 8.2.2__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/accuracy.py +143 -48
- lemonade/tools/adapter.py +6 -1
- lemonade/tools/bench.py +26 -8
- lemonade/tools/flm/utils.py +70 -22
- lemonade/tools/huggingface/bench.py +6 -1
- lemonade/tools/llamacpp/bench.py +146 -27
- lemonade/tools/llamacpp/load.py +30 -2
- lemonade/tools/llamacpp/utils.py +317 -21
- lemonade/tools/oga/bench.py +5 -26
- lemonade/tools/oga/load.py +49 -123
- lemonade/tools/oga/migration.py +403 -0
- lemonade/tools/report/table.py +76 -8
- lemonade/tools/server/flm.py +2 -6
- lemonade/tools/server/llamacpp.py +43 -2
- lemonade/tools/server/serve.py +354 -18
- lemonade/tools/server/static/js/chat.js +15 -77
- lemonade/tools/server/static/js/model-settings.js +24 -3
- lemonade/tools/server/static/js/models.js +440 -37
- lemonade/tools/server/static/js/shared.js +61 -8
- lemonade/tools/server/static/logs.html +157 -13
- lemonade/tools/server/static/styles.css +204 -0
- lemonade/tools/server/static/webapp.html +39 -1
- lemonade/version.py +1 -1
- lemonade_install/install.py +33 -579
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/METADATA +6 -4
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/RECORD +38 -37
- lemonade_server/cli.py +10 -0
- lemonade_server/model_manager.py +172 -11
- lemonade_server/pydantic_models.py +3 -0
- lemonade_server/server_models.json +102 -66
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/WHEEL +0 -0
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/entry_points.txt +0 -0
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/licenses/LICENSE +0 -0
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/licenses/NOTICE.md +0 -0
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/top_level.txt +0 -0
|
@@ -28,9 +28,11 @@ function loadModelSettings() {
|
|
|
28
28
|
const topKInput = document.getElementById('setting-top-k');
|
|
29
29
|
const topPInput = document.getElementById('setting-top-p');
|
|
30
30
|
const repeatInput = document.getElementById('setting-repeat-penalty');
|
|
31
|
+
const thinkingCheckbox = document.getElementById('enable-thinking');
|
|
32
|
+
|
|
31
33
|
|
|
32
34
|
// Check if DOM elements exist
|
|
33
|
-
if (!tempInput || !topKInput || !topPInput || !repeatInput) {
|
|
35
|
+
if (!tempInput || !topKInput || !topPInput || !repeatInput || !thinkingCheckbox) {
|
|
34
36
|
return;
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -47,11 +49,18 @@ function loadModelSettings() {
|
|
|
47
49
|
if (modelSettings.repeat_penalty !== undefined) {
|
|
48
50
|
repeatInput.value = modelSettings.repeat_penalty;
|
|
49
51
|
}
|
|
52
|
+
if (modelSettings.enable_thinking !== undefined) {
|
|
53
|
+
thinkingCheckbox.checked = modelSettings.enable_thinking;
|
|
54
|
+
|
|
55
|
+
} else {
|
|
56
|
+
thinkingCheckbox.checked = true; // default to enabled
|
|
57
|
+
|
|
58
|
+
}
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
// Auto-save model settings whenever inputs change
|
|
53
62
|
function setupAutoSaveSettings() {
|
|
54
|
-
const inputs = ['setting-temperature', 'setting-top-k', 'setting-top-p', 'setting-repeat-penalty'];
|
|
63
|
+
const inputs = ['setting-temperature', 'setting-top-k', 'setting-top-p', 'setting-repeat-penalty', 'enable-thinking'];
|
|
55
64
|
|
|
56
65
|
inputs.forEach(inputId => {
|
|
57
66
|
const input = document.getElementById(inputId);
|
|
@@ -63,6 +72,12 @@ function setupAutoSaveSettings() {
|
|
|
63
72
|
updateModelSettings();
|
|
64
73
|
});
|
|
65
74
|
}
|
|
75
|
+
const thinkingCheckbox = document.getElementById('enable-thinking');
|
|
76
|
+
if (thinkingCheckbox) {
|
|
77
|
+
thinkingCheckbox.addEventListener('change', function() {
|
|
78
|
+
updateModelSettings();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
66
81
|
});
|
|
67
82
|
}
|
|
68
83
|
|
|
@@ -72,9 +87,10 @@ function updateModelSettings() {
|
|
|
72
87
|
const topKInput = document.getElementById('setting-top-k');
|
|
73
88
|
const topPInput = document.getElementById('setting-top-p');
|
|
74
89
|
const repeatInput = document.getElementById('setting-repeat-penalty');
|
|
90
|
+
const thinkingCheckbox = document.getElementById('enable-thinking');
|
|
75
91
|
|
|
76
92
|
// Check if DOM elements exist (might not be available if DOM isn't ready)
|
|
77
|
-
if (!tempInput || !topKInput || !topPInput || !repeatInput) {
|
|
93
|
+
if (!tempInput || !topKInput || !topPInput || !repeatInput || !thinkingCheckbox) {
|
|
78
94
|
return;
|
|
79
95
|
}
|
|
80
96
|
|
|
@@ -93,6 +109,7 @@ function updateModelSettings() {
|
|
|
93
109
|
if (repeatInput.value && repeatInput.value.trim() !== '') {
|
|
94
110
|
modelSettings.repeat_penalty = parseFloat(repeatInput.value);
|
|
95
111
|
}
|
|
112
|
+
modelSettings.enable_thinking = thinkingCheckbox.checked;
|
|
96
113
|
|
|
97
114
|
// Save to localStorage
|
|
98
115
|
localStorage.setItem('lemonade_model_settings', JSON.stringify(modelSettings));
|
|
@@ -107,6 +124,7 @@ function resetModelSettings() {
|
|
|
107
124
|
document.getElementById('setting-top-k').value = '';
|
|
108
125
|
document.getElementById('setting-top-p').value = '';
|
|
109
126
|
document.getElementById('setting-repeat-penalty').value = '';
|
|
127
|
+
document.getElementById('enable-thinking').checked = true;
|
|
110
128
|
|
|
111
129
|
localStorage.removeItem('lemonade_model_settings');
|
|
112
130
|
}
|
|
@@ -135,6 +153,9 @@ function getCurrentModelSettings() {
|
|
|
135
153
|
if (modelSettings.repeat_penalty !== undefined) {
|
|
136
154
|
currentSettings.repeat_penalty = modelSettings.repeat_penalty;
|
|
137
155
|
}
|
|
156
|
+
if (modelSettings.enable_thinking !== undefined) {
|
|
157
|
+
currentSettings.enable_thinking = modelSettings.enable_thinking;
|
|
158
|
+
}
|
|
138
159
|
|
|
139
160
|
console.log('getCurrentModelSettings returning:', currentSettings);
|
|
140
161
|
return currentSettings;
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// State variables for model management
|
|
4
4
|
let currentLoadedModel = null;
|
|
5
5
|
let installedModels = new Set(); // Track which models are actually installed
|
|
6
|
+
let activeOperations = new Set(); // Track models currently being downloaded or loaded
|
|
6
7
|
|
|
7
|
-
// Make installedModels accessible globally
|
|
8
|
+
// Make installedModels and activeOperations accessible globally
|
|
8
9
|
window.installedModels = installedModels;
|
|
10
|
+
window.activeOperations = activeOperations;
|
|
9
11
|
let currentCategory = 'hot';
|
|
10
12
|
let currentFilter = null;
|
|
11
13
|
|
|
@@ -166,6 +168,29 @@ async function unloadModel() {
|
|
|
166
168
|
|
|
167
169
|
// === Model Browser Management ===
|
|
168
170
|
|
|
171
|
+
// Update visibility of categories/subcategories based on available models
|
|
172
|
+
function updateCategoryVisibility() {
|
|
173
|
+
const allModels = window.SERVER_MODELS || {};
|
|
174
|
+
|
|
175
|
+
// Count models for each recipe
|
|
176
|
+
const recipeCounts = {};
|
|
177
|
+
const recipes = ['llamacpp', 'oga-hybrid', 'oga-npu', 'oga-cpu', 'flm'];
|
|
178
|
+
recipes.forEach(recipe => {
|
|
179
|
+
recipeCounts[recipe] = 0;
|
|
180
|
+
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
181
|
+
if (modelData.recipe === recipe && (modelData.suggested || installedModels.has(modelId))) {
|
|
182
|
+
recipeCounts[recipe]++;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Show/hide recipe subcategory
|
|
187
|
+
const subcategory = document.querySelector(`[data-recipe="${recipe}"]`);
|
|
188
|
+
if (subcategory) {
|
|
189
|
+
subcategory.style.display = recipeCounts[recipe] > 0 ? 'block' : 'none';
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
169
194
|
// Toggle category in model browser (only for Hot Models now)
|
|
170
195
|
function toggleCategory(categoryName) {
|
|
171
196
|
const header = document.querySelector(`[data-category="${categoryName}"] .category-header`);
|
|
@@ -283,7 +308,7 @@ function displayHotModels() {
|
|
|
283
308
|
modelList.innerHTML = '';
|
|
284
309
|
|
|
285
310
|
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
286
|
-
if (modelData.labels && modelData.labels.includes('hot')) {
|
|
311
|
+
if (modelData.labels && modelData.labels.includes('hot') && (modelData.suggested || installedModels.has(modelId))) {
|
|
287
312
|
createModelItem(modelId, modelData, modelList);
|
|
288
313
|
}
|
|
289
314
|
});
|
|
@@ -317,7 +342,7 @@ function displayModelsByRecipe(recipe) {
|
|
|
317
342
|
}
|
|
318
343
|
|
|
319
344
|
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
320
|
-
if (modelData.recipe === recipe) {
|
|
345
|
+
if (modelData.recipe === recipe && (modelData.suggested || installedModels.has(modelId))) {
|
|
321
346
|
createModelItem(modelId, modelData, modelList);
|
|
322
347
|
}
|
|
323
348
|
});
|
|
@@ -341,7 +366,7 @@ function displayModelsByLabel(label) {
|
|
|
341
366
|
if (modelId.startsWith('user.')) {
|
|
342
367
|
createModelItem(modelId, modelData, modelList);
|
|
343
368
|
}
|
|
344
|
-
} else if (modelData.labels && modelData.labels.includes(label)) {
|
|
369
|
+
} else if (modelData.labels && modelData.labels.includes(label) && (modelData.suggested || installedModels.has(modelId))) {
|
|
345
370
|
createModelItem(modelId, modelData, modelList);
|
|
346
371
|
}
|
|
347
372
|
});
|
|
@@ -443,6 +468,9 @@ async function installModel(modelId) {
|
|
|
443
468
|
installBtn.textContent = '⏳';
|
|
444
469
|
}
|
|
445
470
|
|
|
471
|
+
// Track this download as active
|
|
472
|
+
activeOperations.add(modelId);
|
|
473
|
+
|
|
446
474
|
try {
|
|
447
475
|
const modelData = window.SERVER_MODELS[modelId];
|
|
448
476
|
await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
|
|
@@ -451,6 +479,9 @@ async function installModel(modelId) {
|
|
|
451
479
|
body: JSON.stringify({ model_name: modelId, ...modelData })
|
|
452
480
|
});
|
|
453
481
|
|
|
482
|
+
// Download complete - remove from active operations
|
|
483
|
+
activeOperations.delete(modelId);
|
|
484
|
+
|
|
454
485
|
// Refresh installed models and model status
|
|
455
486
|
await fetchInstalledModels();
|
|
456
487
|
await updateModelStatusIndicator();
|
|
@@ -468,6 +499,9 @@ async function installModel(modelId) {
|
|
|
468
499
|
console.error('Error installing model:', error);
|
|
469
500
|
showErrorBanner('Failed to install model: ' + error.message);
|
|
470
501
|
|
|
502
|
+
// Remove from active operations on error too
|
|
503
|
+
activeOperations.delete(modelId);
|
|
504
|
+
|
|
471
505
|
// Reset button state on error
|
|
472
506
|
if (installBtn) {
|
|
473
507
|
installBtn.disabled = false;
|
|
@@ -505,7 +539,11 @@ async function deleteModel(modelId) {
|
|
|
505
539
|
headers: { 'Content-Type': 'application/json' },
|
|
506
540
|
body: JSON.stringify({ model_name: modelId })
|
|
507
541
|
});
|
|
508
|
-
|
|
542
|
+
installedModels.delete(modelId);
|
|
543
|
+
// Remove custom models from SERVER_MODELS to prevent them from reappearing without having to do a manual refresh
|
|
544
|
+
if (modelId.startsWith('user.')) {
|
|
545
|
+
delete window.SERVER_MODELS[modelId];
|
|
546
|
+
}
|
|
509
547
|
// Refresh installed models and model status
|
|
510
548
|
await fetchInstalledModels();
|
|
511
549
|
await updateModelStatusIndicator();
|
|
@@ -597,29 +635,11 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|
|
597
635
|
unloadBtn.onclick = unloadModel;
|
|
598
636
|
}
|
|
599
637
|
|
|
600
|
-
const modelSelect = document.getElementById('model-select');
|
|
601
|
-
if (modelSelect) {
|
|
602
|
-
modelSelect.addEventListener('change', async function() {
|
|
603
|
-
const modelId = this.value;
|
|
604
|
-
if (modelId) {
|
|
605
|
-
await loadModelStandardized(modelId, {
|
|
606
|
-
onSuccess: (loadedModelId) => {
|
|
607
|
-
console.log(`Model ${loadedModelId} loaded successfully`);
|
|
608
|
-
},
|
|
609
|
-
onError: (error, failedModelId) => {
|
|
610
|
-
console.error(`Failed to load model ${failedModelId}:`, error);
|
|
611
|
-
showErrorBanner('Failed to load model: ' + error.message);
|
|
612
|
-
}
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
|
|
618
638
|
// Initial fetch of model data - this will populate installedModels
|
|
619
639
|
await updateModelStatusIndicator();
|
|
620
640
|
|
|
621
|
-
//
|
|
622
|
-
|
|
641
|
+
// Update category visibility on initial load
|
|
642
|
+
updateCategoryVisibility();
|
|
623
643
|
|
|
624
644
|
// Initialize model browser with hot models
|
|
625
645
|
displayHotModels();
|
|
@@ -635,6 +655,46 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|
|
635
655
|
|
|
636
656
|
// Set up register model form
|
|
637
657
|
setupRegisterModelForm();
|
|
658
|
+
setupFolderSelection();
|
|
659
|
+
|
|
660
|
+
// Set up smart periodic refresh to detect external model changes
|
|
661
|
+
// Poll every 15 seconds (much less aggressive than 1 second)
|
|
662
|
+
// Only poll when page is visible to save resources
|
|
663
|
+
let pollInterval = null;
|
|
664
|
+
|
|
665
|
+
function startPolling() {
|
|
666
|
+
if (!pollInterval) {
|
|
667
|
+
pollInterval = setInterval(async () => {
|
|
668
|
+
// Only update if page is visible AND no active operations
|
|
669
|
+
// Skip polling during downloads/loads to prevent false positives
|
|
670
|
+
if (document.visibilityState === 'visible' && activeOperations.size === 0) {
|
|
671
|
+
await updateModelStatusIndicator();
|
|
672
|
+
}
|
|
673
|
+
}, 15000); // Check every 15 seconds
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function stopPolling() {
|
|
678
|
+
if (pollInterval) {
|
|
679
|
+
clearInterval(pollInterval);
|
|
680
|
+
pollInterval = null;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Start polling when page is visible, stop when hidden
|
|
685
|
+
document.addEventListener('visibilitychange', () => {
|
|
686
|
+
if (document.visibilityState === 'visible') {
|
|
687
|
+
// Page became visible - update immediately and resume polling
|
|
688
|
+
updateModelStatusIndicator();
|
|
689
|
+
startPolling();
|
|
690
|
+
} else {
|
|
691
|
+
// Page hidden - stop polling to save resources
|
|
692
|
+
stopPolling();
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// Start polling initially
|
|
697
|
+
startPolling();
|
|
638
698
|
});
|
|
639
699
|
|
|
640
700
|
// Toggle Add Model form
|
|
@@ -675,12 +735,20 @@ function renderModelTable(tbody, models, allModels, emptyMessage) {
|
|
|
675
735
|
btn.disabled = true;
|
|
676
736
|
btn.textContent = '⏳';
|
|
677
737
|
btn.classList.add('installing-btn');
|
|
738
|
+
|
|
739
|
+
// Track this download as active
|
|
740
|
+
activeOperations.add(mid);
|
|
741
|
+
|
|
678
742
|
try {
|
|
679
743
|
await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
|
|
680
744
|
method: 'POST',
|
|
681
745
|
headers: { 'Content-Type': 'application/json' },
|
|
682
746
|
body: JSON.stringify({ model_name: mid })
|
|
683
747
|
});
|
|
748
|
+
|
|
749
|
+
// Download complete - remove from active operations
|
|
750
|
+
activeOperations.delete(mid);
|
|
751
|
+
|
|
684
752
|
await refreshModelMgmtUI();
|
|
685
753
|
// Update chat dropdown too if loadModels function exists
|
|
686
754
|
if (typeof loadModels === 'function') {
|
|
@@ -690,6 +758,9 @@ function renderModelTable(tbody, models, allModels, emptyMessage) {
|
|
|
690
758
|
btn.textContent = 'Error';
|
|
691
759
|
btn.disabled = false;
|
|
692
760
|
showErrorBanner(`Failed to install model: ${e.message}`);
|
|
761
|
+
|
|
762
|
+
// Remove from active operations on error too
|
|
763
|
+
activeOperations.delete(mid);
|
|
693
764
|
}
|
|
694
765
|
};
|
|
695
766
|
tdBtn.appendChild(btn);
|
|
@@ -928,7 +999,17 @@ function setupRegisterModelForm() {
|
|
|
928
999
|
if (!name.startsWith('user.')) {
|
|
929
1000
|
name = 'user.' + name;
|
|
930
1001
|
}
|
|
931
|
-
|
|
1002
|
+
|
|
1003
|
+
// Check if model name already exists
|
|
1004
|
+
const allModels = window.SERVER_MODELS || {};
|
|
1005
|
+
if (allModels[name] || installedModels.has(name)) {
|
|
1006
|
+
showErrorBanner('Model name already exists. Please enter a different name.');
|
|
1007
|
+
registerStatus.textContent = 'Model name already exists';
|
|
1008
|
+
registerStatus.style.color = '#b10819ff';
|
|
1009
|
+
registerStatus.className = 'register-status error';
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
932
1013
|
const checkpoint = document.getElementById('register-checkpoint').value.trim();
|
|
933
1014
|
const recipe = document.getElementById('register-recipe').value;
|
|
934
1015
|
const reasoning = document.getElementById('register-reasoning').checked;
|
|
@@ -939,24 +1020,70 @@ function setupRegisterModelForm() {
|
|
|
939
1020
|
return;
|
|
940
1021
|
}
|
|
941
1022
|
|
|
942
|
-
const payload = { model_name: name, recipe, reasoning, vision };
|
|
943
|
-
if (checkpoint) payload.checkpoint = checkpoint;
|
|
944
|
-
if (mmproj) payload.mmproj = mmproj;
|
|
945
|
-
|
|
946
1023
|
const btn = document.getElementById('register-submit');
|
|
947
1024
|
btn.disabled = true;
|
|
948
1025
|
btn.textContent = 'Installing...';
|
|
949
1026
|
|
|
950
1027
|
try {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1028
|
+
if (isLocalModel && selectedModelFiles) {
|
|
1029
|
+
if (recipe === 'llamacpp' && !Array.from(selectedModelFiles).some(file => file.name.toLowerCase().endsWith('.gguf'))) {
|
|
1030
|
+
throw new Error('No .gguf files found in the selected folder for llamacpp');
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const formData = new FormData();
|
|
1034
|
+
formData.append('model_name', name);
|
|
1035
|
+
formData.append('checkpoint', checkpoint);
|
|
1036
|
+
formData.append('recipe', recipe);
|
|
1037
|
+
formData.append('reasoning', reasoning);
|
|
1038
|
+
formData.append('vision', vision);
|
|
1039
|
+
if (mmproj) formData.append('mmproj', mmproj);
|
|
1040
|
+
Array.from(selectedModelFiles).forEach(file => {
|
|
1041
|
+
formData.append('model_files', file, file.webkitRelativePath);
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/add-local-model', {
|
|
1045
|
+
method: 'POST',
|
|
1046
|
+
body: formData
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
else {
|
|
1050
|
+
if (!checkpoint) {
|
|
1051
|
+
throw new Error('Checkpoint is required for remote models');
|
|
1052
|
+
}
|
|
1053
|
+
const payload = { model_name: name, recipe, reasoning, vision };
|
|
1054
|
+
if (checkpoint) payload.checkpoint = checkpoint;
|
|
1055
|
+
if (mmproj) payload.mmproj = mmproj;
|
|
1056
|
+
|
|
1057
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
|
|
1058
|
+
method: 'POST',
|
|
1059
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1060
|
+
body: JSON.stringify(payload)
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
956
1064
|
registerStatus.textContent = 'Model installed!';
|
|
957
|
-
registerStatus.style.color = '#
|
|
1065
|
+
registerStatus.style.color = '#0eaf51ff';
|
|
958
1066
|
registerStatus.className = 'register-status success';
|
|
1067
|
+
|
|
1068
|
+
// Add custom model to SERVER_MODELS so it appears in the UI without having to do a manual refresh
|
|
1069
|
+
if (name.startsWith('user.')) {
|
|
1070
|
+
const labels = ['custom'];
|
|
1071
|
+
if (vision) labels.push('vision');
|
|
1072
|
+
if (reasoning) labels.push('reasoning');
|
|
1073
|
+
|
|
1074
|
+
window.SERVER_MODELS[name] = {
|
|
1075
|
+
recipe: recipe,
|
|
1076
|
+
labels: labels
|
|
1077
|
+
};
|
|
1078
|
+
if (checkpoint) window.SERVER_MODELS[name].checkpoint = checkpoint;
|
|
1079
|
+
if (mmproj) window.SERVER_MODELS[name].mmproj = mmproj;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
959
1082
|
registerForm.reset();
|
|
1083
|
+
isLocalModel = false;
|
|
1084
|
+
selectedModelFiles = null;
|
|
1085
|
+
document.getElementById('folder-input').value = '';
|
|
1086
|
+
|
|
960
1087
|
await refreshModelMgmtUI();
|
|
961
1088
|
// Update chat dropdown too if loadModels function exists
|
|
962
1089
|
if (typeof loadModels === 'function') {
|
|
@@ -975,6 +1102,278 @@ function setupRegisterModelForm() {
|
|
|
975
1102
|
};
|
|
976
1103
|
}
|
|
977
1104
|
}
|
|
1105
|
+
let isLocalModel = false;
|
|
1106
|
+
let selectedModelFiles = null;
|
|
1107
|
+
// Helper function to find mmproj file in selected folder
|
|
1108
|
+
function findMmprojFile(files) {
|
|
1109
|
+
for (let i = 0; i < files.length; i++) {
|
|
1110
|
+
const file = files[i];
|
|
1111
|
+
const fileName = file.name.toLowerCase();
|
|
1112
|
+
const relativePath = file.webkitRelativePath;
|
|
1113
|
+
|
|
1114
|
+
// Check if file contains 'mmproj' and has .gguf extension
|
|
1115
|
+
if (fileName.includes('mmproj') && fileName.endsWith('.gguf')) {
|
|
1116
|
+
// Return just the filename (last part of the path)
|
|
1117
|
+
return relativePath.split('/').pop();
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Helper function to find all non-mmproj GGUF files in selected folder
|
|
1124
|
+
function findGgufFiles(files) {
|
|
1125
|
+
const ggufFiles = [];
|
|
1126
|
+
for (let i = 0; i < files.length; i++) {
|
|
1127
|
+
const file = files[i];
|
|
1128
|
+
const fileName = file.name.toLowerCase();
|
|
1129
|
+
const relativePath = file.webkitRelativePath;
|
|
1130
|
+
|
|
1131
|
+
// Check if file has .gguf extension but is NOT an mmproj file
|
|
1132
|
+
if (fileName.endsWith('.gguf') && !fileName.includes('mmproj')) {
|
|
1133
|
+
// Store just the filename (last part of the path)
|
|
1134
|
+
ggufFiles.push(relativePath.split('/').pop());
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return ggufFiles;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Helper function to check GGUF files and show appropriate banners
|
|
1141
|
+
function checkGgufFilesAndShowBanner(files) {
|
|
1142
|
+
const recipeSelect = document.getElementById('register-recipe');
|
|
1143
|
+
|
|
1144
|
+
// Only check if llamacpp is selected
|
|
1145
|
+
if (!recipeSelect || recipeSelect.value !== 'llamacpp') {
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const mmprojFile = findMmprojFile(files);
|
|
1150
|
+
const ggufFiles = findGgufFiles(files);
|
|
1151
|
+
|
|
1152
|
+
// Hide any existing banners first
|
|
1153
|
+
hideErrorBanner();
|
|
1154
|
+
|
|
1155
|
+
if (ggufFiles.length > 1) {
|
|
1156
|
+
// Multiple GGUF files detected
|
|
1157
|
+
const folderPath = files[0].webkitRelativePath.split('/')[0];
|
|
1158
|
+
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]}`;
|
|
1159
|
+
|
|
1160
|
+
if (mmprojFile) {
|
|
1161
|
+
bannerMsg += `\n\nDon't forget to enter the mmproj file name and check the 'vision' checkbox if it is a vision model.`;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
showBanner(bannerMsg, 'warning');
|
|
1165
|
+
} else if (mmprojFile) {
|
|
1166
|
+
// MMproj detected
|
|
1167
|
+
showBanner("MMproj detected and populated. Please validate the file name and check the 'vision' checkbox if it is a vision model.", 'success');
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
// Helper function to auto-fill mmproj field if llamacpp is selected
|
|
1171
|
+
function autoFillMmproj() {
|
|
1172
|
+
const recipeSelect = document.getElementById('register-recipe');
|
|
1173
|
+
const mmprojInput = document.getElementById('register-mmproj');
|
|
1174
|
+
|
|
1175
|
+
if (recipeSelect && mmprojInput && isLocalModel && selectedModelFiles) {
|
|
1176
|
+
const selectedRecipe = recipeSelect.value;
|
|
1177
|
+
|
|
1178
|
+
if (selectedRecipe === 'llamacpp') {
|
|
1179
|
+
const mmprojFile = findMmprojFile(selectedModelFiles);
|
|
1180
|
+
if (mmprojFile) {
|
|
1181
|
+
mmprojInput.value = mmprojFile;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Check GGUF files and show appropriate banner
|
|
1185
|
+
checkGgufFilesAndShowBanner(selectedModelFiles);
|
|
1186
|
+
} else {
|
|
1187
|
+
// Hide banners if not llamacpp
|
|
1188
|
+
hideErrorBanner();
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
function setupFolderSelection() {
|
|
1193
|
+
const selectFolderBtn = document.getElementById('select-folder-btn');
|
|
1194
|
+
const folderInput = document.getElementById('folder-input');
|
|
1195
|
+
const checkpointInput = document.getElementById('register-checkpoint');
|
|
1196
|
+
const recipeSelect = document.getElementById('register-recipe');
|
|
1197
|
+
|
|
1198
|
+
if (selectFolderBtn && folderInput && checkpointInput) {
|
|
1199
|
+
selectFolderBtn.addEventListener('click', () => {
|
|
1200
|
+
folderInput.click();
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
folderInput.addEventListener('change', (event) => {
|
|
1204
|
+
const files = event.target.files;
|
|
1205
|
+
if (files.length > 0) {
|
|
1206
|
+
const firstFile = files[0];
|
|
1207
|
+
const folderPath = firstFile.webkitRelativePath.split('/')[0];
|
|
1208
|
+
checkpointInput.value = folderPath;
|
|
1209
|
+
isLocalModel = true;
|
|
1210
|
+
selectedModelFiles = files;
|
|
1211
|
+
|
|
1212
|
+
// Auto-fill mmproj if llamacpp is already selected
|
|
1213
|
+
autoFillMmproj();
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
isLocalModel = false;
|
|
1217
|
+
selectedModelFiles = null;
|
|
1218
|
+
checkpointInput.value = '';
|
|
1219
|
+
hideErrorBanner();
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
// Add listener to recipe dropdown to auto-fill mmproj when changed to llamacpp
|
|
1224
|
+
if (recipeSelect) {
|
|
1225
|
+
recipeSelect.addEventListener('change', () => {
|
|
1226
|
+
autoFillMmproj();
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
// === Migration/Cleanup Functions ===
|
|
1232
|
+
|
|
1233
|
+
// Store incompatible models data globally
|
|
1234
|
+
let incompatibleModelsData = null;
|
|
1235
|
+
|
|
1236
|
+
// Check for incompatible models on page load
|
|
1237
|
+
async function checkIncompatibleModels() {
|
|
1238
|
+
try {
|
|
1239
|
+
const response = await httpJson(getServerBaseUrl() + '/api/v1/migration/incompatible-models');
|
|
1240
|
+
incompatibleModelsData = response;
|
|
1241
|
+
|
|
1242
|
+
if (response.count > 0) {
|
|
1243
|
+
showMigrationBanner(response.count, response.total_size);
|
|
1244
|
+
}
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
console.error('Error checking for incompatible models:', error);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Show migration banner
|
|
1251
|
+
function showMigrationBanner(count, totalSize) {
|
|
1252
|
+
const banner = document.getElementById('migration-banner');
|
|
1253
|
+
const msg = document.getElementById('migration-banner-msg');
|
|
1254
|
+
|
|
1255
|
+
const sizeGB = (totalSize / (1024 * 1024 * 1024)).toFixed(1);
|
|
1256
|
+
msg.textContent = `Found ${count} incompatible RyzenAI model${count > 1 ? 's' : ''} (${sizeGB} GB). Clean up to free disk space.`;
|
|
1257
|
+
banner.style.display = 'flex';
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// Hide migration banner
|
|
1261
|
+
function hideMigrationBanner() {
|
|
1262
|
+
const banner = document.getElementById('migration-banner');
|
|
1263
|
+
banner.style.display = 'none';
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Show migration modal with model list
|
|
1267
|
+
function showMigrationModal() {
|
|
1268
|
+
if (!incompatibleModelsData || incompatibleModelsData.count === 0) {
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const modal = document.getElementById('migration-modal');
|
|
1273
|
+
const modelList = document.getElementById('migration-model-list');
|
|
1274
|
+
const totalSize = document.getElementById('migration-total-size');
|
|
1275
|
+
|
|
1276
|
+
// Populate model list
|
|
1277
|
+
modelList.innerHTML = '';
|
|
1278
|
+
incompatibleModelsData.models.forEach(model => {
|
|
1279
|
+
const item = document.createElement('div');
|
|
1280
|
+
item.className = 'migration-model-item';
|
|
1281
|
+
|
|
1282
|
+
const nameSpan = document.createElement('span');
|
|
1283
|
+
nameSpan.className = 'migration-model-name';
|
|
1284
|
+
nameSpan.textContent = model.name;
|
|
1285
|
+
|
|
1286
|
+
const sizeSpan = document.createElement('span');
|
|
1287
|
+
sizeSpan.className = 'migration-model-size';
|
|
1288
|
+
sizeSpan.textContent = model.size_formatted;
|
|
1289
|
+
|
|
1290
|
+
item.appendChild(nameSpan);
|
|
1291
|
+
item.appendChild(sizeSpan);
|
|
1292
|
+
modelList.appendChild(item);
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
// Set total size
|
|
1296
|
+
const sizeGB = (incompatibleModelsData.total_size / (1024 * 1024 * 1024)).toFixed(1);
|
|
1297
|
+
totalSize.textContent = `${sizeGB} GB`;
|
|
1298
|
+
|
|
1299
|
+
modal.style.display = 'flex';
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Hide migration modal
|
|
1303
|
+
function hideMigrationModal() {
|
|
1304
|
+
const modal = document.getElementById('migration-modal');
|
|
1305
|
+
modal.style.display = 'none';
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Delete incompatible models
|
|
1309
|
+
async function deleteIncompatibleModels() {
|
|
1310
|
+
if (!incompatibleModelsData || incompatibleModelsData.count === 0) {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const modelPaths = incompatibleModelsData.models.map(m => m.path);
|
|
1315
|
+
|
|
1316
|
+
try {
|
|
1317
|
+
// Disable buttons during deletion
|
|
1318
|
+
const deleteBtn = document.querySelector('.delete-btn');
|
|
1319
|
+
const cancelBtn = document.querySelector('.cancel-btn');
|
|
1320
|
+
deleteBtn.disabled = true;
|
|
1321
|
+
cancelBtn.disabled = true;
|
|
1322
|
+
deleteBtn.textContent = 'Deleting...';
|
|
1323
|
+
|
|
1324
|
+
const response = await httpRequest(getServerBaseUrl() + '/api/v1/migration/cleanup', {
|
|
1325
|
+
method: 'POST',
|
|
1326
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1327
|
+
body: JSON.stringify({ model_paths: modelPaths })
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
const result = await response.json();
|
|
1331
|
+
|
|
1332
|
+
// Close modal
|
|
1333
|
+
hideMigrationModal();
|
|
1334
|
+
|
|
1335
|
+
// Hide banner
|
|
1336
|
+
hideMigrationBanner();
|
|
1337
|
+
|
|
1338
|
+
// Show success message
|
|
1339
|
+
showSuccessMessage(`Successfully deleted ${result.success_count} model${result.success_count > 1 ? 's' : ''}, freed ${result.freed_size_formatted}`);
|
|
1340
|
+
|
|
1341
|
+
// Clear cached data
|
|
1342
|
+
incompatibleModelsData = null;
|
|
1343
|
+
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
console.error('Error deleting incompatible models:', error);
|
|
1346
|
+
showErrorBanner('Failed to delete models: ' + error.message);
|
|
1347
|
+
|
|
1348
|
+
// Re-enable buttons
|
|
1349
|
+
const deleteBtn = document.querySelector('.delete-btn');
|
|
1350
|
+
const cancelBtn = document.querySelector('.cancel-btn');
|
|
1351
|
+
deleteBtn.disabled = false;
|
|
1352
|
+
cancelBtn.disabled = false;
|
|
1353
|
+
deleteBtn.textContent = 'Delete All';
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// Show success message (reuse error banner with green color)
|
|
1358
|
+
function showSuccessMessage(message) {
|
|
1359
|
+
const banner = document.getElementById('error-banner');
|
|
1360
|
+
const msg = document.getElementById('error-banner-msg');
|
|
1361
|
+
msg.textContent = message;
|
|
1362
|
+
banner.style.backgroundColor = '#2d7f47';
|
|
1363
|
+
banner.style.display = 'flex';
|
|
1364
|
+
|
|
1365
|
+
// Auto-hide after 5 seconds
|
|
1366
|
+
setTimeout(() => {
|
|
1367
|
+
banner.style.display = 'none';
|
|
1368
|
+
banner.style.backgroundColor = ''; // Reset to default
|
|
1369
|
+
}, 5000);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// Check for incompatible models when page loads
|
|
1373
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1374
|
+
// Run check after a short delay to let the page load
|
|
1375
|
+
setTimeout(checkIncompatibleModels, 1000);
|
|
1376
|
+
});
|
|
978
1377
|
|
|
979
1378
|
// Make functions globally available for HTML onclick handlers and other components
|
|
980
1379
|
window.toggleCategory = toggleCategory;
|
|
@@ -983,4 +1382,8 @@ window.selectLabel = selectLabel;
|
|
|
983
1382
|
window.showAddModelForm = showAddModelForm;
|
|
984
1383
|
window.unloadModel = unloadModel;
|
|
985
1384
|
window.installModel = installModel;
|
|
986
|
-
window.deleteModel = deleteModel;
|
|
1385
|
+
window.deleteModel = deleteModel;
|
|
1386
|
+
window.showMigrationModal = showMigrationModal;
|
|
1387
|
+
window.hideMigrationModal = hideMigrationModal;
|
|
1388
|
+
window.hideMigrationBanner = hideMigrationBanner;
|
|
1389
|
+
window.deleteIncompatibleModels = deleteIncompatibleModels;
|