lemonade-sdk 9.1.1__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.
Files changed (84) hide show
  1. lemonade/__init__.py +5 -0
  2. lemonade/api.py +180 -0
  3. lemonade/cache.py +92 -0
  4. lemonade/cli.py +173 -0
  5. lemonade/common/__init__.py +0 -0
  6. lemonade/common/build.py +176 -0
  7. lemonade/common/cli_helpers.py +139 -0
  8. lemonade/common/exceptions.py +98 -0
  9. lemonade/common/filesystem.py +368 -0
  10. lemonade/common/inference_engines.py +408 -0
  11. lemonade/common/network.py +93 -0
  12. lemonade/common/printing.py +110 -0
  13. lemonade/common/status.py +471 -0
  14. lemonade/common/system_info.py +1411 -0
  15. lemonade/common/test_helpers.py +28 -0
  16. lemonade/profilers/__init__.py +1 -0
  17. lemonade/profilers/agt_power.py +437 -0
  18. lemonade/profilers/hwinfo_power.py +429 -0
  19. lemonade/profilers/memory_tracker.py +259 -0
  20. lemonade/profilers/profiler.py +58 -0
  21. lemonade/sequence.py +363 -0
  22. lemonade/state.py +159 -0
  23. lemonade/tools/__init__.py +1 -0
  24. lemonade/tools/accuracy.py +432 -0
  25. lemonade/tools/adapter.py +114 -0
  26. lemonade/tools/bench.py +302 -0
  27. lemonade/tools/flm/__init__.py +1 -0
  28. lemonade/tools/flm/utils.py +305 -0
  29. lemonade/tools/huggingface/bench.py +187 -0
  30. lemonade/tools/huggingface/load.py +235 -0
  31. lemonade/tools/huggingface/utils.py +359 -0
  32. lemonade/tools/humaneval.py +264 -0
  33. lemonade/tools/llamacpp/bench.py +255 -0
  34. lemonade/tools/llamacpp/load.py +222 -0
  35. lemonade/tools/llamacpp/utils.py +1260 -0
  36. lemonade/tools/management_tools.py +319 -0
  37. lemonade/tools/mmlu.py +319 -0
  38. lemonade/tools/oga/__init__.py +0 -0
  39. lemonade/tools/oga/bench.py +120 -0
  40. lemonade/tools/oga/load.py +804 -0
  41. lemonade/tools/oga/migration.py +403 -0
  42. lemonade/tools/oga/utils.py +462 -0
  43. lemonade/tools/perplexity.py +147 -0
  44. lemonade/tools/prompt.py +263 -0
  45. lemonade/tools/report/__init__.py +0 -0
  46. lemonade/tools/report/llm_report.py +203 -0
  47. lemonade/tools/report/table.py +899 -0
  48. lemonade/tools/server/__init__.py +0 -0
  49. lemonade/tools/server/flm.py +133 -0
  50. lemonade/tools/server/llamacpp.py +320 -0
  51. lemonade/tools/server/serve.py +2123 -0
  52. lemonade/tools/server/static/favicon.ico +0 -0
  53. lemonade/tools/server/static/index.html +279 -0
  54. lemonade/tools/server/static/js/chat.js +1059 -0
  55. lemonade/tools/server/static/js/model-settings.js +183 -0
  56. lemonade/tools/server/static/js/models.js +1395 -0
  57. lemonade/tools/server/static/js/shared.js +556 -0
  58. lemonade/tools/server/static/logs.html +191 -0
  59. lemonade/tools/server/static/styles.css +2654 -0
  60. lemonade/tools/server/static/webapp.html +321 -0
  61. lemonade/tools/server/tool_calls.py +153 -0
  62. lemonade/tools/server/tray.py +664 -0
  63. lemonade/tools/server/utils/macos_tray.py +226 -0
  64. lemonade/tools/server/utils/port.py +77 -0
  65. lemonade/tools/server/utils/thread.py +85 -0
  66. lemonade/tools/server/utils/windows_tray.py +408 -0
  67. lemonade/tools/server/webapp.py +34 -0
  68. lemonade/tools/server/wrapped_server.py +559 -0
  69. lemonade/tools/tool.py +374 -0
  70. lemonade/version.py +1 -0
  71. lemonade_install/__init__.py +1 -0
  72. lemonade_install/install.py +239 -0
  73. lemonade_sdk-9.1.1.dist-info/METADATA +276 -0
  74. lemonade_sdk-9.1.1.dist-info/RECORD +84 -0
  75. lemonade_sdk-9.1.1.dist-info/WHEEL +5 -0
  76. lemonade_sdk-9.1.1.dist-info/entry_points.txt +5 -0
  77. lemonade_sdk-9.1.1.dist-info/licenses/LICENSE +201 -0
  78. lemonade_sdk-9.1.1.dist-info/licenses/NOTICE.md +47 -0
  79. lemonade_sdk-9.1.1.dist-info/top_level.txt +3 -0
  80. lemonade_server/cli.py +805 -0
  81. lemonade_server/model_manager.py +758 -0
  82. lemonade_server/pydantic_models.py +159 -0
  83. lemonade_server/server_models.json +643 -0
  84. lemonade_server/settings.py +39 -0
@@ -0,0 +1,1395 @@
1
+ // Model Management functionality
2
+
3
+ // State variables for model management
4
+ let currentLoadedModel = null;
5
+ let installedModels = new Set(); // Track which models are actually installed
6
+ let activeOperations = new Set(); // Track models currently being downloaded or loaded
7
+
8
+ // Make installedModels and activeOperations accessible globally
9
+ window.installedModels = installedModels;
10
+ window.activeOperations = activeOperations;
11
+ let currentCategory = 'hot';
12
+ let currentFilter = null;
13
+
14
+ // === Model Status Management ===
15
+
16
+ // Fetch installed models from the server
17
+ async function fetchInstalledModels() {
18
+ try {
19
+ const response = await httpJson(getServerBaseUrl() + '/api/v1/models');
20
+ installedModels.clear();
21
+ if (response && response.data) {
22
+ response.data.forEach(model => {
23
+ installedModels.add(model.id);
24
+ });
25
+ }
26
+ } catch (error) {
27
+ console.error('Error fetching installed models:', error);
28
+ // If we can't fetch, assume all are installed to maintain current functionality
29
+ const allModels = window.SERVER_MODELS || {};
30
+ Object.keys(allModels).forEach(modelId => {
31
+ installedModels.add(modelId);
32
+ });
33
+ }
34
+ }
35
+
36
+ // Check health endpoint to get current model status
37
+ async function checkModelHealth() {
38
+ try {
39
+ const response = await httpJson(getServerBaseUrl() + '/api/v1/health');
40
+ return response;
41
+ } catch (error) {
42
+ console.error('Error checking model health:', error);
43
+ return null;
44
+ }
45
+ }
46
+
47
+ // Populate the model dropdown with all installed models
48
+ function populateModelDropdown() {
49
+ const indicator = document.getElementById('model-status-indicator');
50
+ const select = document.getElementById('model-select');
51
+ select.innerHTML = '';
52
+
53
+ // Add the default option
54
+ const defaultOption = document.createElement('option');
55
+ defaultOption.value = '';
56
+ defaultOption.textContent = 'Click to select a model ▼';
57
+ select.appendChild(defaultOption);
58
+
59
+ // Add the hidden 'Server Offline' option
60
+ const hiddenOption = document.createElement('option');
61
+ hiddenOption.value = 'server-offline';
62
+ hiddenOption.textContent = 'Server Offline';
63
+ hiddenOption.hidden = true;
64
+ select.appendChild(hiddenOption);
65
+
66
+ // Get all installed models from the global set
67
+ const sortedModels = Array.from(installedModels).sort();
68
+
69
+ // Add options for each installed model
70
+ sortedModels.forEach(modelId => {
71
+ const option = document.createElement('option');
72
+ option.value = modelId;
73
+ option.textContent = modelId;
74
+ select.appendChild(option);
75
+ });
76
+ }
77
+
78
+ // Update model status indicator
79
+ async function updateModelStatusIndicator() {
80
+ const indicator = document.getElementById('model-status-indicator');
81
+ const select = document.getElementById('model-select');
82
+ const buttonIcons = document.querySelectorAll('button');
83
+
84
+ // Fetch both health and installed models
85
+ const [health] = await Promise.all([
86
+ checkModelHealth(),
87
+ fetchInstalledModels()
88
+ ]);
89
+
90
+ // Populate the dropdown with the newly fetched installed models
91
+ populateModelDropdown();
92
+
93
+ // Refresh model management UI if we're on the models tab
94
+ const modelsTab = document.getElementById('content-models');
95
+ if (modelsTab && modelsTab.classList.contains('active')) {
96
+ // Use the display-only version to avoid re-fetching data we just fetched
97
+ refreshModelMgmtUIDisplay();
98
+
99
+ // Also refresh the model browser to show updated button states
100
+ if (currentCategory === 'hot') displayHotModels();
101
+ else if (currentCategory === 'recipes') displayModelsByRecipe(currentFilter);
102
+ else if (currentCategory === 'labels') displayModelsByLabel(currentFilter);
103
+ }
104
+
105
+ if (health && health.model_loaded) {
106
+ // Model is loaded - show model name with online status
107
+ indicator.classList.remove('online', 'offline', 'loading');
108
+ currentLoadedModel = health.model_loaded;
109
+ indicator.classList.add('loaded');
110
+ select.value = currentLoadedModel;
111
+ select.disabled = false;
112
+ buttonIcons.forEach(btn => btn.disabled = false);
113
+ } else if (health !== null) {
114
+ // Server is online but no model loaded
115
+ indicator.classList.remove('loaded', 'offline', 'loading');
116
+ currentLoadedModel = null;
117
+ indicator.classList.add('online');
118
+ select.value = ''; // Set to the "Click to select a model ▼" option
119
+ select.disabled = false;
120
+ buttonIcons.forEach(btn => btn.disabled = false);
121
+ } else {
122
+ // Server is offline
123
+ indicator.classList.remove('loaded', 'online', 'loading');
124
+ currentLoadedModel = null;
125
+ // Add the hidden 'Server Offline' option
126
+ const hiddenOption = document.createElement('option');
127
+ hiddenOption.value = 'server-offline';
128
+ hiddenOption.textContent = 'Server Offline';
129
+ hiddenOption.hidden = true;
130
+ select.appendChild(hiddenOption);
131
+ indicator.classList.add('offline');
132
+ select.value = 'server-offline';
133
+ select.disabled = true;
134
+ buttonIcons.forEach(btn => btn.disabled = true);
135
+ return;
136
+ }
137
+ }
138
+
139
+ // Unload current model
140
+ async function unloadModel() {
141
+ if (!currentLoadedModel) return;
142
+
143
+ try {
144
+ // Set loading state
145
+ const indicator = document.getElementById('model-status-indicator');
146
+ const select = document.getElementById('model-select');
147
+ indicator.classList.remove('loaded', 'online', 'offline');
148
+ indicator.classList.add('loading');
149
+ select.disabled = true;
150
+ select.value = currentLoadedModel; // Keep the selected model visible during unload
151
+
152
+ await httpRequest(getServerBaseUrl() + '/api/v1/unload', {
153
+ method: 'POST'
154
+ });
155
+
156
+ await updateModelStatusIndicator();
157
+
158
+ // Refresh model list to show updated button states
159
+ if (currentCategory === 'hot') displayHotModels();
160
+ else if (currentCategory === 'recipes') displayModelsByRecipe(currentFilter);
161
+ else if (currentCategory === 'labels') displayModelsByLabel(currentFilter);
162
+ } catch (error) {
163
+ console.error('Error unloading model:', error);
164
+ showErrorBanner('Failed to unload model: ' + error.message);
165
+ await updateModelStatusIndicator(); // Revert state on error
166
+ }
167
+ }
168
+
169
+ // === Model Browser Management ===
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
+
194
+ // Toggle category in model browser (only for Hot Models now)
195
+ function toggleCategory(categoryName) {
196
+ const header = document.querySelector(`[data-category="${categoryName}"] .category-header`);
197
+ const content = document.getElementById(`category-${categoryName}`);
198
+
199
+ if (categoryName === 'hot') {
200
+ // Check if hot models is already selected
201
+ const isCurrentlyActive = header.classList.contains('active');
202
+
203
+ // Clear all other active states
204
+ document.querySelectorAll('.subcategory').forEach(s => s.classList.remove('active'));
205
+
206
+ if (!isCurrentlyActive) {
207
+ // Show hot models
208
+ header.classList.add('active');
209
+ content.classList.add('expanded');
210
+ currentCategory = categoryName;
211
+ currentFilter = null;
212
+ displayHotModels();
213
+ }
214
+ // If already active, keep it active (don't toggle off)
215
+ }
216
+ }
217
+
218
+ // Show add model form in main area
219
+ function showAddModelForm() {
220
+ // Clear all sidebar active states
221
+ document.querySelectorAll('.category-header').forEach(h => h.classList.remove('active'));
222
+ document.querySelectorAll('.category-content').forEach(c => c.classList.remove('expanded'));
223
+ document.querySelectorAll('.subcategory').forEach(s => s.classList.remove('active'));
224
+
225
+ // Highlight "Add a Model" as selected
226
+ const addModelHeader = document.querySelector('[data-category="add"] .category-header');
227
+ if (addModelHeader) {
228
+ addModelHeader.classList.add('active');
229
+ }
230
+
231
+ // Hide model list and show form
232
+ document.getElementById('model-list').style.display = 'none';
233
+ document.getElementById('add-model-form-main').style.display = 'block';
234
+
235
+ // Set current state
236
+ currentCategory = 'add';
237
+ currentFilter = null;
238
+ }
239
+
240
+ // Select recipe filter
241
+ function selectRecipe(recipe) {
242
+ // Clear hot models active state
243
+ document.querySelectorAll('.category-header').forEach(h => h.classList.remove('active'));
244
+ document.querySelectorAll('.category-content').forEach(c => c.classList.remove('expanded'));
245
+
246
+ // Clear all subcategory selections
247
+ document.querySelectorAll('.subcategory').forEach(s => s.classList.remove('active'));
248
+
249
+ // Set this recipe as active
250
+ document.querySelector(`[data-recipe="${recipe}"]`).classList.add('active');
251
+
252
+ currentCategory = 'recipes';
253
+ currentFilter = recipe;
254
+ displayModelsByRecipe(recipe);
255
+ }
256
+
257
+ // Select label filter
258
+ function selectLabel(label) {
259
+ // Clear hot models active state
260
+ document.querySelectorAll('.category-header').forEach(h => h.classList.remove('active'));
261
+ document.querySelectorAll('.category-content').forEach(c => c.classList.remove('expanded'));
262
+
263
+ // Clear all subcategory selections
264
+ document.querySelectorAll('.subcategory').forEach(s => s.classList.remove('active'));
265
+
266
+ // Set this label as active
267
+ document.querySelector(`[data-label="${label}"]`).classList.add('active');
268
+
269
+ currentCategory = 'labels';
270
+ currentFilter = label;
271
+ displayModelsByLabel(label);
272
+ }
273
+
274
+ // Display suggested models (Qwen3-0.6B-GGUF as default)
275
+ function displaySuggestedModels() {
276
+ const modelList = document.getElementById('model-list');
277
+ const allModels = window.SERVER_MODELS || {};
278
+
279
+ modelList.innerHTML = '';
280
+
281
+ // First show Qwen3-0.6B-GGUF as the default suggested model
282
+ if (allModels['Qwen3-0.6B-GGUF']) {
283
+ createModelItem('Qwen3-0.6B-GGUF', allModels['Qwen3-0.6B-GGUF'], modelList);
284
+ }
285
+
286
+ // Then show other suggested models (excluding the one already shown)
287
+ Object.entries(allModels).forEach(([modelId, modelData]) => {
288
+ if (modelData.suggested && modelId !== 'Qwen3-0.6B-GGUF') {
289
+ createModelItem(modelId, modelData, modelList);
290
+ }
291
+ });
292
+
293
+ if (modelList.innerHTML === '') {
294
+ modelList.innerHTML = '<p>No suggested models available</p>';
295
+ }
296
+ }
297
+
298
+ // Display hot models
299
+ function displayHotModels() {
300
+ const modelList = document.getElementById('model-list');
301
+ const addModelForm = document.getElementById('add-model-form-main');
302
+ const allModels = window.SERVER_MODELS || {};
303
+
304
+ // Show model list, hide form
305
+ modelList.style.display = 'block';
306
+ addModelForm.style.display = 'none';
307
+
308
+ modelList.innerHTML = '';
309
+
310
+ Object.entries(allModels).forEach(([modelId, modelData]) => {
311
+ if (modelData.labels && modelData.labels.includes('hot') && (modelData.suggested || installedModels.has(modelId))) {
312
+ createModelItem(modelId, modelData, modelList);
313
+ }
314
+ });
315
+ }
316
+
317
+ // Display models by recipe
318
+ function displayModelsByRecipe(recipe) {
319
+ const modelList = document.getElementById('model-list');
320
+ const addModelForm = document.getElementById('add-model-form-main');
321
+ const allModels = window.SERVER_MODELS || {};
322
+
323
+ // Show model list, hide form
324
+ modelList.style.display = 'block';
325
+ addModelForm.style.display = 'none';
326
+
327
+ modelList.innerHTML = '';
328
+
329
+ // Add FastFlowLM notice if this is the FLM recipe
330
+ if (recipe === 'flm') {
331
+ const notice = document.createElement('div');
332
+ notice.className = 'flm-notice';
333
+ notice.innerHTML = `
334
+ <div class="flm-notice-content">
335
+ <div class="flm-notice-icon">⚠️</div>
336
+ <div class="flm-notice-text">
337
+ <strong><a href="https://github.com/FastFlowLM/FastFlowLM">FastFlowLM (FLM)</a> support in Lemonade is in Early Access.</strong> FLM is free for non-commercial use, however note that commercial licensing terms apply. Installing an FLM model will automatically launch the FLM installer, which will require you to accept the FLM license terms to continue. Contact <a href="mailto:lemonade@amd.com">lemonade@amd.com</a> for inquiries.
338
+ </div>
339
+ </div>
340
+ `;
341
+ modelList.appendChild(notice);
342
+ }
343
+
344
+ Object.entries(allModels).forEach(([modelId, modelData]) => {
345
+ if (modelData.recipe === recipe && (modelData.suggested || installedModels.has(modelId))) {
346
+ createModelItem(modelId, modelData, modelList);
347
+ }
348
+ });
349
+ }
350
+
351
+ // Display models by label
352
+ function displayModelsByLabel(label) {
353
+ const modelList = document.getElementById('model-list');
354
+ const addModelForm = document.getElementById('add-model-form-main');
355
+ const allModels = window.SERVER_MODELS || {};
356
+
357
+ // Show model list, hide form
358
+ modelList.style.display = 'block';
359
+ addModelForm.style.display = 'none';
360
+
361
+ modelList.innerHTML = '';
362
+
363
+ Object.entries(allModels).forEach(([modelId, modelData]) => {
364
+ if (label === 'custom') {
365
+ // Show user-added models (those starting with 'user.')
366
+ if (modelId.startsWith('user.')) {
367
+ createModelItem(modelId, modelData, modelList);
368
+ }
369
+ } else if (modelData.labels && modelData.labels.includes(label) && (modelData.suggested || installedModels.has(modelId))) {
370
+ createModelItem(modelId, modelData, modelList);
371
+ }
372
+ });
373
+ }
374
+
375
+ // Create model item element
376
+ function createModelItem(modelId, modelData, container) {
377
+ const item = document.createElement('div');
378
+ item.className = 'model-item';
379
+
380
+ const info = document.createElement('div');
381
+ info.className = 'model-item-info';
382
+
383
+ const name = document.createElement('div');
384
+ name.className = 'model-item-name';
385
+ name.appendChild(createModelNameWithLabels(modelId, window.SERVER_MODELS || {}));
386
+
387
+ info.appendChild(name);
388
+
389
+ // Only add description if it exists and is not empty
390
+ if (modelData.description && modelData.description.trim()) {
391
+ const description = document.createElement('div');
392
+ description.className = 'model-item-description';
393
+ description.textContent = modelData.description;
394
+ info.appendChild(description);
395
+ }
396
+
397
+ const actions = document.createElement('div');
398
+ actions.className = 'model-item-actions';
399
+
400
+ // Check if model is actually installed by looking at the installedModels set
401
+ const isInstalled = installedModels.has(modelId);
402
+ const isLoaded = currentLoadedModel === modelId;
403
+
404
+ if (!isInstalled) {
405
+ const installBtn = document.createElement('button');
406
+ installBtn.className = 'model-item-btn install';
407
+ installBtn.textContent = '📥';
408
+ installBtn.title = 'Install';
409
+ installBtn.onclick = () => installModel(modelId);
410
+ actions.appendChild(installBtn);
411
+ } else {
412
+ if (isLoaded) {
413
+ const unloadBtn = document.createElement('button');
414
+ unloadBtn.className = 'model-item-btn unload';
415
+ unloadBtn.textContent = '⏏️';
416
+ unloadBtn.title = 'Unload';
417
+ unloadBtn.onclick = () => unloadModel();
418
+ actions.appendChild(unloadBtn);
419
+ } else {
420
+ const loadBtn = document.createElement('button');
421
+ const modelSelect = document.getElementById('model-select');
422
+ loadBtn.className = 'model-item-btn load';
423
+ loadBtn.textContent = '🚀';
424
+ loadBtn.title = 'Load';
425
+ loadBtn.onclick = () => {
426
+ loadModelStandardized(modelId, {
427
+ loadButton: loadBtn,
428
+ onSuccess: (loadedModelId) => {
429
+ console.log(`Model ${loadedModelId} loaded successfully`);
430
+ },
431
+ onError: (error, failedModelId) => {
432
+ console.error(`Failed to load model ${failedModelId}:`, error);
433
+ showErrorBanner('Failed to load model: ' + error.message);
434
+ }
435
+ });
436
+ };
437
+ actions.appendChild(loadBtn);
438
+ }
439
+
440
+ const deleteBtn = document.createElement('button');
441
+ deleteBtn.className = 'model-item-btn delete';
442
+ deleteBtn.textContent = '🗑️';
443
+ deleteBtn.title = 'Delete';
444
+ deleteBtn.onclick = () => deleteModel(modelId);
445
+ actions.appendChild(deleteBtn);
446
+ }
447
+
448
+ item.appendChild(info);
449
+ item.appendChild(actions);
450
+ container.appendChild(item);
451
+ }
452
+
453
+ // Install model
454
+ async function installModel(modelId) {
455
+ // Find the install button and show loading state
456
+ const modelItems = document.querySelectorAll('.model-item');
457
+ let installBtn = null;
458
+
459
+ modelItems.forEach(item => {
460
+ const nameElement = item.querySelector('.model-item-name .model-labels-container span');
461
+ if (nameElement && nameElement.getAttribute('data-model-id') === modelId) {
462
+ installBtn = item.querySelector('.model-item-btn.install');
463
+ }
464
+ });
465
+
466
+ if (installBtn) {
467
+ installBtn.disabled = true;
468
+ installBtn.textContent = '⏳';
469
+ }
470
+
471
+ // Track this download as active
472
+ activeOperations.add(modelId);
473
+
474
+ try {
475
+ // For registered models, only send model_name (per API spec)
476
+ await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
477
+ method: 'POST',
478
+ headers: { 'Content-Type': 'application/json' },
479
+ body: JSON.stringify({ model_name: modelId })
480
+ });
481
+
482
+ // Download complete - remove from active operations
483
+ activeOperations.delete(modelId);
484
+
485
+ // Refresh installed models and model status
486
+ await fetchInstalledModels();
487
+ await updateModelStatusIndicator();
488
+
489
+ // Refresh model dropdown in chat
490
+ if (window.initializeModelDropdown) {
491
+ window.initializeModelDropdown();
492
+ }
493
+
494
+ // Refresh model list
495
+ if (currentCategory === 'hot') displayHotModels();
496
+ else if (currentCategory === 'recipes') displayModelsByRecipe(currentFilter);
497
+ else if (currentCategory === 'labels') displayModelsByLabel(currentFilter);
498
+ } catch (error) {
499
+ console.error('Error installing model:', error);
500
+ showErrorBanner('Failed to install model: ' + error.message);
501
+
502
+ // Remove from active operations on error too
503
+ activeOperations.delete(modelId);
504
+
505
+ // Reset button state on error
506
+ if (installBtn) {
507
+ installBtn.disabled = false;
508
+ installBtn.textContent = '📥';
509
+ }
510
+ }
511
+ }
512
+
513
+
514
+ // Delete model
515
+ async function deleteModel(modelId) {
516
+ if (!confirm(`Are you sure you want to delete the model "${modelId}"?`)) {
517
+ return;
518
+ }
519
+
520
+ // Find the delete button and show loading state
521
+ const modelItems = document.querySelectorAll('.model-item');
522
+ let deleteBtn = null;
523
+
524
+ modelItems.forEach(item => {
525
+ const nameElement = item.querySelector('.model-item-name .model-labels-container span');
526
+ if (nameElement && nameElement.getAttribute('data-model-id') === modelId) {
527
+ deleteBtn = item.querySelector('.model-item-btn.delete');
528
+ }
529
+ });
530
+
531
+ if (deleteBtn) {
532
+ deleteBtn.disabled = true;
533
+ deleteBtn.textContent = '⏳';
534
+ }
535
+
536
+ try {
537
+ await httpRequest(getServerBaseUrl() + '/api/v1/delete', {
538
+ method: 'POST',
539
+ headers: { 'Content-Type': 'application/json' },
540
+ body: JSON.stringify({ model_name: modelId })
541
+ });
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
+ }
547
+ // Refresh installed models and model status
548
+ await fetchInstalledModels();
549
+ await updateModelStatusIndicator();
550
+
551
+ // Refresh model dropdown in chat
552
+ if (window.initializeModelDropdown) {
553
+ window.initializeModelDropdown();
554
+ }
555
+
556
+ // Refresh model list
557
+ if (currentCategory === 'hot') displayHotModels();
558
+ else if (currentCategory === 'recipes') displayModelsByRecipe(currentFilter);
559
+ else if (currentCategory === 'labels') displayModelsByLabel(currentFilter);
560
+ } catch (error) {
561
+ console.error('Error deleting model:', error);
562
+ showErrorBanner('Failed to delete model: ' + error.message);
563
+
564
+ // Reset button state on error
565
+ if (deleteBtn) {
566
+ deleteBtn.disabled = false;
567
+ deleteBtn.textContent = '🗑️';
568
+ }
569
+ }
570
+ }
571
+
572
+ // === Model Name Display ===
573
+
574
+ // Create model name with labels
575
+ function createModelNameWithLabels(modelId, serverModels) {
576
+ const modelData = serverModels[modelId];
577
+ const container = document.createElement('div');
578
+ container.className = 'model-labels-container';
579
+
580
+ // Model name
581
+ const nameSpan = document.createElement('span');
582
+
583
+ // Store the original modelId as a data attribute for button finding
584
+ nameSpan.setAttribute('data-model-id', modelId);
585
+
586
+ // Append size if available
587
+ let displayName = modelId;
588
+ if (modelData && typeof modelData.size === 'number') {
589
+ displayName += ` (${modelData.size} GB)`;
590
+ }
591
+ nameSpan.textContent = displayName;
592
+ container.appendChild(nameSpan);
593
+
594
+ // Labels
595
+ if (modelData && modelData.labels && Array.isArray(modelData.labels)) {
596
+ modelData.labels.forEach(label => {
597
+ const labelLower = label.toLowerCase();
598
+
599
+ // Skip "hot" labels since they have their own section
600
+ if (labelLower === 'hot') {
601
+ return;
602
+ }
603
+
604
+ const labelSpan = document.createElement('span');
605
+ let labelClass = 'other';
606
+ if (labelLower === 'vision') {
607
+ labelClass = 'vision';
608
+ } else if (labelLower === 'embeddings') {
609
+ labelClass = 'embeddings';
610
+ } else if (labelLower === 'reasoning') {
611
+ labelClass = 'reasoning';
612
+ } else if (labelLower === 'reranking') {
613
+ labelClass = 'reranking';
614
+ } else if (labelLower === 'coding') {
615
+ labelClass = 'coding';
616
+ } else if (labelLower === 'tool-calling') {
617
+ labelClass = 'tool-calling';
618
+ }
619
+ labelSpan.className = `model-label ${labelClass}`;
620
+ labelSpan.textContent = label;
621
+ container.appendChild(labelSpan);
622
+ });
623
+ }
624
+
625
+ return container;
626
+ }
627
+
628
+ // === Model Management Table (for models tab) ===
629
+
630
+ // Initialize model management functionality when DOM is loaded
631
+ document.addEventListener('DOMContentLoaded', async function() {
632
+ // Set up model status controls
633
+ const unloadBtn = document.getElementById('model-unload-btn');
634
+ if (unloadBtn) {
635
+ unloadBtn.onclick = unloadModel;
636
+ }
637
+
638
+ // Initial fetch of model data - this will populate installedModels
639
+ await updateModelStatusIndicator();
640
+
641
+ // Update category visibility on initial load
642
+ updateCategoryVisibility();
643
+
644
+ // Initialize model browser with hot models
645
+ displayHotModels();
646
+
647
+ // Initial load of model management UI - this will use the populated installedModels
648
+ await refreshModelMgmtUI();
649
+
650
+ // Refresh when switching to the models tab
651
+ const modelsTab = document.getElementById('tab-models');
652
+ if (modelsTab) {
653
+ modelsTab.addEventListener('click', refreshModelMgmtUI);
654
+ }
655
+
656
+ // Set up register model form
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();
698
+ });
699
+
700
+ // Toggle Add Model form
701
+ function toggleAddModelForm() {
702
+ const form = document.querySelector('.model-mgmt-register-form');
703
+ form.classList.toggle('collapsed');
704
+ }
705
+
706
+ // Helper function to render a model table section
707
+ function renderModelTable(tbody, models, allModels, emptyMessage) {
708
+ tbody.innerHTML = '';
709
+ if (models.length === 0) {
710
+ const tr = document.createElement('tr');
711
+ const td = document.createElement('td');
712
+ td.colSpan = 2;
713
+ td.textContent = emptyMessage;
714
+ td.style.textAlign = 'center';
715
+ td.style.fontStyle = 'italic';
716
+ td.style.color = '#666';
717
+ td.style.padding = '1em';
718
+ tr.appendChild(td);
719
+ tbody.appendChild(tr);
720
+ } else {
721
+ models.forEach(mid => {
722
+ const tr = document.createElement('tr');
723
+ const tdName = document.createElement('td');
724
+
725
+ tdName.appendChild(createModelNameWithLabels(mid, allModels));
726
+ tdName.style.paddingRight = '1em';
727
+ tdName.style.verticalAlign = 'middle';
728
+ const tdBtn = document.createElement('td');
729
+ tdBtn.style.width = '1%';
730
+ tdBtn.style.verticalAlign = 'middle';
731
+ const btn = document.createElement('button');
732
+ btn.textContent = '+';
733
+ btn.title = 'Install model';
734
+ btn.onclick = async function() {
735
+ btn.disabled = true;
736
+ btn.textContent = '⏳';
737
+ btn.classList.add('installing-btn');
738
+
739
+ // Track this download as active
740
+ activeOperations.add(mid);
741
+
742
+ try {
743
+ await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
744
+ method: 'POST',
745
+ headers: { 'Content-Type': 'application/json' },
746
+ body: JSON.stringify({ model_name: mid })
747
+ });
748
+
749
+ // Download complete - remove from active operations
750
+ activeOperations.delete(mid);
751
+
752
+ await refreshModelMgmtUI();
753
+ // Update chat dropdown too if loadModels function exists
754
+ if (typeof loadModels === 'function') {
755
+ await loadModels();
756
+ }
757
+ } catch (e) {
758
+ btn.textContent = 'Error';
759
+ btn.disabled = false;
760
+ showErrorBanner(`Failed to install model: ${e.message}`);
761
+
762
+ // Remove from active operations on error too
763
+ activeOperations.delete(mid);
764
+ }
765
+ };
766
+ tdBtn.appendChild(btn);
767
+ tr.appendChild(tdName);
768
+ tr.appendChild(tdBtn);
769
+ tbody.appendChild(tr);
770
+ });
771
+ }
772
+ }
773
+
774
+ // Model Management Tab Logic
775
+ async function refreshModelMgmtUI() {
776
+ // Get installed models from /api/v1/models
777
+ let installed = [];
778
+ try {
779
+ const data = await httpJson(getServerBaseUrl() + '/api/v1/models');
780
+ if (data.data && Array.isArray(data.data)) {
781
+ installed = data.data.map(m => m.id || m.name || m);
782
+ }
783
+ } catch (e) {
784
+ showErrorBanner(`Error loading models: ${e.message}`);
785
+ }
786
+
787
+ // Update the global installedModels set
788
+ installedModels.clear();
789
+ installed.forEach(modelId => {
790
+ installedModels.add(modelId);
791
+ });
792
+
793
+ // All models from server_models.json (window.SERVER_MODELS)
794
+ const allModels = window.SERVER_MODELS || {};
795
+
796
+ // Separate hot models and regular suggested models not installed
797
+ const hotModels = [];
798
+ const regularSuggested = [];
799
+
800
+ Object.keys(allModels).forEach(k => {
801
+ if (allModels[k].suggested && !installed.includes(k)) {
802
+ const modelData = allModels[k];
803
+ const hasHotLabel = modelData.labels && modelData.labels.some(label =>
804
+ label.toLowerCase() === 'hot'
805
+ );
806
+
807
+ if (hasHotLabel) {
808
+ hotModels.push(k);
809
+ } else {
810
+ regularSuggested.push(k);
811
+ }
812
+ }
813
+ });
814
+
815
+ // Render installed models as a table (two columns, second is invisible)
816
+ const installedTbody = document.getElementById('installed-models-tbody');
817
+ if (installedTbody) {
818
+ installedTbody.innerHTML = '';
819
+ installed.forEach(function(mid) {
820
+ var tr = document.createElement('tr');
821
+ var tdName = document.createElement('td');
822
+
823
+ tdName.appendChild(createModelNameWithLabels(mid, allModels));
824
+ tdName.style.paddingRight = '1em';
825
+ tdName.style.verticalAlign = 'middle';
826
+
827
+ var tdBtn = document.createElement('td');
828
+ tdBtn.style.width = '1%';
829
+ tdBtn.style.verticalAlign = 'middle';
830
+ const btn = document.createElement('button');
831
+ btn.textContent = '−';
832
+ btn.title = 'Delete model';
833
+ btn.style.cursor = 'pointer';
834
+ btn.onclick = async function() {
835
+ if (!confirm(`Are you sure you want to delete the model "${mid}"?`)) {
836
+ return;
837
+ }
838
+ btn.disabled = true;
839
+ btn.textContent = '⏳';
840
+ btn.style.backgroundColor = '#888';
841
+ try {
842
+ await httpRequest(getServerBaseUrl() + '/api/v1/delete', {
843
+ method: 'POST',
844
+ headers: { 'Content-Type': 'application/json' },
845
+ body: JSON.stringify({ model_name: mid })
846
+ });
847
+ await refreshModelMgmtUI();
848
+ // Update chat dropdown too if loadModels function exists
849
+ if (typeof loadModels === 'function') {
850
+ await loadModels();
851
+ }
852
+ } catch (e) {
853
+ btn.textContent = 'Error';
854
+ btn.disabled = false;
855
+ btn.style.backgroundColor = '';
856
+ showErrorBanner(`Failed to delete model: ${e.message}`);
857
+ }
858
+ };
859
+ tdBtn.appendChild(btn);
860
+ tr.appendChild(tdName);
861
+ tr.appendChild(tdBtn);
862
+ installedTbody.appendChild(tr);
863
+ });
864
+ }
865
+
866
+ // Render hot models and suggested models using the helper function
867
+ const hotTbody = document.getElementById('hot-models-tbody');
868
+ const suggestedTbody = document.getElementById('suggested-models-tbody');
869
+
870
+ if (hotTbody) {
871
+ renderModelTable(hotTbody, hotModels, allModels, "Nice, you've already installed all these models!");
872
+ }
873
+ if (suggestedTbody) {
874
+ renderModelTable(suggestedTbody, regularSuggested, allModels, "Nice, you've already installed all these models!");
875
+ }
876
+
877
+ // Refresh model dropdown in chat after updating installed models
878
+ if (window.initializeModelDropdown) {
879
+ window.initializeModelDropdown();
880
+ }
881
+
882
+ // Update system message when installed models change
883
+ if (window.displaySystemMessage) {
884
+ window.displaySystemMessage();
885
+ }
886
+ }
887
+
888
+ // Make refreshModelMgmtUI globally accessible
889
+ window.refreshModelMgmtUI = refreshModelMgmtUI;
890
+
891
+ // Display-only version that uses already-fetched installedModels data
892
+ function refreshModelMgmtUIDisplay() {
893
+ // Use the already-populated installedModels set
894
+ const installed = Array.from(installedModels);
895
+
896
+ // All models from server_models.json (window.SERVER_MODELS)
897
+ const allModels = window.SERVER_MODELS || {};
898
+
899
+ // Separate hot models and regular suggested models not installed
900
+ const hotModels = [];
901
+ const regularSuggested = [];
902
+
903
+ Object.keys(allModels).forEach(k => {
904
+ if (allModels[k].suggested && !installed.includes(k)) {
905
+ if (allModels[k].labels && allModels[k].labels.some(label => label.toLowerCase() === 'hot')) {
906
+ hotModels.push(k);
907
+ } else {
908
+ regularSuggested.push(k);
909
+ }
910
+ }
911
+ });
912
+
913
+ // Render installed models as a table (two columns, second is invisible)
914
+ const installedTbody = document.getElementById('installed-models-tbody');
915
+ if (installedTbody) {
916
+ installedTbody.innerHTML = '';
917
+ installed.forEach(function(mid) {
918
+ var tr = document.createElement('tr');
919
+ var tdName = document.createElement('td');
920
+
921
+ tdName.appendChild(createModelNameWithLabels(mid, allModels));
922
+ tdName.style.paddingRight = '1em';
923
+ tdName.style.verticalAlign = 'middle';
924
+
925
+ var tdBtn = document.createElement('td');
926
+ tdBtn.style.width = '1%';
927
+ tdBtn.style.verticalAlign = 'middle';
928
+ const btn = document.createElement('button');
929
+ btn.textContent = '−';
930
+ btn.className = 'btn-remove-model';
931
+ btn.style.minWidth = '24px';
932
+ btn.style.padding = '2px 8px';
933
+ btn.style.fontSize = '16px';
934
+ btn.style.lineHeight = '1';
935
+ btn.style.border = '1px solid #ddd';
936
+ btn.style.backgroundColor = '#f8f9fa';
937
+ btn.style.cursor = 'pointer';
938
+ btn.style.borderRadius = '4px';
939
+ btn.title = 'Remove this model';
940
+ btn.onclick = async function() {
941
+ if (confirm(`Are you sure you want to remove the model "${mid}"?`)) {
942
+ btn.disabled = true;
943
+ btn.textContent = '⏳';
944
+ const originalBgColor = btn.style.backgroundColor;
945
+ btn.style.backgroundColor = '#888';
946
+ try {
947
+ await httpRequest(getServerBaseUrl() + '/api/v1/delete', {
948
+ method: 'POST',
949
+ headers: { 'Content-Type': 'application/json' },
950
+ body: JSON.stringify({ model_name: mid })
951
+ });
952
+ await refreshModelMgmtUI();
953
+ } catch (error) {
954
+ console.error('Error removing model:', error);
955
+ showErrorBanner('Failed to remove model: ' + error.message);
956
+ // Reset button state on error
957
+ btn.disabled = false;
958
+ btn.textContent = '−';
959
+ btn.style.backgroundColor = originalBgColor;
960
+ }
961
+ }
962
+ };
963
+ tdBtn.appendChild(btn);
964
+ tr.appendChild(tdName);
965
+ tr.appendChild(tdBtn);
966
+ installedTbody.appendChild(tr);
967
+ });
968
+ }
969
+
970
+ // Render hot models and suggested models using the helper function
971
+ const hotTbody = document.getElementById('hot-models-tbody');
972
+ const suggestedTbody = document.getElementById('suggested-models-tbody');
973
+
974
+ if (hotTbody) {
975
+ renderModelTable(hotTbody, hotModels, allModels, "Nice, you've already installed all these models!");
976
+ }
977
+ if (suggestedTbody) {
978
+ renderModelTable(suggestedTbody, regularSuggested, allModels, "Nice, you've already installed all these models!");
979
+ }
980
+
981
+ // Refresh model dropdown in chat after updating installed models
982
+ if (window.initializeModelDropdown) {
983
+ window.initializeModelDropdown();
984
+ }
985
+ }
986
+
987
+ // Set up the register model form
988
+ function setupRegisterModelForm() {
989
+ const registerForm = document.getElementById('register-model-form');
990
+ const registerStatus = document.getElementById('register-model-status');
991
+
992
+ if (registerForm && registerStatus) {
993
+ registerForm.onsubmit = async function(e) {
994
+ e.preventDefault();
995
+ registerStatus.textContent = '';
996
+ let name = document.getElementById('register-model-name').value.trim();
997
+
998
+ // Always prepend 'user.' if not already present
999
+ if (!name.startsWith('user.')) {
1000
+ name = 'user.' + name;
1001
+ }
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
+
1013
+ const checkpoint = document.getElementById('register-checkpoint').value.trim();
1014
+ const recipe = document.getElementById('register-recipe').value;
1015
+ const reasoning = document.getElementById('register-reasoning').checked;
1016
+ const vision = document.getElementById('register-vision').checked;
1017
+ const embedding = document.getElementById('register-embedding').checked;
1018
+ const reranking = document.getElementById('register-reranking').checked;
1019
+ const mmproj = document.getElementById('register-mmproj').value.trim();
1020
+
1021
+ if (!name || !recipe) {
1022
+ return;
1023
+ }
1024
+
1025
+ const btn = document.getElementById('register-submit');
1026
+ btn.disabled = true;
1027
+ btn.textContent = 'Installing...';
1028
+
1029
+ try {
1030
+ if (isLocalModel && selectedModelFiles) {
1031
+ if (recipe === 'llamacpp' && !Array.from(selectedModelFiles).some(file => file.name.toLowerCase().endsWith('.gguf'))) {
1032
+ throw new Error('No .gguf files found in the selected folder for llamacpp');
1033
+ }
1034
+
1035
+ const formData = new FormData();
1036
+ formData.append('model_name', name);
1037
+ formData.append('checkpoint', checkpoint);
1038
+ formData.append('recipe', recipe);
1039
+ formData.append('reasoning', reasoning);
1040
+ formData.append('vision', vision);
1041
+ formData.append('embedding', embedding);
1042
+ formData.append('reranking', reranking);
1043
+ if (mmproj) formData.append('mmproj', mmproj);
1044
+ Array.from(selectedModelFiles).forEach(file => {
1045
+ formData.append('model_files', file, file.webkitRelativePath);
1046
+ });
1047
+
1048
+ await httpRequest(getServerBaseUrl() + '/api/v1/add-local-model', {
1049
+ method: 'POST',
1050
+ body: formData
1051
+ });
1052
+ }
1053
+ else {
1054
+ if (!checkpoint) {
1055
+ throw new Error('Checkpoint is required for remote models');
1056
+ }
1057
+ const payload = { model_name: name, recipe, reasoning, vision, embedding, reranking };
1058
+ if (checkpoint) payload.checkpoint = checkpoint;
1059
+ if (mmproj) payload.mmproj = mmproj;
1060
+
1061
+ await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
1062
+ method: 'POST',
1063
+ headers: { 'Content-Type': 'application/json' },
1064
+ body: JSON.stringify(payload)
1065
+ });
1066
+ }
1067
+
1068
+ registerStatus.textContent = 'Model installed!';
1069
+ registerStatus.style.color = '#0eaf51ff';
1070
+ registerStatus.className = 'register-status success';
1071
+
1072
+ // Add custom model to SERVER_MODELS so it appears in the UI without having to do a manual refresh
1073
+ if (name.startsWith('user.')) {
1074
+ const labels = ['custom'];
1075
+ if (vision) labels.push('vision');
1076
+ if (reasoning) labels.push('reasoning');
1077
+ if (embedding) labels.push('embeddings');
1078
+ if (reranking) labels.push('reranking');
1079
+
1080
+ window.SERVER_MODELS[name] = {
1081
+ recipe: recipe,
1082
+ labels: labels
1083
+ };
1084
+ if (checkpoint) window.SERVER_MODELS[name].checkpoint = checkpoint;
1085
+ if (mmproj) window.SERVER_MODELS[name].mmproj = mmproj;
1086
+ }
1087
+
1088
+ registerForm.reset();
1089
+ isLocalModel = false;
1090
+ selectedModelFiles = null;
1091
+ document.getElementById('folder-input').value = '';
1092
+
1093
+ await refreshModelMgmtUI();
1094
+ // Update chat dropdown too if loadModels function exists
1095
+ if (typeof loadModels === 'function') {
1096
+ await loadModels();
1097
+ }
1098
+ } catch (e) {
1099
+ registerStatus.textContent = e.message + ' See the Lemonade Server log for details.';
1100
+ registerStatus.style.color = '#dc3545';
1101
+ registerStatus.className = 'register-status error';
1102
+ showErrorBanner(`Model install failed: ${e.message}`);
1103
+ }
1104
+
1105
+ btn.disabled = false;
1106
+ btn.textContent = 'Install';
1107
+ refreshModelMgmtUI();
1108
+ };
1109
+ }
1110
+ }
1111
+ let isLocalModel = false;
1112
+ let selectedModelFiles = null;
1113
+ // Helper function to find mmproj file in selected folder
1114
+ function findMmprojFile(files) {
1115
+ for (let i = 0; i < files.length; i++) {
1116
+ const file = files[i];
1117
+ const fileName = file.name.toLowerCase();
1118
+ const relativePath = file.webkitRelativePath;
1119
+
1120
+ // Check if file contains 'mmproj' and has .gguf extension
1121
+ if (fileName.includes('mmproj') && fileName.endsWith('.gguf')) {
1122
+ // Return just the filename (last part of the path)
1123
+ return relativePath.split('/').pop();
1124
+ }
1125
+ }
1126
+ return null;
1127
+ }
1128
+
1129
+ // Helper function to find all non-mmproj GGUF files in selected folder
1130
+ function findGgufFiles(files) {
1131
+ const ggufFiles = [];
1132
+ for (let i = 0; i < files.length; i++) {
1133
+ const file = files[i];
1134
+ const fileName = file.name.toLowerCase();
1135
+ const relativePath = file.webkitRelativePath;
1136
+
1137
+ // Check if file has .gguf extension but is NOT an mmproj file
1138
+ if (fileName.endsWith('.gguf') && !fileName.includes('mmproj')) {
1139
+ // Store just the filename (last part of the path)
1140
+ ggufFiles.push(relativePath.split('/').pop());
1141
+ }
1142
+ }
1143
+ return ggufFiles;
1144
+ }
1145
+
1146
+ // Helper function to check GGUF files and show appropriate banners
1147
+ function checkGgufFilesAndShowBanner(files) {
1148
+ const recipeSelect = document.getElementById('register-recipe');
1149
+
1150
+ // Only check if llamacpp is selected
1151
+ if (!recipeSelect || recipeSelect.value !== 'llamacpp') {
1152
+ return;
1153
+ }
1154
+
1155
+ const mmprojFile = findMmprojFile(files);
1156
+ const ggufFiles = findGgufFiles(files);
1157
+
1158
+ // Hide any existing banners first
1159
+ hideErrorBanner();
1160
+
1161
+ if (ggufFiles.length > 1) {
1162
+ // Multiple GGUF files detected
1163
+ const folderPath = files[0].webkitRelativePath.split('/')[0];
1164
+ 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]}`;
1165
+
1166
+ if (mmprojFile) {
1167
+ bannerMsg += `\n\nDon't forget to enter the mmproj file name and check the 'vision' checkbox if it is a vision model.`;
1168
+ }
1169
+
1170
+ showBanner(bannerMsg, 'warning');
1171
+ } else if (mmprojFile) {
1172
+ // MMproj detected
1173
+ showBanner("MMproj detected and populated. Please validate the file name and check the 'vision' checkbox if it is a vision model.", 'success');
1174
+ }
1175
+ }
1176
+ // Helper function to auto-fill mmproj field if llamacpp is selected
1177
+ function autoFillMmproj() {
1178
+ const recipeSelect = document.getElementById('register-recipe');
1179
+ const mmprojInput = document.getElementById('register-mmproj');
1180
+
1181
+ if (recipeSelect && mmprojInput && isLocalModel && selectedModelFiles) {
1182
+ const selectedRecipe = recipeSelect.value;
1183
+
1184
+ if (selectedRecipe === 'llamacpp') {
1185
+ const mmprojFile = findMmprojFile(selectedModelFiles);
1186
+ if (mmprojFile) {
1187
+ mmprojInput.value = mmprojFile;
1188
+ }
1189
+
1190
+ // Check GGUF files and show appropriate banner
1191
+ checkGgufFilesAndShowBanner(selectedModelFiles);
1192
+ } else {
1193
+ // Hide banners if not llamacpp
1194
+ hideErrorBanner();
1195
+ }
1196
+ }
1197
+ }
1198
+ function setupFolderSelection() {
1199
+ const selectFolderBtn = document.getElementById('select-folder-btn');
1200
+ const folderInput = document.getElementById('folder-input');
1201
+ const checkpointInput = document.getElementById('register-checkpoint');
1202
+ const recipeSelect = document.getElementById('register-recipe');
1203
+
1204
+ if (selectFolderBtn && folderInput && checkpointInput) {
1205
+ selectFolderBtn.addEventListener('click', () => {
1206
+ folderInput.click();
1207
+ });
1208
+
1209
+ folderInput.addEventListener('change', (event) => {
1210
+ const files = event.target.files;
1211
+ if (files.length > 0) {
1212
+ const firstFile = files[0];
1213
+ const folderPath = firstFile.webkitRelativePath.split('/')[0];
1214
+ checkpointInput.value = folderPath;
1215
+ isLocalModel = true;
1216
+ selectedModelFiles = files;
1217
+
1218
+ // Auto-fill mmproj if llamacpp is already selected
1219
+ autoFillMmproj();
1220
+ }
1221
+ else {
1222
+ isLocalModel = false;
1223
+ selectedModelFiles = null;
1224
+ checkpointInput.value = '';
1225
+ hideErrorBanner();
1226
+ }
1227
+ });
1228
+
1229
+ // Add listener to recipe dropdown to auto-fill mmproj when changed to llamacpp
1230
+ if (recipeSelect) {
1231
+ recipeSelect.addEventListener('change', () => {
1232
+ autoFillMmproj();
1233
+ });
1234
+ }
1235
+ }
1236
+ }
1237
+ // === Migration/Cleanup Functions ===
1238
+
1239
+ // Store incompatible models data globally
1240
+ let incompatibleModelsData = null;
1241
+
1242
+ // Check for incompatible models on page load
1243
+ async function checkIncompatibleModels() {
1244
+ try {
1245
+ const response = await httpJson(getServerBaseUrl() + '/api/v1/migration/incompatible-models');
1246
+ incompatibleModelsData = response;
1247
+
1248
+ if (response.count > 0) {
1249
+ showMigrationBanner(response.count, response.total_size);
1250
+ }
1251
+ } catch (error) {
1252
+ console.error('Error checking for incompatible models:', error);
1253
+ }
1254
+ }
1255
+
1256
+ // Show migration banner
1257
+ function showMigrationBanner(count, totalSize) {
1258
+ const banner = document.getElementById('migration-banner');
1259
+ const msg = document.getElementById('migration-banner-msg');
1260
+
1261
+ const sizeGB = (totalSize / (1024 * 1024 * 1024)).toFixed(1);
1262
+ msg.textContent = `Found ${count} incompatible RyzenAI model${count > 1 ? 's' : ''} (${sizeGB} GB). Clean up to free disk space.`;
1263
+ banner.style.display = 'flex';
1264
+ }
1265
+
1266
+ // Hide migration banner
1267
+ function hideMigrationBanner() {
1268
+ const banner = document.getElementById('migration-banner');
1269
+ banner.style.display = 'none';
1270
+ }
1271
+
1272
+ // Show migration modal with model list
1273
+ function showMigrationModal() {
1274
+ if (!incompatibleModelsData || incompatibleModelsData.count === 0) {
1275
+ return;
1276
+ }
1277
+
1278
+ const modal = document.getElementById('migration-modal');
1279
+ const modelList = document.getElementById('migration-model-list');
1280
+ const totalSize = document.getElementById('migration-total-size');
1281
+
1282
+ // Populate model list
1283
+ modelList.innerHTML = '';
1284
+ incompatibleModelsData.models.forEach(model => {
1285
+ const item = document.createElement('div');
1286
+ item.className = 'migration-model-item';
1287
+
1288
+ const nameSpan = document.createElement('span');
1289
+ nameSpan.className = 'migration-model-name';
1290
+ nameSpan.textContent = model.name;
1291
+
1292
+ const sizeSpan = document.createElement('span');
1293
+ sizeSpan.className = 'migration-model-size';
1294
+ sizeSpan.textContent = model.size_formatted;
1295
+
1296
+ item.appendChild(nameSpan);
1297
+ item.appendChild(sizeSpan);
1298
+ modelList.appendChild(item);
1299
+ });
1300
+
1301
+ // Set total size
1302
+ const sizeGB = (incompatibleModelsData.total_size / (1024 * 1024 * 1024)).toFixed(1);
1303
+ totalSize.textContent = `${sizeGB} GB`;
1304
+
1305
+ modal.style.display = 'flex';
1306
+ }
1307
+
1308
+ // Hide migration modal
1309
+ function hideMigrationModal() {
1310
+ const modal = document.getElementById('migration-modal');
1311
+ modal.style.display = 'none';
1312
+ }
1313
+
1314
+ // Delete incompatible models
1315
+ async function deleteIncompatibleModels() {
1316
+ if (!incompatibleModelsData || incompatibleModelsData.count === 0) {
1317
+ return;
1318
+ }
1319
+
1320
+ const modelPaths = incompatibleModelsData.models.map(m => m.path);
1321
+
1322
+ try {
1323
+ // Disable buttons during deletion
1324
+ const deleteBtn = document.querySelector('.delete-btn');
1325
+ const cancelBtn = document.querySelector('.cancel-btn');
1326
+ deleteBtn.disabled = true;
1327
+ cancelBtn.disabled = true;
1328
+ deleteBtn.textContent = 'Deleting...';
1329
+
1330
+ const response = await httpRequest(getServerBaseUrl() + '/api/v1/migration/cleanup', {
1331
+ method: 'POST',
1332
+ headers: { 'Content-Type': 'application/json' },
1333
+ body: JSON.stringify({ model_paths: modelPaths })
1334
+ });
1335
+
1336
+ const result = await response.json();
1337
+
1338
+ // Close modal
1339
+ hideMigrationModal();
1340
+
1341
+ // Hide banner
1342
+ hideMigrationBanner();
1343
+
1344
+ // Show success message
1345
+ showSuccessMessage(`Successfully deleted ${result.success_count} model${result.success_count > 1 ? 's' : ''}, freed ${result.freed_size_formatted}`);
1346
+
1347
+ // Clear cached data
1348
+ incompatibleModelsData = null;
1349
+
1350
+ } catch (error) {
1351
+ console.error('Error deleting incompatible models:', error);
1352
+ showErrorBanner('Failed to delete models: ' + error.message);
1353
+
1354
+ // Re-enable buttons
1355
+ const deleteBtn = document.querySelector('.delete-btn');
1356
+ const cancelBtn = document.querySelector('.cancel-btn');
1357
+ deleteBtn.disabled = false;
1358
+ cancelBtn.disabled = false;
1359
+ deleteBtn.textContent = 'Delete All';
1360
+ }
1361
+ }
1362
+
1363
+ // Show success message (reuse error banner with green color)
1364
+ function showSuccessMessage(message) {
1365
+ const banner = document.getElementById('error-banner');
1366
+ const msg = document.getElementById('error-banner-msg');
1367
+ msg.textContent = message;
1368
+ banner.style.backgroundColor = '#2d7f47';
1369
+ banner.style.display = 'flex';
1370
+
1371
+ // Auto-hide after 5 seconds
1372
+ setTimeout(() => {
1373
+ banner.style.display = 'none';
1374
+ banner.style.backgroundColor = ''; // Reset to default
1375
+ }, 5000);
1376
+ }
1377
+
1378
+ // Check for incompatible models when page loads
1379
+ document.addEventListener('DOMContentLoaded', function() {
1380
+ // Run check after a short delay to let the page load
1381
+ setTimeout(checkIncompatibleModels, 1000);
1382
+ });
1383
+
1384
+ // Make functions globally available for HTML onclick handlers and other components
1385
+ window.toggleCategory = toggleCategory;
1386
+ window.selectRecipe = selectRecipe;
1387
+ window.selectLabel = selectLabel;
1388
+ window.showAddModelForm = showAddModelForm;
1389
+ window.unloadModel = unloadModel;
1390
+ window.installModel = installModel;
1391
+ window.deleteModel = deleteModel;
1392
+ window.showMigrationModal = showMigrationModal;
1393
+ window.hideMigrationModal = hideMigrationModal;
1394
+ window.hideMigrationBanner = hideMigrationBanner;
1395
+ window.deleteIncompatibleModels = deleteIncompatibleModels;