lemonade-sdk 8.1.2__py3-none-any.whl → 8.1.3__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/tools/oga/utils.py +54 -33
- lemonade/tools/server/llamacpp.py +96 -4
- lemonade/tools/server/serve.py +74 -8
- lemonade/tools/server/static/js/chat.js +735 -0
- lemonade/tools/server/static/js/model-settings.js +162 -0
- lemonade/tools/server/static/js/models.js +865 -0
- lemonade/tools/server/static/js/shared.js +491 -0
- lemonade/tools/server/static/styles.css +652 -26
- lemonade/tools/server/static/webapp.html +145 -1092
- lemonade/tools/server/utils/port.py +3 -2
- lemonade/version.py +1 -1
- {lemonade_sdk-8.1.2.dist-info → lemonade_sdk-8.1.3.dist-info}/METADATA +7 -6
- {lemonade_sdk-8.1.2.dist-info → lemonade_sdk-8.1.3.dist-info}/RECORD +21 -17
- lemonade_server/cli.py +31 -17
- lemonade_server/pydantic_models.py +15 -3
- lemonade_server/server_models.json +9 -3
- {lemonade_sdk-8.1.2.dist-info → lemonade_sdk-8.1.3.dist-info}/WHEEL +0 -0
- {lemonade_sdk-8.1.2.dist-info → lemonade_sdk-8.1.3.dist-info}/entry_points.txt +0 -0
- {lemonade_sdk-8.1.2.dist-info → lemonade_sdk-8.1.3.dist-info}/licenses/LICENSE +0 -0
- {lemonade_sdk-8.1.2.dist-info → lemonade_sdk-8.1.3.dist-info}/licenses/NOTICE.md +0 -0
- {lemonade_sdk-8.1.2.dist-info → lemonade_sdk-8.1.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,865 @@
|
|
|
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
|
+
|
|
7
|
+
// Make installedModels accessible globally for the chat dropdown
|
|
8
|
+
window.installedModels = installedModels;
|
|
9
|
+
let currentCategory = 'hot';
|
|
10
|
+
let currentFilter = null;
|
|
11
|
+
|
|
12
|
+
// === Model Status Management ===
|
|
13
|
+
|
|
14
|
+
// Fetch installed models from the server
|
|
15
|
+
async function fetchInstalledModels() {
|
|
16
|
+
try {
|
|
17
|
+
const response = await httpJson(getServerBaseUrl() + '/api/v1/models');
|
|
18
|
+
installedModels.clear();
|
|
19
|
+
if (response && response.data) {
|
|
20
|
+
response.data.forEach(model => {
|
|
21
|
+
installedModels.add(model.id);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Error fetching installed models:', error);
|
|
26
|
+
// If we can't fetch, assume all are installed to maintain current functionality
|
|
27
|
+
const allModels = window.SERVER_MODELS || {};
|
|
28
|
+
Object.keys(allModels).forEach(modelId => {
|
|
29
|
+
installedModels.add(modelId);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check health endpoint to get current model status
|
|
35
|
+
async function checkModelHealth() {
|
|
36
|
+
try {
|
|
37
|
+
const response = await httpJson(getServerBaseUrl() + '/api/v1/health');
|
|
38
|
+
return response;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Error checking model health:', error);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Update model status indicator
|
|
46
|
+
async function updateModelStatusIndicator() {
|
|
47
|
+
const indicator = document.getElementById('model-status-indicator');
|
|
48
|
+
const statusText = document.getElementById('model-status-text');
|
|
49
|
+
const unloadBtn = document.getElementById('model-unload-btn');
|
|
50
|
+
|
|
51
|
+
// Fetch both health and installed models
|
|
52
|
+
const [health] = await Promise.all([
|
|
53
|
+
checkModelHealth(),
|
|
54
|
+
fetchInstalledModels()
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
// Refresh model dropdown in chat after fetching installed models
|
|
58
|
+
if (window.initializeModelDropdown) {
|
|
59
|
+
window.initializeModelDropdown();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Refresh model management UI if we're on the models tab
|
|
63
|
+
const modelsTab = document.getElementById('content-models');
|
|
64
|
+
if (modelsTab && modelsTab.classList.contains('active')) {
|
|
65
|
+
// Use the display-only version to avoid re-fetching data we just fetched
|
|
66
|
+
refreshModelMgmtUIDisplay();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Remove any click handlers
|
|
70
|
+
indicator.onclick = null;
|
|
71
|
+
|
|
72
|
+
if (health && health.model_loaded) {
|
|
73
|
+
// Model is loaded - show model name with online status
|
|
74
|
+
currentLoadedModel = health.model_loaded;
|
|
75
|
+
updateStatusIndicator(health.model_loaded, 'loaded');
|
|
76
|
+
unloadBtn.style.display = 'block';
|
|
77
|
+
} else if (health) {
|
|
78
|
+
// Server is online but no model loaded
|
|
79
|
+
currentLoadedModel = null;
|
|
80
|
+
updateStatusIndicator('Server Online', 'online');
|
|
81
|
+
unloadBtn.style.display = 'none';
|
|
82
|
+
} else {
|
|
83
|
+
// Server is offline
|
|
84
|
+
currentLoadedModel = null;
|
|
85
|
+
updateStatusIndicator('Server Offline', 'offline');
|
|
86
|
+
unloadBtn.style.display = 'none';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Unload current model
|
|
91
|
+
async function unloadModel() {
|
|
92
|
+
if (!currentLoadedModel) return;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/unload', {
|
|
96
|
+
method: 'POST'
|
|
97
|
+
});
|
|
98
|
+
await updateModelStatusIndicator();
|
|
99
|
+
|
|
100
|
+
// Refresh model list to show updated button states
|
|
101
|
+
if (currentCategory === 'hot') displayHotModels();
|
|
102
|
+
else if (currentCategory === 'recipes') displayModelsByRecipe(currentFilter);
|
|
103
|
+
else if (currentCategory === 'labels') displayModelsByLabel(currentFilter);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('Error unloading model:', error);
|
|
106
|
+
showErrorBanner('Failed to unload model: ' + error.message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// === Model Browser Management ===
|
|
111
|
+
|
|
112
|
+
// Toggle category in model browser (only for Hot Models now)
|
|
113
|
+
function toggleCategory(categoryName) {
|
|
114
|
+
const header = document.querySelector(`[data-category="${categoryName}"] .category-header`);
|
|
115
|
+
const content = document.getElementById(`category-${categoryName}`);
|
|
116
|
+
|
|
117
|
+
if (categoryName === 'hot') {
|
|
118
|
+
// Check if hot models is already selected
|
|
119
|
+
const isCurrentlyActive = header.classList.contains('active');
|
|
120
|
+
|
|
121
|
+
// Clear all other active states
|
|
122
|
+
document.querySelectorAll('.subcategory').forEach(s => s.classList.remove('active'));
|
|
123
|
+
|
|
124
|
+
if (!isCurrentlyActive) {
|
|
125
|
+
// Show hot models
|
|
126
|
+
header.classList.add('active');
|
|
127
|
+
content.classList.add('expanded');
|
|
128
|
+
currentCategory = categoryName;
|
|
129
|
+
currentFilter = null;
|
|
130
|
+
displayHotModels();
|
|
131
|
+
}
|
|
132
|
+
// If already active, keep it active (don't toggle off)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Show add model form in main area
|
|
137
|
+
function showAddModelForm() {
|
|
138
|
+
// Clear all sidebar active states
|
|
139
|
+
document.querySelectorAll('.category-header').forEach(h => h.classList.remove('active'));
|
|
140
|
+
document.querySelectorAll('.category-content').forEach(c => c.classList.remove('expanded'));
|
|
141
|
+
document.querySelectorAll('.subcategory').forEach(s => s.classList.remove('active'));
|
|
142
|
+
|
|
143
|
+
// Highlight "Add a Model" as selected
|
|
144
|
+
const addModelHeader = document.querySelector('[data-category="add"] .category-header');
|
|
145
|
+
if (addModelHeader) {
|
|
146
|
+
addModelHeader.classList.add('active');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Hide model list and show form
|
|
150
|
+
document.getElementById('model-list').style.display = 'none';
|
|
151
|
+
document.getElementById('add-model-form-main').style.display = 'block';
|
|
152
|
+
|
|
153
|
+
// Set current state
|
|
154
|
+
currentCategory = 'add';
|
|
155
|
+
currentFilter = null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Select recipe filter
|
|
159
|
+
function selectRecipe(recipe) {
|
|
160
|
+
// Clear hot models active state
|
|
161
|
+
document.querySelectorAll('.category-header').forEach(h => h.classList.remove('active'));
|
|
162
|
+
document.querySelectorAll('.category-content').forEach(c => c.classList.remove('expanded'));
|
|
163
|
+
|
|
164
|
+
// Clear all subcategory selections
|
|
165
|
+
document.querySelectorAll('.subcategory').forEach(s => s.classList.remove('active'));
|
|
166
|
+
|
|
167
|
+
// Set this recipe as active
|
|
168
|
+
document.querySelector(`[data-recipe="${recipe}"]`).classList.add('active');
|
|
169
|
+
|
|
170
|
+
currentCategory = 'recipes';
|
|
171
|
+
currentFilter = recipe;
|
|
172
|
+
displayModelsByRecipe(recipe);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Select label filter
|
|
176
|
+
function selectLabel(label) {
|
|
177
|
+
// Clear hot models active state
|
|
178
|
+
document.querySelectorAll('.category-header').forEach(h => h.classList.remove('active'));
|
|
179
|
+
document.querySelectorAll('.category-content').forEach(c => c.classList.remove('expanded'));
|
|
180
|
+
|
|
181
|
+
// Clear all subcategory selections
|
|
182
|
+
document.querySelectorAll('.subcategory').forEach(s => s.classList.remove('active'));
|
|
183
|
+
|
|
184
|
+
// Set this label as active
|
|
185
|
+
document.querySelector(`[data-label="${label}"]`).classList.add('active');
|
|
186
|
+
|
|
187
|
+
currentCategory = 'labels';
|
|
188
|
+
currentFilter = label;
|
|
189
|
+
displayModelsByLabel(label);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Display suggested models (Qwen3-0.6B-GGUF as default)
|
|
193
|
+
function displaySuggestedModels() {
|
|
194
|
+
const modelList = document.getElementById('model-list');
|
|
195
|
+
const allModels = window.SERVER_MODELS || {};
|
|
196
|
+
|
|
197
|
+
modelList.innerHTML = '';
|
|
198
|
+
|
|
199
|
+
// First show Qwen3-0.6B-GGUF as the default suggested model
|
|
200
|
+
if (allModels['Qwen3-0.6B-GGUF']) {
|
|
201
|
+
createModelItem('Qwen3-0.6B-GGUF', allModels['Qwen3-0.6B-GGUF'], modelList);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Then show other suggested models (excluding the one already shown)
|
|
205
|
+
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
206
|
+
if (modelData.suggested && modelId !== 'Qwen3-0.6B-GGUF') {
|
|
207
|
+
createModelItem(modelId, modelData, modelList);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (modelList.innerHTML === '') {
|
|
212
|
+
modelList.innerHTML = '<p>No suggested models available</p>';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Display hot models
|
|
217
|
+
function displayHotModels() {
|
|
218
|
+
const modelList = document.getElementById('model-list');
|
|
219
|
+
const addModelForm = document.getElementById('add-model-form-main');
|
|
220
|
+
const allModels = window.SERVER_MODELS || {};
|
|
221
|
+
|
|
222
|
+
// Show model list, hide form
|
|
223
|
+
modelList.style.display = 'block';
|
|
224
|
+
addModelForm.style.display = 'none';
|
|
225
|
+
|
|
226
|
+
modelList.innerHTML = '';
|
|
227
|
+
|
|
228
|
+
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
229
|
+
if (modelData.labels && modelData.labels.includes('hot')) {
|
|
230
|
+
createModelItem(modelId, modelData, modelList);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Display models by recipe
|
|
236
|
+
function displayModelsByRecipe(recipe) {
|
|
237
|
+
const modelList = document.getElementById('model-list');
|
|
238
|
+
const addModelForm = document.getElementById('add-model-form-main');
|
|
239
|
+
const allModels = window.SERVER_MODELS || {};
|
|
240
|
+
|
|
241
|
+
// Show model list, hide form
|
|
242
|
+
modelList.style.display = 'block';
|
|
243
|
+
addModelForm.style.display = 'none';
|
|
244
|
+
|
|
245
|
+
modelList.innerHTML = '';
|
|
246
|
+
|
|
247
|
+
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
248
|
+
if (modelData.recipe === recipe) {
|
|
249
|
+
createModelItem(modelId, modelData, modelList);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Display models by label
|
|
255
|
+
function displayModelsByLabel(label) {
|
|
256
|
+
const modelList = document.getElementById('model-list');
|
|
257
|
+
const addModelForm = document.getElementById('add-model-form-main');
|
|
258
|
+
const allModels = window.SERVER_MODELS || {};
|
|
259
|
+
|
|
260
|
+
// Show model list, hide form
|
|
261
|
+
modelList.style.display = 'block';
|
|
262
|
+
addModelForm.style.display = 'none';
|
|
263
|
+
|
|
264
|
+
modelList.innerHTML = '';
|
|
265
|
+
|
|
266
|
+
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
267
|
+
if (label === 'custom') {
|
|
268
|
+
// Show user-added models (those starting with 'user.')
|
|
269
|
+
if (modelId.startsWith('user.')) {
|
|
270
|
+
createModelItem(modelId, modelData, modelList);
|
|
271
|
+
}
|
|
272
|
+
} else if (modelData.labels && modelData.labels.includes(label)) {
|
|
273
|
+
createModelItem(modelId, modelData, modelList);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Create model item element
|
|
279
|
+
function createModelItem(modelId, modelData, container) {
|
|
280
|
+
const item = document.createElement('div');
|
|
281
|
+
item.className = 'model-item';
|
|
282
|
+
|
|
283
|
+
const info = document.createElement('div');
|
|
284
|
+
info.className = 'model-item-info';
|
|
285
|
+
|
|
286
|
+
const name = document.createElement('div');
|
|
287
|
+
name.className = 'model-item-name';
|
|
288
|
+
name.appendChild(createModelNameWithLabels(modelId, window.SERVER_MODELS || {}));
|
|
289
|
+
|
|
290
|
+
info.appendChild(name);
|
|
291
|
+
|
|
292
|
+
// Only add description if it exists and is not empty
|
|
293
|
+
if (modelData.description && modelData.description.trim()) {
|
|
294
|
+
const description = document.createElement('div');
|
|
295
|
+
description.className = 'model-item-description';
|
|
296
|
+
description.textContent = modelData.description;
|
|
297
|
+
info.appendChild(description);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const actions = document.createElement('div');
|
|
301
|
+
actions.className = 'model-item-actions';
|
|
302
|
+
|
|
303
|
+
// Check if model is actually installed by looking at the installedModels set
|
|
304
|
+
const isInstalled = installedModels.has(modelId);
|
|
305
|
+
const isLoaded = currentLoadedModel === modelId;
|
|
306
|
+
|
|
307
|
+
if (!isInstalled) {
|
|
308
|
+
const installBtn = document.createElement('button');
|
|
309
|
+
installBtn.className = 'model-item-btn install';
|
|
310
|
+
installBtn.textContent = '📥';
|
|
311
|
+
installBtn.title = 'Install';
|
|
312
|
+
installBtn.onclick = () => installModel(modelId);
|
|
313
|
+
actions.appendChild(installBtn);
|
|
314
|
+
} else {
|
|
315
|
+
if (isLoaded) {
|
|
316
|
+
const unloadBtn = document.createElement('button');
|
|
317
|
+
unloadBtn.className = 'model-item-btn unload';
|
|
318
|
+
unloadBtn.textContent = '⏏️';
|
|
319
|
+
unloadBtn.title = 'Unload';
|
|
320
|
+
unloadBtn.onclick = () => unloadModel();
|
|
321
|
+
actions.appendChild(unloadBtn);
|
|
322
|
+
} else {
|
|
323
|
+
const loadBtn = document.createElement('button');
|
|
324
|
+
loadBtn.className = 'model-item-btn load';
|
|
325
|
+
loadBtn.textContent = '🚀';
|
|
326
|
+
loadBtn.title = 'Load';
|
|
327
|
+
loadBtn.onclick = () => loadModel(modelId);
|
|
328
|
+
actions.appendChild(loadBtn);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const deleteBtn = document.createElement('button');
|
|
332
|
+
deleteBtn.className = 'model-item-btn delete';
|
|
333
|
+
deleteBtn.textContent = '🗑️';
|
|
334
|
+
deleteBtn.title = 'Delete';
|
|
335
|
+
deleteBtn.onclick = () => deleteModel(modelId);
|
|
336
|
+
actions.appendChild(deleteBtn);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
item.appendChild(info);
|
|
340
|
+
item.appendChild(actions);
|
|
341
|
+
container.appendChild(item);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Install model
|
|
345
|
+
async function installModel(modelId) {
|
|
346
|
+
// Find the install button and show loading state
|
|
347
|
+
const modelItems = document.querySelectorAll('.model-item');
|
|
348
|
+
let installBtn = null;
|
|
349
|
+
|
|
350
|
+
modelItems.forEach(item => {
|
|
351
|
+
const nameElement = item.querySelector('.model-item-name .model-labels-container span');
|
|
352
|
+
if (nameElement && nameElement.textContent === modelId) {
|
|
353
|
+
installBtn = item.querySelector('.model-item-btn.install');
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
if (installBtn) {
|
|
358
|
+
installBtn.disabled = true;
|
|
359
|
+
installBtn.textContent = '⏳';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const modelData = window.SERVER_MODELS[modelId];
|
|
364
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
|
|
365
|
+
method: 'POST',
|
|
366
|
+
headers: { 'Content-Type': 'application/json' },
|
|
367
|
+
body: JSON.stringify({ model_name: modelId, ...modelData })
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Refresh installed models and model status
|
|
371
|
+
await fetchInstalledModels();
|
|
372
|
+
await updateModelStatusIndicator();
|
|
373
|
+
|
|
374
|
+
// Refresh model dropdown in chat
|
|
375
|
+
if (window.initializeModelDropdown) {
|
|
376
|
+
window.initializeModelDropdown();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Refresh model list
|
|
380
|
+
if (currentCategory === 'hot') displayHotModels();
|
|
381
|
+
else if (currentCategory === 'recipes') displayModelsByRecipe(currentFilter);
|
|
382
|
+
else if (currentCategory === 'labels') displayModelsByLabel(currentFilter);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
console.error('Error installing model:', error);
|
|
385
|
+
showErrorBanner('Failed to install model: ' + error.message);
|
|
386
|
+
|
|
387
|
+
// Reset button state on error
|
|
388
|
+
if (installBtn) {
|
|
389
|
+
installBtn.disabled = false;
|
|
390
|
+
installBtn.textContent = 'Install';
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Load model
|
|
396
|
+
async function loadModel(modelId) {
|
|
397
|
+
// Find the load button and show loading state
|
|
398
|
+
const modelItems = document.querySelectorAll('.model-item');
|
|
399
|
+
let loadBtn = null;
|
|
400
|
+
|
|
401
|
+
modelItems.forEach(item => {
|
|
402
|
+
const nameElement = item.querySelector('.model-item-name .model-labels-container span');
|
|
403
|
+
if (nameElement && nameElement.textContent === modelId) {
|
|
404
|
+
loadBtn = item.querySelector('.model-item-btn.load');
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Use the standardized load function
|
|
409
|
+
const success = await loadModelStandardized(modelId, {
|
|
410
|
+
loadButton: loadBtn,
|
|
411
|
+
onSuccess: (loadedModelId) => {
|
|
412
|
+
console.log(`Model ${loadedModelId} loaded successfully`);
|
|
413
|
+
// Refresh model list after successful load
|
|
414
|
+
if (currentCategory === 'hot') displayHotModels();
|
|
415
|
+
else if (currentCategory === 'recipes') displayModelsByRecipe(currentFilter);
|
|
416
|
+
else if (currentCategory === 'labels') displayModelsByLabel(currentFilter);
|
|
417
|
+
},
|
|
418
|
+
onError: (error, failedModelId) => {
|
|
419
|
+
console.error(`Failed to load model ${failedModelId}:`, error);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Delete model
|
|
425
|
+
async function deleteModel(modelId) {
|
|
426
|
+
if (!confirm(`Are you sure you want to delete the model "${modelId}"?`)) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/delete', {
|
|
432
|
+
method: 'POST',
|
|
433
|
+
headers: { 'Content-Type': 'application/json' },
|
|
434
|
+
body: JSON.stringify({ model_name: modelId })
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Refresh installed models and model status
|
|
438
|
+
await fetchInstalledModels();
|
|
439
|
+
await updateModelStatusIndicator();
|
|
440
|
+
|
|
441
|
+
// Refresh model dropdown in chat
|
|
442
|
+
if (window.initializeModelDropdown) {
|
|
443
|
+
window.initializeModelDropdown();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Refresh model list
|
|
447
|
+
if (currentCategory === 'hot') displayHotModels();
|
|
448
|
+
else if (currentCategory === 'recipes') displayModelsByRecipe(currentFilter);
|
|
449
|
+
else if (currentCategory === 'labels') displayModelsByLabel(currentFilter);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
console.error('Error deleting model:', error);
|
|
452
|
+
showErrorBanner('Failed to delete model: ' + error.message);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// === Model Name Display ===
|
|
457
|
+
|
|
458
|
+
// Create model name with labels
|
|
459
|
+
function createModelNameWithLabels(modelId, serverModels) {
|
|
460
|
+
const container = document.createElement('div');
|
|
461
|
+
container.className = 'model-labels-container';
|
|
462
|
+
|
|
463
|
+
// Model name
|
|
464
|
+
const nameSpan = document.createElement('span');
|
|
465
|
+
nameSpan.textContent = modelId;
|
|
466
|
+
container.appendChild(nameSpan);
|
|
467
|
+
|
|
468
|
+
// Labels
|
|
469
|
+
const modelData = serverModels[modelId];
|
|
470
|
+
if (modelData && modelData.labels && Array.isArray(modelData.labels)) {
|
|
471
|
+
modelData.labels.forEach(label => {
|
|
472
|
+
const labelLower = label.toLowerCase();
|
|
473
|
+
|
|
474
|
+
// Skip "hot" labels since they have their own section
|
|
475
|
+
if (labelLower === 'hot') {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const labelSpan = document.createElement('span');
|
|
480
|
+
let labelClass = 'other';
|
|
481
|
+
if (labelLower === 'vision') {
|
|
482
|
+
labelClass = 'vision';
|
|
483
|
+
} else if (labelLower === 'embeddings') {
|
|
484
|
+
labelClass = 'embeddings';
|
|
485
|
+
} else if (labelLower === 'reasoning') {
|
|
486
|
+
labelClass = 'reasoning';
|
|
487
|
+
} else if (labelLower === 'reranking') {
|
|
488
|
+
labelClass = 'reranking';
|
|
489
|
+
} else if (labelLower === 'coding') {
|
|
490
|
+
labelClass = 'coding';
|
|
491
|
+
}
|
|
492
|
+
labelSpan.className = `model-label ${labelClass}`;
|
|
493
|
+
labelSpan.textContent = label;
|
|
494
|
+
container.appendChild(labelSpan);
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return container;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// === Model Management Table (for models tab) ===
|
|
502
|
+
|
|
503
|
+
// Initialize model management functionality when DOM is loaded
|
|
504
|
+
document.addEventListener('DOMContentLoaded', async function() {
|
|
505
|
+
// Set up model status controls
|
|
506
|
+
const unloadBtn = document.getElementById('model-unload-btn');
|
|
507
|
+
if (unloadBtn) {
|
|
508
|
+
unloadBtn.onclick = unloadModel;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Initial fetch of model data - this will populate installedModels
|
|
512
|
+
await updateModelStatusIndicator();
|
|
513
|
+
|
|
514
|
+
// Set up periodic refresh of model status
|
|
515
|
+
setInterval(updateModelStatusIndicator, 5000); // Check every 5 seconds
|
|
516
|
+
|
|
517
|
+
// Initialize model browser with hot models
|
|
518
|
+
displayHotModels();
|
|
519
|
+
|
|
520
|
+
// Initial load of model management UI - this will use the populated installedModels
|
|
521
|
+
await refreshModelMgmtUI();
|
|
522
|
+
|
|
523
|
+
// Refresh when switching to the models tab
|
|
524
|
+
const modelsTab = document.getElementById('tab-models');
|
|
525
|
+
if (modelsTab) {
|
|
526
|
+
modelsTab.addEventListener('click', refreshModelMgmtUI);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Set up register model form
|
|
530
|
+
setupRegisterModelForm();
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// Toggle Add Model form
|
|
534
|
+
function toggleAddModelForm() {
|
|
535
|
+
const form = document.querySelector('.model-mgmt-register-form');
|
|
536
|
+
form.classList.toggle('collapsed');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Helper function to render a model table section
|
|
540
|
+
function renderModelTable(tbody, models, allModels, emptyMessage) {
|
|
541
|
+
tbody.innerHTML = '';
|
|
542
|
+
if (models.length === 0) {
|
|
543
|
+
const tr = document.createElement('tr');
|
|
544
|
+
const td = document.createElement('td');
|
|
545
|
+
td.colSpan = 2;
|
|
546
|
+
td.textContent = emptyMessage;
|
|
547
|
+
td.style.textAlign = 'center';
|
|
548
|
+
td.style.fontStyle = 'italic';
|
|
549
|
+
td.style.color = '#666';
|
|
550
|
+
td.style.padding = '1em';
|
|
551
|
+
tr.appendChild(td);
|
|
552
|
+
tbody.appendChild(tr);
|
|
553
|
+
} else {
|
|
554
|
+
models.forEach(mid => {
|
|
555
|
+
const tr = document.createElement('tr');
|
|
556
|
+
const tdName = document.createElement('td');
|
|
557
|
+
|
|
558
|
+
tdName.appendChild(createModelNameWithLabels(mid, allModels));
|
|
559
|
+
tdName.style.paddingRight = '1em';
|
|
560
|
+
tdName.style.verticalAlign = 'middle';
|
|
561
|
+
const tdBtn = document.createElement('td');
|
|
562
|
+
tdBtn.style.width = '1%';
|
|
563
|
+
tdBtn.style.verticalAlign = 'middle';
|
|
564
|
+
const btn = document.createElement('button');
|
|
565
|
+
btn.textContent = '+';
|
|
566
|
+
btn.title = 'Install model';
|
|
567
|
+
btn.onclick = async function() {
|
|
568
|
+
btn.disabled = true;
|
|
569
|
+
btn.textContent = 'Installing...';
|
|
570
|
+
btn.classList.add('installing-btn');
|
|
571
|
+
try {
|
|
572
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
|
|
573
|
+
method: 'POST',
|
|
574
|
+
headers: { 'Content-Type': 'application/json' },
|
|
575
|
+
body: JSON.stringify({ model_name: mid })
|
|
576
|
+
});
|
|
577
|
+
await refreshModelMgmtUI();
|
|
578
|
+
// Update chat dropdown too if loadModels function exists
|
|
579
|
+
if (typeof loadModels === 'function') {
|
|
580
|
+
await loadModels();
|
|
581
|
+
}
|
|
582
|
+
} catch (e) {
|
|
583
|
+
btn.textContent = 'Error';
|
|
584
|
+
showErrorBanner(`Failed to install model: ${e.message}`);
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
tdBtn.appendChild(btn);
|
|
588
|
+
tr.appendChild(tdName);
|
|
589
|
+
tr.appendChild(tdBtn);
|
|
590
|
+
tbody.appendChild(tr);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Model Management Tab Logic
|
|
596
|
+
async function refreshModelMgmtUI() {
|
|
597
|
+
// Get installed models from /api/v1/models
|
|
598
|
+
let installed = [];
|
|
599
|
+
try {
|
|
600
|
+
const data = await httpJson(getServerBaseUrl() + '/api/v1/models');
|
|
601
|
+
if (data.data && Array.isArray(data.data)) {
|
|
602
|
+
installed = data.data.map(m => m.id || m.name || m);
|
|
603
|
+
}
|
|
604
|
+
} catch (e) {
|
|
605
|
+
showErrorBanner(`Error loading models: ${e.message}`);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Update the global installedModels set
|
|
609
|
+
installedModels.clear();
|
|
610
|
+
installed.forEach(modelId => {
|
|
611
|
+
installedModels.add(modelId);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// All models from server_models.json (window.SERVER_MODELS)
|
|
615
|
+
const allModels = window.SERVER_MODELS || {};
|
|
616
|
+
|
|
617
|
+
// Separate hot models and regular suggested models not installed
|
|
618
|
+
const hotModels = [];
|
|
619
|
+
const regularSuggested = [];
|
|
620
|
+
|
|
621
|
+
Object.keys(allModels).forEach(k => {
|
|
622
|
+
if (allModels[k].suggested && !installed.includes(k)) {
|
|
623
|
+
const modelData = allModels[k];
|
|
624
|
+
const hasHotLabel = modelData.labels && modelData.labels.some(label =>
|
|
625
|
+
label.toLowerCase() === 'hot'
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
if (hasHotLabel) {
|
|
629
|
+
hotModels.push(k);
|
|
630
|
+
} else {
|
|
631
|
+
regularSuggested.push(k);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// Render installed models as a table (two columns, second is invisible)
|
|
637
|
+
const installedTbody = document.getElementById('installed-models-tbody');
|
|
638
|
+
if (installedTbody) {
|
|
639
|
+
installedTbody.innerHTML = '';
|
|
640
|
+
installed.forEach(function(mid) {
|
|
641
|
+
var tr = document.createElement('tr');
|
|
642
|
+
var tdName = document.createElement('td');
|
|
643
|
+
|
|
644
|
+
tdName.appendChild(createModelNameWithLabels(mid, allModels));
|
|
645
|
+
tdName.style.paddingRight = '1em';
|
|
646
|
+
tdName.style.verticalAlign = 'middle';
|
|
647
|
+
|
|
648
|
+
var tdBtn = document.createElement('td');
|
|
649
|
+
tdBtn.style.width = '1%';
|
|
650
|
+
tdBtn.style.verticalAlign = 'middle';
|
|
651
|
+
const btn = document.createElement('button');
|
|
652
|
+
btn.textContent = '−';
|
|
653
|
+
btn.title = 'Delete model';
|
|
654
|
+
btn.style.cursor = 'pointer';
|
|
655
|
+
btn.onclick = async function() {
|
|
656
|
+
if (!confirm(`Are you sure you want to delete the model "${mid}"?`)) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
btn.disabled = true;
|
|
660
|
+
btn.textContent = 'Deleting...';
|
|
661
|
+
btn.style.backgroundColor = '#888';
|
|
662
|
+
try {
|
|
663
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/delete', {
|
|
664
|
+
method: 'POST',
|
|
665
|
+
headers: { 'Content-Type': 'application/json' },
|
|
666
|
+
body: JSON.stringify({ model_name: mid })
|
|
667
|
+
});
|
|
668
|
+
await refreshModelMgmtUI();
|
|
669
|
+
// Update chat dropdown too if loadModels function exists
|
|
670
|
+
if (typeof loadModels === 'function') {
|
|
671
|
+
await loadModels();
|
|
672
|
+
}
|
|
673
|
+
} catch (e) {
|
|
674
|
+
btn.textContent = 'Error';
|
|
675
|
+
btn.disabled = false;
|
|
676
|
+
showErrorBanner(`Failed to delete model: ${e.message}`);
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
tdBtn.appendChild(btn);
|
|
680
|
+
|
|
681
|
+
tr.appendChild(tdName);
|
|
682
|
+
tr.appendChild(tdBtn);
|
|
683
|
+
installedTbody.appendChild(tr);
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Render hot models and suggested models using the helper function
|
|
688
|
+
const hotTbody = document.getElementById('hot-models-tbody');
|
|
689
|
+
const suggestedTbody = document.getElementById('suggested-models-tbody');
|
|
690
|
+
|
|
691
|
+
if (hotTbody) {
|
|
692
|
+
renderModelTable(hotTbody, hotModels, allModels, "Nice, you've already installed all these models!");
|
|
693
|
+
}
|
|
694
|
+
if (suggestedTbody) {
|
|
695
|
+
renderModelTable(suggestedTbody, regularSuggested, allModels, "Nice, you've already installed all these models!");
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Refresh model dropdown in chat after updating installed models
|
|
699
|
+
if (window.initializeModelDropdown) {
|
|
700
|
+
window.initializeModelDropdown();
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Make refreshModelMgmtUI globally accessible
|
|
705
|
+
window.refreshModelMgmtUI = refreshModelMgmtUI;
|
|
706
|
+
|
|
707
|
+
// Display-only version that uses already-fetched installedModels data
|
|
708
|
+
function refreshModelMgmtUIDisplay() {
|
|
709
|
+
// Use the already-populated installedModels set
|
|
710
|
+
const installed = Array.from(installedModels);
|
|
711
|
+
|
|
712
|
+
// All models from server_models.json (window.SERVER_MODELS)
|
|
713
|
+
const allModels = window.SERVER_MODELS || {};
|
|
714
|
+
|
|
715
|
+
// Separate hot models and regular suggested models not installed
|
|
716
|
+
const hotModels = [];
|
|
717
|
+
const regularSuggested = [];
|
|
718
|
+
|
|
719
|
+
Object.keys(allModels).forEach(k => {
|
|
720
|
+
if (allModels[k].suggested && !installed.includes(k)) {
|
|
721
|
+
if (allModels[k].labels && allModels[k].labels.some(label => label.toLowerCase() === 'hot')) {
|
|
722
|
+
hotModels.push(k);
|
|
723
|
+
} else {
|
|
724
|
+
regularSuggested.push(k);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
// Render installed models as a table (two columns, second is invisible)
|
|
730
|
+
const installedTbody = document.getElementById('installed-models-tbody');
|
|
731
|
+
if (installedTbody) {
|
|
732
|
+
installedTbody.innerHTML = '';
|
|
733
|
+
installed.forEach(function(mid) {
|
|
734
|
+
var tr = document.createElement('tr');
|
|
735
|
+
var tdName = document.createElement('td');
|
|
736
|
+
|
|
737
|
+
tdName.appendChild(createModelNameWithLabels(mid, allModels));
|
|
738
|
+
tdName.style.paddingRight = '1em';
|
|
739
|
+
tdName.style.verticalAlign = 'middle';
|
|
740
|
+
|
|
741
|
+
var tdBtn = document.createElement('td');
|
|
742
|
+
tdBtn.style.width = '1%';
|
|
743
|
+
tdBtn.style.verticalAlign = 'middle';
|
|
744
|
+
const btn = document.createElement('button');
|
|
745
|
+
btn.textContent = '−';
|
|
746
|
+
btn.className = 'btn-remove-model';
|
|
747
|
+
btn.style.minWidth = '24px';
|
|
748
|
+
btn.style.padding = '2px 8px';
|
|
749
|
+
btn.style.fontSize = '16px';
|
|
750
|
+
btn.style.lineHeight = '1';
|
|
751
|
+
btn.style.border = '1px solid #ddd';
|
|
752
|
+
btn.style.backgroundColor = '#f8f9fa';
|
|
753
|
+
btn.style.cursor = 'pointer';
|
|
754
|
+
btn.style.borderRadius = '4px';
|
|
755
|
+
btn.title = 'Remove this model';
|
|
756
|
+
btn.onclick = async function() {
|
|
757
|
+
if (confirm(`Are you sure you want to remove the model "${mid}"?`)) {
|
|
758
|
+
try {
|
|
759
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/delete', {
|
|
760
|
+
method: 'POST',
|
|
761
|
+
headers: { 'Content-Type': 'application/json' },
|
|
762
|
+
body: JSON.stringify({ model_name: mid })
|
|
763
|
+
});
|
|
764
|
+
await refreshModelMgmtUI();
|
|
765
|
+
} catch (error) {
|
|
766
|
+
console.error('Error removing model:', error);
|
|
767
|
+
showErrorBanner('Failed to remove model: ' + error.message);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
tdBtn.appendChild(btn);
|
|
772
|
+
tr.appendChild(tdName);
|
|
773
|
+
tr.appendChild(tdBtn);
|
|
774
|
+
installedTbody.appendChild(tr);
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Render hot models and suggested models using the helper function
|
|
779
|
+
const hotTbody = document.getElementById('hot-models-tbody');
|
|
780
|
+
const suggestedTbody = document.getElementById('suggested-models-tbody');
|
|
781
|
+
|
|
782
|
+
if (hotTbody) {
|
|
783
|
+
renderModelTable(hotTbody, hotModels, allModels, "Nice, you've already installed all these models!");
|
|
784
|
+
}
|
|
785
|
+
if (suggestedTbody) {
|
|
786
|
+
renderModelTable(suggestedTbody, regularSuggested, allModels, "Nice, you've already installed all these models!");
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Refresh model dropdown in chat after updating installed models
|
|
790
|
+
if (window.initializeModelDropdown) {
|
|
791
|
+
window.initializeModelDropdown();
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Set up the register model form
|
|
796
|
+
function setupRegisterModelForm() {
|
|
797
|
+
const registerForm = document.getElementById('register-model-form');
|
|
798
|
+
const registerStatus = document.getElementById('register-model-status');
|
|
799
|
+
|
|
800
|
+
if (registerForm && registerStatus) {
|
|
801
|
+
registerForm.onsubmit = async function(e) {
|
|
802
|
+
e.preventDefault();
|
|
803
|
+
registerStatus.textContent = '';
|
|
804
|
+
let name = document.getElementById('register-model-name').value.trim();
|
|
805
|
+
|
|
806
|
+
// Always prepend 'user.' if not already present
|
|
807
|
+
if (!name.startsWith('user.')) {
|
|
808
|
+
name = 'user.' + name;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const checkpoint = document.getElementById('register-checkpoint').value.trim();
|
|
812
|
+
const recipe = document.getElementById('register-recipe').value;
|
|
813
|
+
const reasoning = document.getElementById('register-reasoning').checked;
|
|
814
|
+
const mmproj = document.getElementById('register-mmproj').value.trim();
|
|
815
|
+
|
|
816
|
+
if (!name || !recipe) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const payload = { model_name: name, recipe, reasoning };
|
|
821
|
+
if (checkpoint) payload.checkpoint = checkpoint;
|
|
822
|
+
if (mmproj) payload.mmproj = mmproj;
|
|
823
|
+
|
|
824
|
+
const btn = document.getElementById('register-submit');
|
|
825
|
+
btn.disabled = true;
|
|
826
|
+
btn.textContent = 'Installing...';
|
|
827
|
+
|
|
828
|
+
try {
|
|
829
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
|
|
830
|
+
method: 'POST',
|
|
831
|
+
headers: { 'Content-Type': 'application/json' },
|
|
832
|
+
body: JSON.stringify(payload)
|
|
833
|
+
});
|
|
834
|
+
registerStatus.textContent = 'Model installed!';
|
|
835
|
+
registerStatus.style.color = '#27ae60';
|
|
836
|
+
registerStatus.className = 'register-status success';
|
|
837
|
+
registerForm.reset();
|
|
838
|
+
await refreshModelMgmtUI();
|
|
839
|
+
// Update chat dropdown too if loadModels function exists
|
|
840
|
+
if (typeof loadModels === 'function') {
|
|
841
|
+
await loadModels();
|
|
842
|
+
}
|
|
843
|
+
} catch (e) {
|
|
844
|
+
registerStatus.textContent = e.message + ' See the Lemonade Server log for details.';
|
|
845
|
+
registerStatus.style.color = '#dc3545';
|
|
846
|
+
registerStatus.className = 'register-status error';
|
|
847
|
+
showErrorBanner(`Model install failed: ${e.message}`);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
btn.disabled = false;
|
|
851
|
+
btn.textContent = 'Install';
|
|
852
|
+
refreshModelMgmtUI();
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Make functions globally available for HTML onclick handlers and other components
|
|
858
|
+
window.toggleCategory = toggleCategory;
|
|
859
|
+
window.selectRecipe = selectRecipe;
|
|
860
|
+
window.selectLabel = selectLabel;
|
|
861
|
+
window.showAddModelForm = showAddModelForm;
|
|
862
|
+
window.unloadModel = unloadModel;
|
|
863
|
+
window.installModel = installModel;
|
|
864
|
+
window.loadModel = loadModel;
|
|
865
|
+
window.deleteModel = deleteModel;
|