lemonade-sdk 7.0.3__py3-none-any.whl → 8.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of lemonade-sdk might be problematic. Click here for more details.
- lemonade/api.py +3 -3
- lemonade/cli.py +11 -17
- lemonade/common/build.py +0 -47
- lemonade/common/network.py +50 -0
- lemonade/common/status.py +2 -21
- lemonade/common/system_info.py +19 -4
- lemonade/profilers/memory_tracker.py +3 -1
- lemonade/tools/accuracy.py +3 -4
- lemonade/tools/adapter.py +1 -2
- lemonade/tools/{huggingface_bench.py → huggingface/bench.py} +2 -87
- lemonade/tools/huggingface/load.py +235 -0
- lemonade/tools/{huggingface_load.py → huggingface/utils.py} +87 -255
- lemonade/tools/humaneval.py +9 -3
- lemonade/tools/{llamacpp_bench.py → llamacpp/bench.py} +1 -1
- lemonade/tools/{llamacpp.py → llamacpp/load.py} +18 -2
- lemonade/tools/mmlu.py +7 -15
- lemonade/tools/{ort_genai/oga.py → oga/load.py} +31 -422
- lemonade/tools/oga/utils.py +423 -0
- lemonade/tools/perplexity.py +4 -3
- lemonade/tools/prompt.py +2 -1
- lemonade/tools/quark/quark_load.py +2 -1
- lemonade/tools/quark/quark_quantize.py +5 -5
- lemonade/tools/report/table.py +3 -3
- lemonade/tools/server/llamacpp.py +159 -34
- lemonade/tools/server/serve.py +169 -147
- lemonade/tools/server/static/favicon.ico +0 -0
- lemonade/tools/server/static/styles.css +568 -0
- lemonade/tools/server/static/webapp.html +439 -0
- lemonade/tools/server/tray.py +458 -0
- lemonade/tools/server/{port_utils.py → utils/port.py} +22 -3
- lemonade/tools/server/utils/system_tray.py +395 -0
- lemonade/tools/server/{instructions.py → webapp.py} +4 -10
- lemonade/version.py +1 -1
- lemonade_install/install.py +46 -28
- {lemonade_sdk-7.0.3.dist-info → lemonade_sdk-8.0.0.dist-info}/METADATA +84 -22
- lemonade_sdk-8.0.0.dist-info/RECORD +70 -0
- lemonade_server/cli.py +182 -27
- lemonade_server/model_manager.py +192 -20
- lemonade_server/pydantic_models.py +9 -4
- lemonade_server/server_models.json +5 -3
- lemonade/common/analyze_model.py +0 -26
- lemonade/common/labels.py +0 -61
- lemonade/common/onnx_helpers.py +0 -176
- lemonade/common/plugins.py +0 -10
- lemonade/common/tensor_helpers.py +0 -83
- lemonade/tools/server/static/instructions.html +0 -262
- lemonade_sdk-7.0.3.dist-info/RECORD +0 -69
- /lemonade/tools/{ort_genai → oga}/__init__.py +0 -0
- /lemonade/tools/{ort_genai/oga_bench.py → oga/bench.py} +0 -0
- /lemonade/tools/server/{thread_utils.py → utils/thread.py} +0 -0
- {lemonade_sdk-7.0.3.dist-info → lemonade_sdk-8.0.0.dist-info}/WHEEL +0 -0
- {lemonade_sdk-7.0.3.dist-info → lemonade_sdk-8.0.0.dist-info}/entry_points.txt +0 -0
- {lemonade_sdk-7.0.3.dist-info → lemonade_sdk-8.0.0.dist-info}/licenses/LICENSE +0 -0
- {lemonade_sdk-7.0.3.dist-info → lemonade_sdk-8.0.0.dist-info}/licenses/NOTICE.md +0 -0
- {lemonade_sdk-7.0.3.dist-info → lemonade_sdk-8.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Lemonade Server</title>
|
|
7
|
+
<link rel="icon" href="/static/favicon.ico">
|
|
8
|
+
<link rel="stylesheet" href="/static/styles.css">
|
|
9
|
+
<script>
|
|
10
|
+
window.SERVER_PORT = {{SERVER_PORT}};
|
|
11
|
+
</script>
|
|
12
|
+
{{SERVER_MODELS_JS}}
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<nav class="navbar">
|
|
16
|
+
<a href="https://github.com/lemonade-sdk/lemonade" target="_blank">GitHub</a>
|
|
17
|
+
<a href="https://lemonade-server.ai/docs/" target="_blank">Docs</a>
|
|
18
|
+
<a href="https://lemonade-server.ai/docs/server/server_models/" target="_blank">Models</a>
|
|
19
|
+
<a href="https://lemonade-server.ai/docs/server/apps/" target="_blank">Featured Apps</a>
|
|
20
|
+
</nav>
|
|
21
|
+
<main class="main">
|
|
22
|
+
<div class="title">🍋 Lemonade Server</div>
|
|
23
|
+
<div class="tab-container">
|
|
24
|
+
<div class="tabs">
|
|
25
|
+
<button class="tab active" id="tab-chat" onclick="showTab('chat')">LLM Chat</button>
|
|
26
|
+
<button class="tab" id="tab-models" onclick="showTab('models')">Model Management</button>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="tab-content active" id="content-chat">
|
|
29
|
+
<div class="chat-container">
|
|
30
|
+
<div class="chat-history" id="chat-history"></div>
|
|
31
|
+
<div class="chat-input-row">
|
|
32
|
+
<select id="model-select"></select>
|
|
33
|
+
<input type="text" id="chat-input" placeholder="Type your message..." />
|
|
34
|
+
<button id="send-btn">Send</button>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="tab-content" id="content-models"> <div class="model-mgmt-register-form collapsed"> <h3 class="model-mgmt-form-title" onclick="toggleAddModelForm()">
|
|
39
|
+
Add a Model
|
|
40
|
+
<span class="tooltip-icon" data-tooltip="Lemonade Server has a built-in set of suggested models, however you can use this form to add any compatible GGUF or ONNX model you like from Hugging Face.">ⓘ</span>
|
|
41
|
+
</h3>
|
|
42
|
+
<form id="register-model-form" autocomplete="off" class="form-content">
|
|
43
|
+
<div class="register-form-row">
|
|
44
|
+
<label class="register-label">
|
|
45
|
+
Model Name
|
|
46
|
+
<span class="tooltip-icon" data-tooltip="Enter a unique short name for your model. This is how the model will be referenced by Lemonade Server and connected apps. It will be prefixed with 'user.' to distinguish it from the built-in models.">ⓘ</span>
|
|
47
|
+
</label>
|
|
48
|
+
<div class="register-model-name-group">
|
|
49
|
+
<span class="register-model-prefix styled-prefix">user.</span>
|
|
50
|
+
<input type="text" id="register-model-name" name="model_name" placeholder="Gemma-3-12b-it-GGUF" required autocomplete="off">
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="register-form-row">
|
|
54
|
+
<label class="register-label">
|
|
55
|
+
Checkpoint
|
|
56
|
+
<span class="tooltip-icon" data-tooltip="Specify the model checkpoint path from Hugging Face (e.g., org-name/model-name:variant).">ⓘ</span>
|
|
57
|
+
</label>
|
|
58
|
+
<input type="text" id="register-checkpoint" name="checkpoint" placeholder="unsloth/gemma-3-12b-it-GGUF:Q4_0" class="register-textbox" autocomplete="off">
|
|
59
|
+
</div>
|
|
60
|
+
<div class="register-form-row">
|
|
61
|
+
<label class="register-label">
|
|
62
|
+
Recipe
|
|
63
|
+
<span class="tooltip-icon" data-tooltip="Select the Lemonade recipe corresponding to the inference engine and device Lemonade Server should use for the model. Use llamacpp for GGUF models. For OGA/ONNX models, click the More Info button to learn about the oga-* recipes.">ⓘ</span>
|
|
64
|
+
</label>
|
|
65
|
+
<select id="register-recipe" name="recipe" required>
|
|
66
|
+
<option value="llamacpp">llamacpp</option>
|
|
67
|
+
<option value="oga-hybrid">oga-hybrid</option>
|
|
68
|
+
<option value="oga-cpu">oga-cpu</option>
|
|
69
|
+
</select>
|
|
70
|
+
<a href="https://lemonade-server.ai/docs/lemonade_api/" target="_blank" class="register-doc-link">More info</a>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="register-form-row register-form-row-tight">
|
|
73
|
+
<label class="register-label">
|
|
74
|
+
mmproj file
|
|
75
|
+
<span class="tooltip-icon" data-tooltip="Specify an mmproj file from the same Hugging Face checkpoint as the model. This is used for multimodal models, such as VLMs. Leave empty if not needed.">ⓘ</span>
|
|
76
|
+
</label>
|
|
77
|
+
<input type="text" id="register-mmproj" name="mmproj" placeholder="(Optional) mmproj-F16.gguf" autocomplete="off">
|
|
78
|
+
<label class="register-label reasoning-inline">
|
|
79
|
+
<input type="checkbox" id="register-reasoning" name="reasoning">
|
|
80
|
+
Reasoning
|
|
81
|
+
<span class="tooltip-icon" data-tooltip="Enable to inform Lemonade Server that the model has reasoning capabilities that will use thinking tokens.">ⓘ</span>
|
|
82
|
+
</label>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="register-form-row register-form-row-tight">
|
|
85
|
+
<button type="submit" id="register-submit">Install</button>
|
|
86
|
+
<span id="register-model-status" class="register-status"></span> </div>
|
|
87
|
+
</form>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="model-mgmt-container">
|
|
90
|
+
<div class="model-mgmt-pane">
|
|
91
|
+
<h3>Installed Models</h3>
|
|
92
|
+
<table class="model-table" id="installed-models-table">
|
|
93
|
+
<colgroup><col style="width:100%"></colgroup>
|
|
94
|
+
<tbody id="installed-models-tbody"></tbody>
|
|
95
|
+
</table>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="model-mgmt-pane">
|
|
98
|
+
<h3>Suggested Models</h3>
|
|
99
|
+
<table class="model-table" id="suggested-models-table">
|
|
100
|
+
<tbody id="suggested-models-tbody"></tbody>
|
|
101
|
+
</table>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</main>
|
|
107
|
+
<footer class="site-footer">
|
|
108
|
+
<div class="dad-joke">When life gives you LLMs, make an LLM aide.</div>
|
|
109
|
+
<div class="copyright">Copyright 2025 AMD</div>
|
|
110
|
+
</footer>
|
|
111
|
+
<script src="https://cdn.jsdelivr.net/npm/openai@4.21.0/dist/openai.min.js"></script>
|
|
112
|
+
<script> // Tab switching logic
|
|
113
|
+
function showTab(tab) {
|
|
114
|
+
document.getElementById('tab-chat').classList.remove('active');
|
|
115
|
+
document.getElementById('tab-models').classList.remove('active');
|
|
116
|
+
document.getElementById('content-chat').classList.remove('active');
|
|
117
|
+
document.getElementById('content-models').classList.remove('active');
|
|
118
|
+
if (tab === 'chat') {
|
|
119
|
+
document.getElementById('tab-chat').classList.add('active');
|
|
120
|
+
document.getElementById('content-chat').classList.add('active');
|
|
121
|
+
} else {
|
|
122
|
+
document.getElementById('tab-models').classList.add('active');
|
|
123
|
+
document.getElementById('content-models').classList.add('active');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Toggle Add Model form
|
|
128
|
+
function toggleAddModelForm() {
|
|
129
|
+
const form = document.querySelector('.model-mgmt-register-form');
|
|
130
|
+
form.classList.toggle('collapsed');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Helper to get server base URL
|
|
134
|
+
function getServerBaseUrl() {
|
|
135
|
+
const port = window.SERVER_PORT || 8000;
|
|
136
|
+
return `http://localhost:${port}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Populate model dropdown from /api/v1/models endpoint
|
|
140
|
+
async function loadModels() {
|
|
141
|
+
try {
|
|
142
|
+
const resp = await fetch(getServerBaseUrl() + '/api/v1/models');
|
|
143
|
+
const data = await resp.json();
|
|
144
|
+
const select = document.getElementById('model-select');
|
|
145
|
+
select.innerHTML = '';
|
|
146
|
+
if (!data.data || !Array.isArray(data.data)) {
|
|
147
|
+
select.innerHTML = '<option>No models found (malformed response)</option>';
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (data.data.length === 0) {
|
|
151
|
+
select.innerHTML = '<option>No models available</option>';
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
let defaultIndex = 0;
|
|
155
|
+
data.data.forEach(function(model, index) {
|
|
156
|
+
const modelId = model.id || model.name || model;
|
|
157
|
+
const opt = document.createElement('option');
|
|
158
|
+
opt.value = modelId;
|
|
159
|
+
opt.textContent = modelId;
|
|
160
|
+
if (modelId === 'Llama-3.2-1B-Instruct-Hybrid') {
|
|
161
|
+
defaultIndex = index;
|
|
162
|
+
}
|
|
163
|
+
select.appendChild(opt);
|
|
164
|
+
});
|
|
165
|
+
select.selectedIndex = defaultIndex;
|
|
166
|
+
} catch (e) {
|
|
167
|
+
const select = document.getElementById('model-select');
|
|
168
|
+
select.innerHTML = `<option>Error loading models: ${e.message}</option>`;
|
|
169
|
+
console.error('Error loading models:', e);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
loadModels();
|
|
173
|
+
|
|
174
|
+
// Helper function to create model name with labels
|
|
175
|
+
function createModelNameWithLabels(modelId, allModels) {
|
|
176
|
+
// Create container for model name and labels
|
|
177
|
+
const container = document.createElement('div');
|
|
178
|
+
container.className = 'model-labels-container';
|
|
179
|
+
|
|
180
|
+
// Add model name
|
|
181
|
+
const nameSpan = document.createElement('span');
|
|
182
|
+
nameSpan.textContent = modelId;
|
|
183
|
+
container.appendChild(nameSpan);
|
|
184
|
+
|
|
185
|
+
// Add labels if they exist
|
|
186
|
+
const modelData = allModels[modelId];
|
|
187
|
+
if (modelData) {
|
|
188
|
+
// Add reasoning label if reasoning is true
|
|
189
|
+
if (modelData.reasoning === true) {
|
|
190
|
+
const reasoningLabel = document.createElement('span');
|
|
191
|
+
reasoningLabel.className = 'model-label reasoning';
|
|
192
|
+
reasoningLabel.textContent = 'reasoning';
|
|
193
|
+
container.appendChild(reasoningLabel);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Add other labels if they exist
|
|
197
|
+
if (modelData.labels && Array.isArray(modelData.labels)) {
|
|
198
|
+
modelData.labels.forEach(label => {
|
|
199
|
+
const labelSpan = document.createElement('span');
|
|
200
|
+
const labelLower = label.toLowerCase();
|
|
201
|
+
const labelClass = (labelLower === 'vision') ? 'vision' : 'other';
|
|
202
|
+
labelSpan.className = `model-label ${labelClass}`;
|
|
203
|
+
labelSpan.textContent = label;
|
|
204
|
+
container.appendChild(labelSpan);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return container;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Model Management Tab Logic
|
|
213
|
+
async function refreshModelMgmtUI() {
|
|
214
|
+
// Get installed models from /api/v1/models
|
|
215
|
+
let installed = [];
|
|
216
|
+
try {
|
|
217
|
+
const resp = await fetch(getServerBaseUrl() + '/api/v1/models');
|
|
218
|
+
const data = await resp.json();
|
|
219
|
+
if (data.data && Array.isArray(data.data)) {
|
|
220
|
+
installed = data.data.map(m => m.id || m.name || m);
|
|
221
|
+
}
|
|
222
|
+
} catch (e) {}
|
|
223
|
+
// All models from server_models.json (window.SERVER_MODELS)
|
|
224
|
+
const allModels = window.SERVER_MODELS || {};
|
|
225
|
+
// Filter suggested models not installed
|
|
226
|
+
const suggested = Object.keys(allModels).filter(
|
|
227
|
+
k => allModels[k].suggested && !installed.includes(k)
|
|
228
|
+
);
|
|
229
|
+
// Render installed models as a table (two columns, second is invisible)
|
|
230
|
+
const installedTbody = document.getElementById('installed-models-tbody');
|
|
231
|
+
installedTbody.innerHTML = '';
|
|
232
|
+
installed.forEach(function(mid) {
|
|
233
|
+
var tr = document.createElement('tr');
|
|
234
|
+
var tdName = document.createElement('td');
|
|
235
|
+
|
|
236
|
+
tdName.appendChild(createModelNameWithLabels(mid, allModels));
|
|
237
|
+
tdName.style.paddingRight = '1em';
|
|
238
|
+
tdName.style.verticalAlign = 'middle';
|
|
239
|
+
|
|
240
|
+
var tdBtn = document.createElement('td');
|
|
241
|
+
tdBtn.style.width = '1%';
|
|
242
|
+
tdBtn.style.verticalAlign = 'middle';
|
|
243
|
+
const btn = document.createElement('button');
|
|
244
|
+
btn.textContent = '−';
|
|
245
|
+
btn.title = 'Delete model';
|
|
246
|
+
btn.style.cursor = 'pointer';
|
|
247
|
+
btn.onclick = async function() {
|
|
248
|
+
if (!confirm(`Are you sure you want to delete the model "${mid}"?`)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
btn.disabled = true;
|
|
252
|
+
btn.textContent = 'Deleting...';
|
|
253
|
+
btn.style.backgroundColor = '#888';
|
|
254
|
+
try {
|
|
255
|
+
const response = await fetch(getServerBaseUrl() + '/api/v1/delete', {
|
|
256
|
+
method: 'POST',
|
|
257
|
+
headers: { 'Content-Type': 'application/json' },
|
|
258
|
+
body: JSON.stringify({ model_name: mid })
|
|
259
|
+
});
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
const errorData = await response.json();
|
|
262
|
+
throw new Error(errorData.detail || 'Failed to delete model');
|
|
263
|
+
}
|
|
264
|
+
await refreshModelMgmtUI();
|
|
265
|
+
await loadModels(); // update chat dropdown too
|
|
266
|
+
} catch (e) {
|
|
267
|
+
btn.textContent = 'Error';
|
|
268
|
+
btn.disabled = false;
|
|
269
|
+
alert(`Failed to delete model: ${e.message}`);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
tdBtn.appendChild(btn);
|
|
273
|
+
|
|
274
|
+
tr.appendChild(tdName);
|
|
275
|
+
tr.appendChild(tdBtn);
|
|
276
|
+
installedTbody.appendChild(tr);
|
|
277
|
+
});
|
|
278
|
+
// Render suggested models as a table
|
|
279
|
+
const suggestedTbody = document.getElementById('suggested-models-tbody');
|
|
280
|
+
suggestedTbody.innerHTML = '';
|
|
281
|
+
suggested.forEach(mid => {
|
|
282
|
+
const tr = document.createElement('tr');
|
|
283
|
+
const tdName = document.createElement('td');
|
|
284
|
+
|
|
285
|
+
tdName.appendChild(createModelNameWithLabels(mid, allModels));
|
|
286
|
+
tdName.style.paddingRight = '1em';
|
|
287
|
+
tdName.style.verticalAlign = 'middle';
|
|
288
|
+
const tdBtn = document.createElement('td');
|
|
289
|
+
tdBtn.style.width = '1%';
|
|
290
|
+
tdBtn.style.verticalAlign = 'middle';
|
|
291
|
+
const btn = document.createElement('button');
|
|
292
|
+
btn.textContent = '+';
|
|
293
|
+
btn.title = 'Install model';
|
|
294
|
+
btn.onclick = async function() {
|
|
295
|
+
btn.disabled = true;
|
|
296
|
+
btn.textContent = 'Installing...';
|
|
297
|
+
btn.classList.add('installing-btn');
|
|
298
|
+
try {
|
|
299
|
+
await fetch(getServerBaseUrl() + '/api/v1/pull', {
|
|
300
|
+
method: 'POST',
|
|
301
|
+
headers: { 'Content-Type': 'application/json' },
|
|
302
|
+
body: JSON.stringify({ model_name: mid })
|
|
303
|
+
});
|
|
304
|
+
await refreshModelMgmtUI();
|
|
305
|
+
await loadModels(); // update chat dropdown too
|
|
306
|
+
} catch (e) {
|
|
307
|
+
btn.textContent = 'Error';
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
tdBtn.appendChild(btn);
|
|
311
|
+
tr.appendChild(tdName);
|
|
312
|
+
tr.appendChild(tdBtn);
|
|
313
|
+
suggestedTbody.appendChild(tr);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
// Initial load
|
|
317
|
+
refreshModelMgmtUI();
|
|
318
|
+
// Optionally, refresh when switching to the tab
|
|
319
|
+
document.getElementById('tab-models').addEventListener('click', refreshModelMgmtUI);
|
|
320
|
+
|
|
321
|
+
// Chat logic (streaming with OpenAI JS client placeholder)
|
|
322
|
+
const chatHistory = document.getElementById('chat-history');
|
|
323
|
+
const chatInput = document.getElementById('chat-input');
|
|
324
|
+
const sendBtn = document.getElementById('send-btn');
|
|
325
|
+
const modelSelect = document.getElementById('model-select');
|
|
326
|
+
let messages = [];
|
|
327
|
+
|
|
328
|
+
function appendMessage(role, text) {
|
|
329
|
+
const div = document.createElement('div');
|
|
330
|
+
div.className = 'chat-message ' + role;
|
|
331
|
+
// Add a bubble for iMessage style
|
|
332
|
+
const bubble = document.createElement('div');
|
|
333
|
+
bubble.className = 'chat-bubble ' + role;
|
|
334
|
+
bubble.innerHTML = text;
|
|
335
|
+
div.appendChild(bubble);
|
|
336
|
+
chatHistory.appendChild(div);
|
|
337
|
+
chatHistory.scrollTop = chatHistory.scrollHeight;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function sendMessage() {
|
|
341
|
+
const text = chatInput.value.trim();
|
|
342
|
+
if (!text) return;
|
|
343
|
+
appendMessage('user', text);
|
|
344
|
+
messages.push({ role: 'user', content: text });
|
|
345
|
+
chatInput.value = '';
|
|
346
|
+
sendBtn.disabled = true;
|
|
347
|
+
// Streaming OpenAI completions (placeholder, adapt as needed)
|
|
348
|
+
let llmText = '';
|
|
349
|
+
appendMessage('llm', '...');
|
|
350
|
+
const llmDiv = chatHistory.lastChild.querySelector('.chat-bubble.llm');
|
|
351
|
+
try {
|
|
352
|
+
// Use the correct endpoint for chat completions
|
|
353
|
+
const resp = await fetch(getServerBaseUrl() + '/api/v1/chat/completions', {
|
|
354
|
+
method: 'POST',
|
|
355
|
+
headers: { 'Content-Type': 'application/json' },
|
|
356
|
+
body: JSON.stringify({
|
|
357
|
+
model: modelSelect.value,
|
|
358
|
+
messages: messages,
|
|
359
|
+
stream: true
|
|
360
|
+
})
|
|
361
|
+
});
|
|
362
|
+
if (!resp.body) throw new Error('No stream');
|
|
363
|
+
const reader = resp.body.getReader();
|
|
364
|
+
let decoder = new TextDecoder();
|
|
365
|
+
llmDiv.textContent = '';
|
|
366
|
+
while (true) {
|
|
367
|
+
const { done, value } = await reader.read();
|
|
368
|
+
if (done) break;
|
|
369
|
+
const chunk = decoder.decode(value);
|
|
370
|
+
if (chunk.trim() === 'data: [DONE]' || chunk.trim() === '[DONE]') continue;
|
|
371
|
+
// Try to extract the content from the OpenAI chunk
|
|
372
|
+
const match = chunk.match(/"content"\s*:\s*"([^"]*)"/);
|
|
373
|
+
if (match && match[1]) {
|
|
374
|
+
llmText += match[1];
|
|
375
|
+
llmDiv.textContent = llmText;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
messages.push({ role: 'assistant', content: llmText });
|
|
379
|
+
} catch (e) {
|
|
380
|
+
llmDiv.textContent = '[Error: ' + e.message + ']';
|
|
381
|
+
}
|
|
382
|
+
sendBtn.disabled = false;
|
|
383
|
+
}
|
|
384
|
+
sendBtn.onclick = sendMessage;
|
|
385
|
+
chatInput.addEventListener('keydown', function(e) {
|
|
386
|
+
if (e.key === 'Enter') sendMessage();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Register & Install Model logic
|
|
390
|
+
const registerForm = document.getElementById('register-model-form');
|
|
391
|
+
const registerStatus = document.getElementById('register-model-status');
|
|
392
|
+
if (registerForm) {
|
|
393
|
+
registerForm.onsubmit = async function(e) {
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
registerStatus.textContent = '';
|
|
396
|
+
let name = document.getElementById('register-model-name').value.trim();
|
|
397
|
+
// Always prepend 'user.' if not already present
|
|
398
|
+
if (!name.startsWith('user.')) {
|
|
399
|
+
name = 'user.' + name;
|
|
400
|
+
}
|
|
401
|
+
const checkpoint = document.getElementById('register-checkpoint').value.trim();
|
|
402
|
+
const recipe = document.getElementById('register-recipe').value;
|
|
403
|
+
const reasoning = document.getElementById('register-reasoning').checked;
|
|
404
|
+
const mmproj = document.getElementById('register-mmproj').value.trim();
|
|
405
|
+
if (!name || !recipe) { return; }
|
|
406
|
+
const payload = { model_name: name, recipe, reasoning };
|
|
407
|
+
if (checkpoint) payload.checkpoint = checkpoint;
|
|
408
|
+
if (mmproj) payload.mmproj = mmproj;
|
|
409
|
+
const btn = document.getElementById('register-submit');
|
|
410
|
+
btn.disabled = true;
|
|
411
|
+
btn.textContent = 'Installing...';
|
|
412
|
+
try {
|
|
413
|
+
const resp = await fetch(getServerBaseUrl() + '/api/v1/pull', {
|
|
414
|
+
method: 'POST',
|
|
415
|
+
headers: { 'Content-Type': 'application/json' },
|
|
416
|
+
body: JSON.stringify(payload)
|
|
417
|
+
});
|
|
418
|
+
if (!resp.ok) {
|
|
419
|
+
const err = await resp.json().catch(() => ({}));
|
|
420
|
+
throw new Error(err.detail || 'Failed to register model.'); }
|
|
421
|
+
registerStatus.textContent = 'Model installed!';
|
|
422
|
+
registerStatus.style.color = '#27ae60';
|
|
423
|
+
registerStatus.className = 'register-status success';
|
|
424
|
+
registerForm.reset();
|
|
425
|
+
await refreshModelMgmtUI();
|
|
426
|
+
await loadModels(); // update chat dropdown too
|
|
427
|
+
} catch (e) {
|
|
428
|
+
registerStatus.textContent = e.message + ' See the Lemonade Server log for details.';
|
|
429
|
+
registerStatus.style.color = '#dc3545';
|
|
430
|
+
registerStatus.className = 'register-status error';
|
|
431
|
+
}
|
|
432
|
+
btn.disabled = false;
|
|
433
|
+
btn.textContent = 'Install';
|
|
434
|
+
refreshModelMgmtUI();
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
</script>
|
|
438
|
+
</body>
|
|
439
|
+
</html>
|