Blame Raw
Cohee · e3f41666 · · 479 lines (15.8 KB)
1 contributor
1import {
2 amount_gen,
3 getRequestHeaders,
4 main_api,
5 max_context,
6 resultCheckStatus,
7 saveSettingsDebounced,
8 setGenerationProgress,
9 setOnlineStatus,
10} from '../script.js';
11import { SECRET_KEYS, writeSecret } from './secrets.js';
12import { delay } from './utils.js';
13import { isMobile } from './RossAscends-mods.js';
14import { autoSelectInstructPreset } from './instruct-mode.js';
15import { t } from './i18n.js';
16import { callGenericPopup, POPUP_TYPE } from './popup.js';
17import { kai_settings } from './kai-settings.js';
18
19export {
20 MIN_LENGTH,
21};
22
23let models = [];
24
25export let horde_settings = {
26 models: [],
27 auto_adjust_response_length: true,
28 auto_adjust_context_length: false,
29 trusted_workers_only: false,
30};
31
32const MAX_RETRIES = 480;
33const CHECK_INTERVAL = 2500;
34const MIN_LENGTH = 16;
35
36/**
37 * Gets the available workers from Horde.
38 * @param {boolean} force Do a force refresh of the workers
39 * @returns {Promise<Array>} Array of workers
40 */
41async function getWorkers(force) {
42 const response = await fetch('/api/horde/text-workers', {
43 method: 'POST',
44 headers: getRequestHeaders(),
45 body: JSON.stringify({ force }),
46 });
47 return await response.json();
48}
49
50/**
51 * Gets the available models from Horde.
52 * @param {boolean} force Do a force refresh of the models
53 * @returns {Promise<Array>} Array of models
54 */
55async function getModels(force) {
56 const response = await fetch('/api/horde/text-models', {
57 method: 'POST',
58 headers: getRequestHeaders(),
59 body: JSON.stringify({ force }),
60 });
61 const data = await response.json();
62 console.log('getModels', data);
63 return data;
64}
65
66
67/**
68 * Gets the status of a Horde task.
69 * @param {string} taskId Task ID
70 * @returns {Promise<Object>} Task status
71 */
72async function getTaskStatus(taskId) {
73 const response = await fetch('/api/horde/task-status', {
74 method: 'POST',
75 headers: getRequestHeaders(),
76 body: JSON.stringify({ taskId }),
77 });
78
79 if (!response.ok) {
80 throw new Error(`Failed to get task status: ${response.statusText}`);
81 }
82
83 return await response.json();
84}
85
86/**
87 * Cancels a Horde task.
88 * @param {string} taskId Task ID
89 */
90async function cancelTask(taskId) {
91 const response = await fetch('/api/horde/cancel-task', {
92 method: 'POST',
93 headers: getRequestHeaders(),
94 body: JSON.stringify({ taskId }),
95 });
96
97 if (!response.ok) {
98 throw new Error(`Failed to cancel task: ${response.statusText}`);
99 }
100}
101
102/**
103 * Checks if Horde is online.
104 * @returns {Promise<boolean>} True if Horde is online, false otherwise
105 */
106export async function checkHordeStatus() {
107 try {
108 const response = await fetch('/api/horde/status', {
109 method: 'POST',
110 headers: getRequestHeaders({ omitContentType: true }),
111 });
112
113 if (!response.ok) {
114 return false;
115 }
116
117 const data = await response.json();
118 return data.ok;
119 } catch (error) {
120 console.error(error);
121 return false;
122 }
123}
124
125export async function getStatusHorde() {
126 try {
127 const hordeStatus = await checkHordeStatus();
128 setOnlineStatus(hordeStatus ? t`Connected` : 'no_connection');
129 } catch {
130 setOnlineStatus('no_connection');
131 }
132
133 return resultCheckStatus();
134}
135
136function validateHordeModel() {
137 let selectedModels = models.filter(m => horde_settings.models.includes(m.name));
138
139 if (selectedModels.length === 0) {
140 toastr.warning('No Horde model selected or the selected models are no longer available. Please choose another model');
141 throw new Error('No Horde model available');
142 }
143
144 return selectedModels;
145}
146
147export async function adjustHordeGenerationParams(max_context_length, max_length) {
148 console.log(max_context_length, max_length);
149 const workers = await getWorkers(false);
150 let maxContextLength = max_context_length;
151 let maxLength = max_length;
152 let availableWorkers = [];
153 let selectedModels = validateHordeModel();
154
155 if (selectedModels.length === 0) {
156 return { maxContextLength, maxLength };
157 }
158
159 for (const model of selectedModels) {
160 for (const worker of workers) {
161 if (model.cluster === worker.cluster && worker.models.includes(model.name)) {
162 // Skip workers that are not trusted if the option is enabled
163 if (horde_settings.trusted_workers_only && !worker.trusted) {
164 continue;
165 }
166
167 availableWorkers.push(worker);
168 }
169 }
170 }
171
172 //get the minimum requires parameters, lowest common value for all selected
173 for (const worker of availableWorkers) {
174 if (horde_settings.auto_adjust_context_length) {
175 maxContextLength = Math.min(worker.max_context_length, maxContextLength);
176 }
177 if (horde_settings.auto_adjust_response_length) {
178 maxLength = Math.min(worker.max_length, maxLength);
179 }
180 }
181 console.log(maxContextLength, maxLength);
182 $('#adjustedHordeParams').text(t`Context` + `: ${maxContextLength}, ` + t`Response` + `: ${maxLength}`);
183 return { maxContextLength, maxLength };
184}
185
186function setContextSizePreview() {
187 if (horde_settings.models.length) {
188 adjustHordeGenerationParams(max_context, amount_gen);
189 } else {
190 $('#adjustedHordeParams').text(t`Context` + ': --, ' + t`Response` + ': --');
191 }
192}
193
194/** Generates text using the Horde API.
195 * @param {string} prompt
196 * @param params
197 * @param signal
198 * @param reportProgress
199 * @returns {Promise<{text: *, workerName: string}>}
200 * @throws {Error}
201 */
202export async function generateHorde(prompt, params, signal, reportProgress) {
203 validateHordeModel();
204 delete params.prompt;
205
206 // No idea what these do
207 params.n = 1;
208 params.frmtadsnsp = false;
209 params.frmtrmblln = false;
210 params.frmtrmspch = false;
211 params.frmttriminc = false;
212
213 const payload = {
214 'prompt': prompt,
215 'params': params,
216 'trusted_workers': horde_settings.trusted_workers_only,
217 //"slow_workers": false,
218 'models': horde_settings.models,
219 };
220
221 const response = await fetch('/api/horde/generate-text', {
222 method: 'POST',
223 headers: getRequestHeaders(),
224 body: JSON.stringify(payload),
225 });
226
227 if (!response.ok) {
228 toastr.error(response.statusText, 'Horde generation failed');
229 throw new Error(`Horde generation failed: ${response.statusText}`);
230 }
231
232 const responseJson = await response.json();
233
234 if (responseJson.error) {
235 const reason = responseJson.error?.message || 'Unknown error';
236 toastr.error(reason, 'Horde generation failed');
237 throw new Error(`Horde generation failed: ${reason}`);
238 }
239
240 const taskId = responseJson.id;
241 let queue_position_first = null;
242 console.log(`Horde task id = ${taskId}`);
243
244 for (let retryNumber = 0; retryNumber < MAX_RETRIES; retryNumber++) {
245 if (signal.aborted) {
246 cancelTask(taskId);
247 throw new Error('Request aborted');
248 }
249
250 const statusCheckJson = await getTaskStatus(taskId);
251 console.log(statusCheckJson);
252
253 if (statusCheckJson.faulted === true) {
254 toastr.error('Horde request faulted. Please try again.');
255 throw new Error('Horde generation failed: Faulted');
256 }
257
258 if (statusCheckJson.is_possible === false) {
259 toastr.error('There are no Horde workers that are able to generate text with your request. Please change the parameters or try again later.');
260 throw new Error('Horde generation failed: Unsatisfiable request');
261 }
262
263 if (statusCheckJson.done && Array.isArray(statusCheckJson.generations) && statusCheckJson.generations.length) {
264 reportProgress && setGenerationProgress(100);
265 const generatedText = statusCheckJson.generations[0].text;
266 const WorkerName = statusCheckJson.generations[0].worker_name;
267 const WorkerModel = statusCheckJson.generations[0].model;
268 console.log(generatedText);
269 console.log(`Generated by Horde Worker: ${WorkerName} [${WorkerModel}]`);
270 return { text: generatedText, workerName: `Generated by Horde worker: ${WorkerName} [${WorkerModel}]` };
271 } else if (!queue_position_first) {
272 queue_position_first = statusCheckJson.queue_position;
273 reportProgress && setGenerationProgress(0);
274 } else if (statusCheckJson.queue_position >= 0) {
275 let queue_position = statusCheckJson.queue_position;
276 const progress = Math.round(100 - (queue_position / queue_position_first * 100));
277 reportProgress && setGenerationProgress(progress);
278 }
279
280 await delay(CHECK_INTERVAL);
281 }
282
283 await callGenericPopup(t`Horde request timed out. Try again`, POPUP_TYPE.TEXT);
284 throw new Error('Horde timeout');
285}
286
287
288/**
289 * Displays the available models in the Horde model selection dropdown.
290 * @param {boolean} force Force refresh of the models
291 */
292export async function getHordeModels(force) {
293 const sortByPerformance = (a, b) => b.performance - a.performance;
294 const sortByWhitelisted = (a, b) => b.is_whitelisted - a.is_whitelisted;
295 const sortByPopular = (a, b) => b.tags?.includes('popular') - a.tags?.includes('popular');
296
297 $('#horde_model').empty();
298 models = (await getModels(force)).sort((a, b) => {
299 return sortByWhitelisted(a, b) || sortByPopular(a, b) || sortByPerformance(a, b);
300 });
301 for (const model of models) {
302 const option = document.createElement('option');
303 option.value = model.name;
304 option.innerText = hordeModelTextString(model);
305 option.selected = horde_settings.models.includes(model.name);
306 $('#horde_model').append(option);
307 }
308
309 // if previously selected is no longer available
310 if (horde_settings.models.length && models.filter(m => horde_settings.models.includes(m.name)).length === 0) {
311 horde_settings.models = [];
312 }
313
314 setContextSizePreview();
315}
316
317export function loadHordeSettings(settings) {
318 if (settings.horde_settings) {
319 Object.assign(horde_settings, settings.horde_settings);
320 }
321
322 $('#horde_auto_adjust_response_length').prop('checked', horde_settings.auto_adjust_response_length);
323 $('#horde_auto_adjust_context_length').prop('checked', horde_settings.auto_adjust_context_length);
324 $('#horde_trusted_workers_only').prop('checked', horde_settings.trusted_workers_only);
325}
326
327async function showKudos() {
328 const response = await fetch('/api/horde/user-info', {
329 method: 'POST',
330 headers: getRequestHeaders({ omitContentType: true }),
331 });
332
333 if (!response.ok) {
334 toastr.warning('Could not load user info from Horde. Please try again later.');
335 return;
336 }
337
338 const data = await response.json();
339
340 if (data.anonymous) {
341 toastr.info('You are in anonymous mode. Set your personal Horde API key to see kudos.');
342 return;
343 }
344
345 console.log('Horde user data', data.user, 'shared key data', data.sharedKey);
346 const kudos = data.sharedKey?.kudos ?? data.user?.kudos ?? 0;
347 toastr.info(`Kudos: ${kudos}`, data.user.username);
348}
349
350function hordeModelTextString(model) {
351 const q = hordeModelQueueStateString(model);
352 return `${model.name} (${q})`;
353}
354
355function hordeModelQueueStateString(model) {
356 return `ETA: ${model.eta}s, Speed: ${model.performance}, Queue: ${model.queued}, Workers: ${model.count}`;
357}
358
359export function isHordeGenerationNotAllowed() {
360 if (main_api == 'koboldhorde' && kai_settings.preset_settings == 'gui') {
361 toastr.error(t`GUI Settings preset is not supported for Horde. Please select another preset.`);
362 return true;
363 }
364
365 return false;
366}
367
368function getHordeModelTemplate(option) {
369 const model = models.find(x => x.name === option?.element?.value);
370
371 if (!option.id || !model) {
372 console.debug('No model found for option', option, option?.element?.value);
373 console.debug('Models', models);
374 return option.text;
375 }
376
377 const strip = html => {
378 const tmp = document.createElement('DIV');
379 tmp.innerHTML = html || '';
380 return tmp.textContent || tmp.innerText || '';
381 };
382
383 // how much do we trust the metadata from the models repo? about this much
384 const displayName = strip(model.display_name || model.name).replace(/.*\//g, '');
385 const description = strip(model.description);
386 const tags = model.tags ? model.tags.map(strip) : [];
387 const url = strip(model.url);
388 const style = strip(model.style);
389
390 const workerInfo = hordeModelQueueStateString(model);
391 const isPopular = model.tags?.includes('popular');
392 const descriptionDiv = description ? `<div class="horde-model-description">${description}</div>` : '';
393 const tagSpans = tags.length > 0 &&
394 `${tags.map(tag => `<span class="tag tag_name">${tag}</span>`).join('')}</span>` || '';
395
396 const modelDetailsLink = url && `<a href="${url}" target="_blank" rel="noopener noreferrer" class="model-details-link fa-solid fa-circle-question"> </a>`;
397 const capitalize = s => s ? s[0].toUpperCase() + s.slice(1) : '';
398 const innerContent = [
399 `<strong>${displayName}</strong> ${modelDetailsLink}`,
400 style ? `${capitalize(style)}` : '',
401 tagSpans ? `<span class="tags tags_inline inline-flex margin-r2">${tagSpans}</span>` : '',
402 ].filter(Boolean).join(' | ');
403
404 return $((`
405 <div class="flex-container flexFlowColumn">
406 <div>
407 ${isPopular ? '<span class="fa-fw fa-solid fa-star" title="Popular"></span>' : ''}
408 ${innerContent}
409 </div>
410 ${descriptionDiv}
411 <div><small>${workerInfo}</small></div>
412 </div>
413 `));
414}
415
416export function initHorde() {
417 $('#horde_model').on('mousedown change', async function (e) {
418 console.log('Horde model change', e);
419 const modelValue = $('#horde_model').val();
420 horde_settings.models = Array.isArray(modelValue) ? modelValue : [];
421 console.log('Updated Horde models', horde_settings.models);
422
423 // Try select instruct preset
424 autoSelectInstructPreset(horde_settings.models.join(' '));
425 if (horde_settings.models.length) {
426 adjustHordeGenerationParams(max_context, amount_gen);
427 } else {
428 $('#adjustedHordeParams').text(t`Context` + ': --, ' + t`Response` + ': --');
429 }
430
431 saveSettingsDebounced();
432 });
433
434 $('#horde_auto_adjust_response_length').on('input', function () {
435 horde_settings.auto_adjust_response_length = !!$(this).prop('checked');
436 setContextSizePreview();
437 saveSettingsDebounced();
438 });
439
440 $('#horde_auto_adjust_context_length').on('input', function () {
441 horde_settings.auto_adjust_context_length = !!$(this).prop('checked');
442 setContextSizePreview();
443 saveSettingsDebounced();
444 });
445
446 $('#horde_trusted_workers_only').on('input', function () {
447 horde_settings.trusted_workers_only = !!$(this).prop('checked');
448 setContextSizePreview();
449 saveSettingsDebounced();
450 });
451
452 $('#horde_api_key_button').on('click', async function () {
453 const key = String($('#horde_api_key').val()).trim();
454 if (!key) {
455 toastr.warning(t`Please enter your Horde API key`);
456 return;
457 }
458 await writeSecret(SECRET_KEYS.HORDE, key);
459 });
460
461 $('#horde_refresh').on('click', () => getHordeModels(true));
462 $('#horde_kudos').on('click', showKudos);
463
464 // Not needed on mobile
465 if (!isMobile()) {
466 $('#horde_model').select2({
467 width: '100%',
468 placeholder: t`Select Horde models`,
469 allowClear: true,
470 closeOnSelect: false,
471 templateSelection: function (data) {
472 // Customize the pillbox text by shortening the full text
473 return data.id;
474 },
475 templateResult: getHordeModelTemplate,
476 });
477 }
478}
479