Blame Raw
Cohee · 51ad27fb · · 1472 lines (46.4 KB)
3 contributors
1import { DOMPurify } from '../lib.js';
2import { isMobile } from './RossAscends-mods.js';
3import { amount_gen, eventSource, event_types, getRequestHeaders, max_context, online_status, setGenerationParamsFromPreset } from '../script.js';
4import { textgenerationwebui_settings as textgen_settings, textgen_types } from './textgen-settings.js';
5import { tokenizers } from './tokenizers.js';
6import { renderTemplateAsync } from './templates.js';
7import { POPUP_TYPE, callGenericPopup } from './popup.js';
8import { t } from './i18n.js';
9import { accountStorage } from './util/AccountStorage.js';
10import { localizePagination, PAGINATION_TEMPLATE, textValueMatcher } from './utils.js';
11
12let mancerModels = [];
13let togetherModels = [];
14let infermaticAIModels = [];
15let dreamGenModels = [];
16let vllmModels = [];
17let aphroditeModels = [];
18let featherlessModels = [];
19let tabbyModels = [];
20let llamacppModels = [];
21export let openRouterModels = [];
22
23/**
24 * List of OpenRouter providers.
25 * @type {string[]}
26 */
27const OPENROUTER_PROVIDERS = [
28 // Providers endpoint: https://openrouter.ai/api/v1/providers
29 // The list should resemble the sidebar from https://openrouter.ai/models
30 // Their docs no longer displays the list, which had "super dead" ones at top, thankfully gone from /v1/providers
31 'AI21',
32 'AionLabs',
33 'Alibaba',
34 'AkashML',
35 'Amazon Bedrock',
36 'Amazon Nova',
37 'Ambient',
38 'Anthropic',
39 'Arcee AI',
40 'AtlasCloud',
41 'Avian',
42 'Azure',
43 'Baidu',
44 'BaseTen',
45 'Black Forest Labs',
46 'Cerebras',
47 'Chutes',
48 'Cirrascale',
49 'Clarifai',
50 'Cloudflare',
51 'Cohere',
52 'Crusoe',
53 'DeepInfra',
54 'DeepSeek',
55 'DekaLLM',
56 'FakeProvider',
57 'Featherless',
58 'Fireworks',
59 'Friendli',
60 'GMICloud',
61 'Google',
62 'Google AI Studio',
63 'Groq',
64 'Hyperbolic',
65 'Inception',
66 'Inceptron',
67 'InferenceNet',
68 'Infermatic',
69 'Inflection',
70 'Io Net',
71 'Ionstream',
72 'Liquid',
73 'Mancer 2',
74 'Mara',
75 'Minimax',
76 'Mistral',
77 'ModelRun',
78 'Modular',
79 'Moonshot AI',
80 'Morph',
81 'NCompass',
82 'Nebius',
83 'NextBit',
84 'Novita',
85 'Nvidia',
86 'OpenAI',
87 'OpenInference',
88 'Parasail',
89 'Perplexity',
90 'Phala',
91 'Recraft',
92 'Reka',
93 'Relace',
94 'SambaNova',
95 'Seed',
96 'SiliconFlow',
97 'Sourceful',
98 'Stealth',
99 'StepFun',
100 'StreamLake',
101 'Switchpoint',
102 'Together',
103 'Upstage',
104 'Venice',
105 'WandB',
106 'xAI',
107 'Xiaomi',
108 'Z.AI',
109];
110
111/**
112 * List of NanoGPT providers.
113 * Providers endpoint: https://nano-gpt.com/api/models/providers
114 * @type {{id: string, label: string}[]}
115 */
116const NANOGPT_PROVIDERS = [
117 {
118 'id': 'akash',
119 'label': 'Akash',
120 },
121 {
122 'id': 'alibaba',
123 'label': 'Alibaba',
124 },
125 {
126 'id': 'ambient',
127 'label': 'Ambient',
128 },
129 {
130 'id': 'arliai',
131 'label': 'ArliAI',
132 },
133 {
134 'id': 'atlascloud',
135 'label': 'AtlasCloud',
136 },
137 {
138 'id': 'azure',
139 'label': 'Azure',
140 },
141 {
142 'id': 'awsbedrock',
143 'label': 'Amazon Bedrock',
144 },
145 {
146 'id': 'baidu',
147 'label': 'Baidu',
148 },
149 {
150 'id': 'baseten',
151 'label': 'BaseTen',
152 },
153 {
154 'id': 'cerebras',
155 'label': 'Cerebras',
156 },
157 {
158 'id': 'chutes',
159 'label': 'Chutes',
160 },
161 {
162 'id': 'clarifai',
163 'label': 'Clarifai',
164 },
165 {
166 'id': 'cloudflare',
167 'label': 'Cloudflare',
168 },
169 {
170 'id': 'crusoe',
171 'label': 'Crusoe',
172 },
173 {
174 'id': 'dekallm',
175 'label': 'DekaLLM',
176 },
177 {
178 'id': 'deepinfra',
179 'label': 'DeepInfra',
180 },
181 {
182 'id': 'deepseek',
183 'label': 'DeepSeek',
184 },
185 {
186 'id': 'fireworks',
187 'label': 'Fireworks',
188 },
189 {
190 'id': 'friendli',
191 'label': 'Friendli',
192 },
193 {
194 'id': 'gmicloud',
195 'label': 'GMICloud',
196 },
197 {
198 'id': 'lilac',
199 'label': 'Lilac',
200 },
201 {
202 'id': 'google',
203 'label': 'Google',
204 },
205 {
206 'id': 'groq',
207 'label': 'Groq',
208 },
209 {
210 'id': 'hyperbolic',
211 'label': 'Hyperbolic',
212 },
213 {
214 'id': 'ionet',
215 'label': 'Io Net',
216 },
217 {
218 'id': 'inceptron',
219 'label': 'Inceptron',
220 },
221 {
222 'id': 'mancer',
223 'label': 'Mancer',
224 },
225 {
226 'id': 'mara',
227 'label': 'Mara',
228 },
229 {
230 'id': 'meganova',
231 'label': 'MegaNova',
232 },
233 {
234 'id': 'minimax',
235 'label': 'MiniMax',
236 },
237 {
238 'id': 'modelrun',
239 'label': 'ModelRun',
240 },
241 {
242 'id': 'moonshot',
243 'label': 'Moonshot',
244 },
245 {
246 'id': 'morph',
247 'label': 'Morph',
248 },
249 {
250 'id': 'ncompass',
251 'label': 'NCompass',
252 },
253 {
254 'id': 'nebius',
255 'label': 'Nebius',
256 },
257 {
258 'id': 'neuralwatt',
259 'label': 'Neuralwatt',
260 },
261 {
262 'id': 'nextbit',
263 'label': 'NextBit',
264 },
265 {
266 'id': 'novita',
267 'label': 'Novita',
268 },
269 {
270 'id': 'parasail',
271 'label': 'Parasail',
272 },
273 {
274 'id': 'phala',
275 'label': 'Phala',
276 },
277 {
278 'id': 'redpill',
279 'label': 'Redpill',
280 },
281 {
282 'id': 'sambanova',
283 'label': 'SambaNova',
284 },
285 {
286 'id': 'sambanova-high-throughput',
287 'label': 'SambaNova (High Throughput)',
288 },
289 {
290 'id': 'siliconflow',
291 'label': 'SiliconFlow',
292 },
293 {
294 'id': 'streamlake',
295 'label': 'StreamLake',
296 },
297 {
298 'id': 'tinfoil',
299 'label': 'Tinfoil',
300 },
301 {
302 'id': 'together',
303 'label': 'Together',
304 },
305 {
306 'id': 'venice',
307 'label': 'Venice',
308 },
309 {
310 'id': 'wandb',
311 'label': 'Weights & Biases',
312 },
313 {
314 'id': 'zai',
315 'label': 'Z.AI',
316 },
317];
318
319const OPENROUTER_PROVIDER_WARNING_SELECTORS = {
320 '#openrouter_providers_text': {
321 fallbackSelector: '#openrouter_allow_fallbacks_textgenerationwebui',
322 warningSelector: '#openrouter_provider_warning_text',
323 },
324 '#openrouter_providers_chat': {
325 fallbackSelector: '#openrouter_allow_fallbacks',
326 warningSelector: '#openrouter_provider_warning_chat',
327 },
328};
329
330export function updateOpenRouterProvidersWarning(providersSelector) {
331 const $providers = $(providersSelector);
332
333 const warningSelectors = OPENROUTER_PROVIDER_WARNING_SELECTORS[providersSelector];
334
335 if ($providers.length === 0 || !warningSelectors) {
336 return;
337 }
338
339 const $fallback = $(warningSelectors.fallbackSelector);
340 const $warning = $(warningSelectors.warningSelector);
341
342 const allowFallback = !!$fallback.prop('checked');
343 const selectedCount = $providers.find('option:selected').length;
344 const applicableSelectedCount = $providers.find('option:selected:not(:disabled)').length;
345 const showWarning = !allowFallback && selectedCount > 0 && applicableSelectedCount === 0;
346
347 $warning.toggleClass('displayNone', !showWarning);
348}
349
350export async function syncOpenRouterProvidersForModel(modelId, providersSelector) {
351 const $providers = $(providersSelector);
352
353 const refreshWarningState = () => {
354 updateOpenRouterProvidersWarning(providersSelector);
355 };
356
357 if (!modelId || !modelId.includes('/')) {
358 $providers.find('option').prop('disabled', false);
359 $providers.trigger('change.select2');
360 refreshWarningState();
361 return;
362 }
363
364 try {
365 const response = await fetch('/api/openrouter/models/providers', {
366 method: 'POST',
367 headers: getRequestHeaders(),
368 body: JSON.stringify({ model: modelId }),
369 });
370
371 if (!response.ok) {
372 refreshWarningState();
373 return;
374 }
375
376 const providerNames = await response.json();
377
378 if (!Array.isArray(providerNames) || providerNames.length === 0) {
379 $providers.find('option').prop('disabled', false);
380 $providers.trigger('change.select2');
381 refreshWarningState();
382 return;
383 }
384
385 $providers.find('option').each(function () {
386 const isAvailable = providerNames.includes($(this).val());
387 $(this).prop('disabled', !isAvailable);
388 });
389
390 $providers.trigger('change.select2');
391 refreshWarningState();
392 } catch (error) {
393 console.error('Failed to fetch OpenRouter providers for model', error);
394 refreshWarningState();
395 }
396}
397
398export async function syncNanoGptProvidersForModel(modelId, providersSelector) {
399 const $providers = $(providersSelector);
400
401 const refreshWarningState = () => {
402 updateNanoGptProvidersWarning(providersSelector);
403 };
404
405 if (!modelId) {
406 $providers.find('option').prop('disabled', false);
407 $providers.trigger('change.select2');
408 refreshWarningState();
409 return;
410 }
411
412 try {
413 const response = await fetch('/api/nanogpt/models/providers', {
414 method: 'POST',
415 headers: getRequestHeaders(),
416 body: JSON.stringify({ model: modelId }),
417 });
418
419 if (!response.ok) {
420 refreshWarningState();
421 return;
422 }
423
424 const data = await response.json();
425 const providerIds = Array.isArray(data?.providers) ? data.providers : [];
426
427 if (!data?.supportsProviderSelection || providerIds.length === 0) {
428 $providers.find('option').each(function () {
429 $(this).prop('disabled', Boolean($(this).val()));
430 });
431 $providers.trigger('change').trigger('change.select2');
432 refreshWarningState();
433 return;
434 }
435
436 $providers.find('option').each(function () {
437 const value = $(this).val();
438 const isAvailable = !value || providerIds.includes(value);
439 $(this).prop('disabled', !isAvailable);
440 });
441
442 $providers.trigger('change.select2');
443 refreshWarningState();
444 } catch (error) {
445 console.error('Failed to fetch NanoGPT providers for model', error);
446 refreshWarningState();
447 }
448}
449
450export function updateNanoGptProvidersWarning(providersSelector) {
451 const $providers = $(providersSelector);
452
453 if ($providers.length === 0) {
454 return;
455 }
456
457 const selectedCount = $providers.find('option:selected').length;
458 const applicableSelectedCount = $providers.find('option:selected:not(:disabled)').length;
459 const showWarning = selectedCount > 0 && applicableSelectedCount === 0;
460
461 $('#nanogpt_provider_warning').toggleClass('displayNone', !showWarning);
462}
463
464export async function loadOllamaModels(data) {
465 if (!Array.isArray(data)) {
466 console.error('Invalid Ollama models data', data);
467 return;
468 }
469
470 if (!data.find(x => x.id === textgen_settings.ollama_model)) {
471 textgen_settings.ollama_model = data[0]?.id || '';
472 }
473
474 $('#ollama_model').empty();
475 for (const model of data) {
476 const option = document.createElement('option');
477 option.value = model.id;
478 option.text = model.name;
479 option.selected = model.id === textgen_settings.ollama_model;
480 $('#ollama_model').append(option);
481 }
482}
483
484export async function loadTabbyModels(data) {
485 if (!Array.isArray(data)) {
486 console.error('Invalid Tabby models data', data);
487 return;
488 }
489
490 tabbyModels = data;
491 tabbyModels.sort((a, b) => a.id.localeCompare(b.id));
492 tabbyModels.unshift({ id: '' });
493
494 if (!tabbyModels.find(x => x.id === textgen_settings.tabby_model)) {
495 textgen_settings.tabby_model = tabbyModels[0]?.id || '';
496 }
497
498 $('#tabby_model').empty();
499 for (const model of tabbyModels) {
500 const option = document.createElement('option');
501 option.value = model.id;
502 option.text = model.id;
503 option.selected = model.id === textgen_settings.tabby_model;
504 $('#tabby_model').append(option);
505 }
506}
507
508export async function loadLlamaCppModels(data) {
509 if (!Array.isArray(data)) {
510 console.error('Invalid llama.cpp models data', data);
511 return;
512 }
513
514 llamacppModels = data;
515 llamacppModels.sort((a, b) => a.id.localeCompare(b.id));
516 llamacppModels.unshift({ id: '' });
517
518 if (!llamacppModels.find(x => x.id === textgen_settings.llamacpp_model)) {
519 textgen_settings.llamacpp_model = llamacppModels[0]?.id || '';
520 }
521
522 $('#llamacpp_model').empty();
523 for (const model of llamacppModels) {
524 const option = document.createElement('option');
525 option.value = model.id;
526 option.text = model.id;
527 option.selected = model.id === textgen_settings.llamacpp_model;
528 $('#llamacpp_model').append(option);
529 }
530}
531
532export async function loadTogetherAIModels(data) {
533 if (!Array.isArray(data)) {
534 console.error('Invalid Together AI models data', data);
535 return;
536 }
537
538 data.sort((a, b) => a.id.localeCompare(b.id));
539 togetherModels = data;
540
541 if (!data.find(x => x.id === textgen_settings.togetherai_model)) {
542 textgen_settings.togetherai_model = data[0]?.id || '';
543 }
544
545 $('#model_togetherai_select').empty();
546 for (const model of data) {
547 // Hey buddy, I think you've got the wrong door.
548 if (model.type === 'image') {
549 continue;
550 }
551
552 const option = document.createElement('option');
553 option.value = model.id;
554 option.text = model.display_name;
555 option.selected = model.id === textgen_settings.togetherai_model;
556 $('#model_togetherai_select').append(option);
557 }
558}
559
560export async function loadInfermaticAIModels(data) {
561 if (!Array.isArray(data)) {
562 console.error('Invalid Infermatic AI models data', data);
563 return;
564 }
565
566 data.sort((a, b) => a.id.localeCompare(b.id));
567 infermaticAIModels = data;
568
569 if (!data.find(x => x.id === textgen_settings.infermaticai_model)) {
570 textgen_settings.infermaticai_model = data[0]?.id || '';
571 }
572
573 $('#model_infermaticai_select').empty();
574 for (const model of data) {
575 if (model.display_type === 'image') {
576 continue;
577 }
578
579 const option = document.createElement('option');
580 option.value = model.id;
581 option.text = model.id;
582 option.selected = model.id === textgen_settings.infermaticai_model;
583 $('#model_infermaticai_select').append(option);
584 }
585}
586
587export function loadGenericModels(data) {
588 if (!Array.isArray(data)) {
589 console.error('Invalid Generic models data', data);
590 return;
591 }
592
593 data.sort((a, b) => a.id.localeCompare(b.id));
594 const dataList = $('#generic_model_fill');
595 dataList.empty();
596
597 for (const model of data) {
598 const option = document.createElement('option');
599 option.value = model.id;
600 option.text = model.id;
601 dataList.append(option);
602 }
603}
604
605export async function loadDreamGenModels(data) {
606 if (!Array.isArray(data)) {
607 console.error('Invalid DreamGen models data', data);
608 return;
609 }
610
611 dreamGenModels = data;
612
613 if (!data.find(x => x.id === textgen_settings.dreamgen_model)) {
614 textgen_settings.dreamgen_model = data[0]?.id || '';
615 }
616
617 $('#model_dreamgen_select').empty();
618 for (const model of data) {
619 if (model.display_type === 'image') {
620 continue;
621 }
622
623 const option = document.createElement('option');
624 option.value = model.id;
625 option.text = model.id;
626 option.selected = model.id === textgen_settings.dreamgen_model;
627 $('#model_dreamgen_select').append(option);
628 }
629}
630
631export async function loadMancerModels(data) {
632 if (!Array.isArray(data)) {
633 console.error('Invalid Mancer models data', data);
634 return;
635 }
636
637 data.sort((a, b) => a.name.localeCompare(b.name));
638 mancerModels = data;
639
640 if (!data.find(x => x.id === textgen_settings.mancer_model)) {
641 textgen_settings.mancer_model = data[0]?.id || '';
642 }
643
644 $('#mancer_model').empty();
645 for (const model of data) {
646 const option = document.createElement('option');
647 option.value = model.id;
648 option.text = model.name;
649 option.selected = model.id === textgen_settings.mancer_model;
650 $('#mancer_model').append(option);
651 }
652}
653
654export async function loadOpenRouterModels(data) {
655 if (!Array.isArray(data)) {
656 console.error('Invalid OpenRouter models data', data);
657 return;
658 }
659
660 data.sort((a, b) => a.name.localeCompare(b.name));
661 openRouterModels = data;
662
663 if (!data.find(x => x.id === textgen_settings.openrouter_model)) {
664 textgen_settings.openrouter_model = data[0]?.id || '';
665 }
666
667 $('#openrouter_model').empty();
668 for (const model of data) {
669 const option = document.createElement('option');
670 option.value = model.id;
671 option.text = model.name;
672 option.selected = model.id === textgen_settings.openrouter_model;
673 $('#openrouter_model').append(option);
674 }
675
676 // Calculate the cost of the selected model + update on settings change
677 calculateOpenRouterCost();
678 syncOpenRouterProvidersForModel(textgen_settings.openrouter_model, '#openrouter_providers_text');
679}
680
681export async function loadVllmModels(data) {
682 if (!Array.isArray(data)) {
683 console.error('Invalid vLLM models data', data);
684 return;
685 }
686
687 vllmModels = data;
688
689 if (!data.find(x => x.id === textgen_settings.vllm_model)) {
690 textgen_settings.vllm_model = data[0]?.id || '';
691 }
692
693 $('#vllm_model').empty();
694 for (const model of data) {
695 const option = document.createElement('option');
696 option.value = model.id;
697 option.text = model.id;
698 option.selected = model.id === textgen_settings.vllm_model;
699 $('#vllm_model').append(option);
700 }
701}
702
703export async function loadAphroditeModels(data) {
704 if (!Array.isArray(data)) {
705 console.error('Invalid Aphrodite models data', data);
706 return;
707 }
708
709 aphroditeModels = data;
710
711 if (!data.find(x => x.id === textgen_settings.aphrodite_model)) {
712 textgen_settings.aphrodite_model = data[0]?.id || '';
713 }
714
715 $('#aphrodite_model').empty();
716 for (const model of data) {
717 const option = document.createElement('option');
718 option.value = model.id;
719 option.text = model.id;
720 option.selected = model.id === textgen_settings.aphrodite_model;
721 $('#aphrodite_model').append(option);
722 }
723}
724
725let featherlessCurrentPage = 1;
726export async function loadFeatherlessModels(data) {
727 const searchBar = document.getElementById('featherless_model_search_bar');
728 const modelCardBlock = document.getElementById('featherless_model_card_block');
729 const paginationContainer = $('#featherless_model_pagination_container');
730 const sortOrderSelect = document.getElementById('featherless_model_sort_order');
731 const classSelect = document.getElementById('featherless_class_selection');
732 const categoriesSelect = document.getElementById('featherless_category_selection');
733 const storageKey = 'FeatherlessModels_PerPage';
734
735 // Store the original models data for search and filtering
736 let originalModels = [];
737
738 if (!Array.isArray(data)) {
739 console.error('Invalid Featherless models data', data);
740 return;
741 }
742
743 originalModels = data; // Store the original data for search
744 featherlessModels = data;
745
746 if (!data.find(x => x.id === textgen_settings.featherless_model)) {
747 textgen_settings.featherless_model = data[0]?.id || '';
748 }
749
750 // Populate class select options with unique classes
751 populateClassSelection(data);
752
753 // Retrieve the stored number of items per page or default to 10
754 const perPage = Number(accountStorage.getItem(storageKey)) || 10;
755
756 // Initialize pagination
757 applyFiltersAndSort();
758
759 // Function to set up pagination (also used for filtered results)
760 function setupPagination(models, perPage, pageNumber = featherlessCurrentPage) {
761 paginationContainer.pagination({
762 dataSource: models,
763 pageSize: perPage,
764 pageNumber: pageNumber,
765 sizeChangerOptions: [6, 10, 26, 50, 100, 250, 500, 1000],
766 pageRange: 1,
767 showPageNumbers: true,
768 showSizeChanger: false,
769 prevText: '<',
770 nextText: '>',
771 formatNavigator: PAGINATION_TEMPLATE,
772 showNavigator: true,
773 callback: function (modelsOnPage, pagination) {
774 modelCardBlock.innerHTML = '';
775
776 modelsOnPage.forEach(model => {
777 const card = document.createElement('div');
778 card.classList.add('model-card');
779
780 const modelNameContainer = document.createElement('div');
781 modelNameContainer.classList.add('model-name-container');
782
783 const modelTitle = document.createElement('div');
784 modelTitle.classList.add('model-title');
785 modelTitle.textContent = model.id.replace(/_/g, '_\u200B');
786 modelNameContainer.appendChild(modelTitle);
787
788 const detailsContainer = document.createElement('div');
789 detailsContainer.classList.add('details-container');
790
791 const modelClassDiv = document.createElement('div');
792 modelClassDiv.classList.add('model-class');
793 modelClassDiv.textContent = t`Class` + `: ${model.model_class || 'N/A'}`;
794
795 const contextLengthDiv = document.createElement('div');
796 contextLengthDiv.classList.add('model-context-length');
797 contextLengthDiv.textContent = t`Context Length` + `: ${model.context_length}`;
798
799 const dateAddedDiv = document.createElement('div');
800 dateAddedDiv.classList.add('model-date-added');
801 dateAddedDiv.textContent = t`Added On` + `: ${new Date(model.created * 1000).toLocaleDateString()}`;
802
803 detailsContainer.appendChild(modelClassDiv);
804 detailsContainer.appendChild(contextLengthDiv);
805 detailsContainer.appendChild(dateAddedDiv);
806
807 card.appendChild(modelNameContainer);
808 card.appendChild(detailsContainer);
809
810 modelCardBlock.appendChild(card);
811
812 if (model.id === textgen_settings.featherless_model) {
813 card.classList.add('selected');
814 }
815
816 card.addEventListener('click', function () {
817 document.querySelectorAll('.model-card').forEach(c => c.classList.remove('selected'));
818 card.classList.add('selected');
819 onFeatherlessModelSelect(model.id);
820 });
821 });
822
823 // Update the current page value whenever the page changes
824 featherlessCurrentPage = pagination.pageNumber;
825 localizePagination(paginationContainer);
826 },
827 afterSizeSelectorChange: function (e) {
828 const newPerPage = e.target.value;
829 accountStorage.setItem(storageKey, newPerPage);
830 setupPagination(models, Number(newPerPage), featherlessCurrentPage); // Use the stored current page number
831 },
832 });
833 }
834
835 // Unset previously added listeners
836 $(searchBar).off('input');
837 $(sortOrderSelect).off('change');
838 $(classSelect).off('change');
839 $(categoriesSelect).off('change');
840
841 // Add event listener for input on the search bar
842 searchBar.addEventListener('input', function () {
843 applyFiltersAndSort();
844 });
845
846 // Add event listener for the sort order select
847 sortOrderSelect.addEventListener('change', function () {
848 applyFiltersAndSort();
849 });
850
851 // Add event listener for the class select
852 classSelect.addEventListener('change', function () {
853 applyFiltersAndSort();
854 });
855
856 categoriesSelect.addEventListener('change', function () {
857 applyFiltersAndSort();
858 });
859
860 // Function to populate class selection dropdown
861 function populateClassSelection(models) {
862 const uniqueClasses = [...new Set(models.map(model => model.model_class).filter(Boolean))]; // Get unique class names
863 uniqueClasses.sort((a, b) => a.localeCompare(b));
864 uniqueClasses.forEach(className => {
865 const option = document.createElement('option');
866 option.value = className;
867 option.textContent = className;
868 classSelect.appendChild(option);
869 });
870 }
871
872 // Function to apply sorting and filtering based on user input
873 async function applyFiltersAndSort() {
874 if (!(searchBar instanceof HTMLInputElement) ||
875 !(sortOrderSelect instanceof HTMLSelectElement) ||
876 !(classSelect instanceof HTMLSelectElement) ||
877 !(categoriesSelect instanceof HTMLSelectElement)) {
878 return;
879 }
880 const searchQuery = searchBar.value.toLowerCase();
881 const selectedSortOrder = sortOrderSelect.value;
882 const selectedClass = classSelect.value;
883 const selectedCategory = categoriesSelect.value;
884 let featherlessTop = [];
885 let featherlessNew = [];
886
887 if (selectedCategory === 'Top') {
888 featherlessTop = await fetchFeatherlessStats();
889 }
890 const featherlessIds = featherlessTop.map(stat => stat.id);
891
892 if (selectedCategory === 'New') {
893 featherlessNew = await fetchFeatherlessNew();
894 }
895 const featherlessNewIds = featherlessNew.map(stat => stat.id);
896
897 let filteredModels = originalModels.filter(model => {
898 const matchesSearch = model.id.toLowerCase().includes(searchQuery);
899 const matchesClass = selectedClass ? model.model_class === selectedClass : true;
900 const matchesTop = featherlessIds.includes(model.id);
901 const matchesNew = featherlessNewIds.includes(model.id);
902
903 if (selectedCategory === 'All') {
904 return matchesSearch && matchesClass;
905 } else if (selectedCategory === 'Top') {
906 return matchesSearch && matchesClass && matchesTop;
907 } else if (selectedCategory === 'New') {
908 return matchesSearch && matchesClass && matchesNew;
909 } else {
910 return matchesSearch && matchesClass;
911 }
912 });
913
914 if (selectedSortOrder === 'asc') {
915 filteredModels.sort((a, b) => a.id.localeCompare(b.id));
916 } else if (selectedSortOrder === 'desc') {
917 filteredModels.sort((a, b) => b.id.localeCompare(a.id));
918 } else if (selectedSortOrder === 'date_asc') {
919 filteredModels.sort((a, b) => a.created - b.created);
920 } else if (selectedSortOrder === 'date_desc') {
921 filteredModels.sort((a, b) => b.created - a.created);
922 }
923
924 const currentModelIndex = filteredModels.findIndex(x => x.id === textgen_settings.featherless_model);
925 featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;
926
927 setupPagination(filteredModels, Number(accountStorage.getItem(storageKey)) || perPage, featherlessCurrentPage);
928 }
929
930 // Required to keep the /model command function
931 $('#featherless_model').empty();
932 for (const model of data) {
933 const option = document.createElement('option');
934 option.value = model.id;
935 option.text = model.id;
936 option.selected = model.id === textgen_settings.featherless_model;
937 $('#featherless_model').append(option);
938 }
939}
940
941async function fetchFeatherlessStats() {
942 const response = await fetch('https://api.featherless.ai/feather/popular');
943 const data = await response.json();
944 return data.popular;
945}
946
947async function fetchFeatherlessNew() {
948 const response = await fetch('https://api.featherless.ai/feather/models?sort=-created_at&perPage=20');
949 const data = await response.json();
950 return data.items;
951}
952
953function onFeatherlessModelSelect(modelId) {
954 const model = featherlessModels.find(x => x.id === modelId);
955 textgen_settings.featherless_model = modelId;
956 $('#featherless_model').val(modelId);
957 $('#api_button_textgenerationwebui').trigger('click');
958 setGenerationParamsFromPreset({ max_length: model.context_length });
959}
960
961let featherlessIsGridView = false; // Default state set to grid view
962
963// Ensure the correct initial view is applied when the page loads
964document.addEventListener('DOMContentLoaded', function () {
965 const modelCardBlock = document.getElementById('featherless_model_card_block');
966 modelCardBlock.classList.add('list-view');
967
968 const toggleButton = document.getElementById('featherless_model_grid_toggle');
969 toggleButton.addEventListener('click', function () {
970 // Toggle between grid and list view
971 if (featherlessIsGridView) {
972 modelCardBlock.classList.remove('grid-view');
973 modelCardBlock.classList.add('list-view');
974 this.title = 'Toggle to grid view';
975 } else {
976 modelCardBlock.classList.remove('list-view');
977 modelCardBlock.classList.add('grid-view');
978 this.title = 'Toggle to list view';
979 }
980
981 featherlessIsGridView = !featherlessIsGridView;
982 });
983});
984function onMancerModelSelect() {
985 const modelId = String($('#mancer_model').val());
986 textgen_settings.mancer_model = modelId;
987 $('#api_button_textgenerationwebui').trigger('click');
988
989 const limits = mancerModels.find(x => x.id === modelId)?.limits;
990 setGenerationParamsFromPreset({ max_length: limits.context });
991}
992
993function onTogetherModelSelect() {
994 const modelName = String($('#model_togetherai_select').val());
995 textgen_settings.togetherai_model = modelName;
996 $('#api_button_textgenerationwebui').trigger('click');
997 const model = togetherModels.find(x => x.id === modelName);
998 setGenerationParamsFromPreset({ max_length: model.context_length });
999}
1000
1001function onInfermaticAIModelSelect() {
1002 const modelName = String($('#model_infermaticai_select').val());
1003 textgen_settings.infermaticai_model = modelName;
1004 $('#api_button_textgenerationwebui').trigger('click');
1005 const model = infermaticAIModels.find(x => x.id === modelName);
1006 setGenerationParamsFromPreset({ max_length: model.context_length });
1007}
1008
1009function onDreamGenModelSelect() {
1010 const modelName = String($('#model_dreamgen_select').val());
1011 textgen_settings.dreamgen_model = modelName;
1012 $('#api_button_textgenerationwebui').trigger('click');
1013 // TODO(DreamGen): Consider retuning max_tokens from API and setting it here.
1014}
1015
1016function onOllamaModelSelect() {
1017 const modelId = String($('#ollama_model').val());
1018 textgen_settings.ollama_model = modelId;
1019 $('#api_button_textgenerationwebui').trigger('click');
1020}
1021
1022function onTabbyModelSelect() {
1023 const modelId = String($('#tabby_model').val());
1024 textgen_settings.tabby_model = modelId;
1025 $('#api_button_textgenerationwebui').trigger('click');
1026}
1027
1028function onLlamaCppModelSelect() {
1029 const modelId = String($('#llamacpp_model').val());
1030 textgen_settings.llamacpp_model = modelId;
1031 $('#api_button_textgenerationwebui').trigger('click');
1032}
1033
1034function onOpenRouterModelSelect() {
1035 const modelId = String($('#openrouter_model').val());
1036 textgen_settings.openrouter_model = modelId;
1037 $('#api_button_textgenerationwebui').trigger('click');
1038 const model = openRouterModels.find(x => x.id === modelId);
1039 syncOpenRouterProvidersForModel(modelId, '#openrouter_providers_text');
1040 setGenerationParamsFromPreset({ max_length: model.context_length });
1041}
1042
1043function onVllmModelSelect() {
1044 const modelId = String($('#vllm_model').val());
1045 textgen_settings.vllm_model = modelId;
1046 $('#api_button_textgenerationwebui').trigger('click');
1047}
1048
1049function onAphroditeModelSelect() {
1050 const modelId = String($('#aphrodite_model').val());
1051 textgen_settings.aphrodite_model = modelId;
1052 $('#api_button_textgenerationwebui').trigger('click');
1053}
1054
1055function getMancerModelTemplate(option) {
1056 const model = mancerModels.find(x => x.id === option?.element?.value);
1057
1058 if (!option.id || !model) {
1059 return option.text;
1060 }
1061
1062 const creditsPerPrompt = (model.limits?.context - model.limits?.completion) * model.pricing?.prompt;
1063 const creditsPerCompletion = model.limits?.completion * model.pricing?.completion;
1064 const creditsTotal = Math.round(creditsPerPrompt + creditsPerCompletion).toFixed(0);
1065
1066 return $((`
1067 <div class="flex-container flexFlowColumn">
1068 <div><strong>${DOMPurify.sanitize(model.name)}</strong> | <span>${model.limits?.context} ctx</span> / <span>${model.limits?.completion} res</span> | <small>Credits per request (max): ${creditsTotal}</small></div>
1069 </div>
1070 `));
1071}
1072
1073function getTogetherModelTemplate(option) {
1074 const model = togetherModels.find(x => x.id === option?.element?.value);
1075
1076 if (!option.id || !model) {
1077 return option.text;
1078 }
1079
1080 return $((`
1081 <div class="flex-container flexFlowColumn">
1082 <div><strong>${DOMPurify.sanitize(model.id)}</strong> | <span>${model.context_length || '???'} tokens</span></div>
1083 <div><small>${DOMPurify.sanitize(model.description)}</small></div>
1084 </div>
1085 `));
1086}
1087
1088function getInfermaticAIModelTemplate(option) {
1089 const model = infermaticAIModels.find(x => x.id === option?.element?.value);
1090
1091 if (!option.id || !model) {
1092 return option.text;
1093 }
1094
1095 return $((`
1096 <div class="flex-container flexFlowColumn">
1097 <div><strong>${DOMPurify.sanitize(model.id)}</strong></div>
1098 </div>
1099 `));
1100}
1101
1102function getDreamGenModelTemplate(option) {
1103 const model = dreamGenModels.find(x => x.id === option?.element?.value);
1104
1105 if (!option.id || !model) {
1106 return option.text;
1107 }
1108
1109 return $((`
1110 <div class="flex-container flexFlowColumn">
1111 <div><strong>${DOMPurify.sanitize(model.id)}</strong></div>
1112 </div>
1113 `));
1114}
1115
1116function getOpenRouterModelTemplate(option) {
1117 const model = openRouterModels.find(x => x.id === option?.element?.value);
1118
1119 if (!option.id || !model) {
1120 return option.text;
1121 }
1122
1123 let tokens_dollar = Number(1 / (1000 * model.pricing?.prompt));
1124 let tokens_rounded = (Math.round(tokens_dollar * 1000) / 1000).toFixed(0);
1125
1126 const price = 0 === Number(model.pricing?.prompt) ? 'Free' : `${tokens_rounded}k t/$ `;
1127
1128 return $((`
1129 <div class="flex-container flexFlowColumn" title="${DOMPurify.sanitize(model.id)}">
1130 <div><strong>${DOMPurify.sanitize(model.name)}</strong> | ${model.context_length} ctx | <small>${price}</small></div>
1131 </div>
1132 `));
1133}
1134
1135function getVllmModelTemplate(option) {
1136 const model = vllmModels.find(x => x.id === option?.element?.value);
1137
1138 if (!option.id || !model) {
1139 return option.text;
1140 }
1141
1142 return $((`
1143 <div class="flex-container flexFlowColumn">
1144 <div><strong>${DOMPurify.sanitize(model.id)}</strong></div>
1145 </div>
1146 `));
1147}
1148
1149function getAphroditeModelTemplate(option) {
1150 const model = aphroditeModels.find(x => x.id === option?.element?.value);
1151
1152 if (!option.id || !model) {
1153 return option.text;
1154 }
1155
1156 return $((`
1157 <div class="flex-container flexFlowColumn">
1158 <div><strong>${DOMPurify.sanitize(model.id)}</strong></div>
1159 </div>
1160 `));
1161}
1162
1163async function downloadOllamaModel() {
1164 try {
1165 const serverUrl = textgen_settings.server_urls[textgen_types.OLLAMA];
1166
1167 if (!serverUrl) {
1168 toastr.info('Please connect to an Ollama server first.');
1169 return;
1170 }
1171
1172 const html = `Enter a model tag, for example <code>llama2:latest</code>.<br>
1173 See <a target="_blank" href="https://ollama.ai/library">Library</a> for available models.`;
1174 const name = await callGenericPopup(html, POPUP_TYPE.INPUT, '', { okButton: 'Download' });
1175
1176 if (!name) {
1177 return;
1178 }
1179
1180 toastr.info('Download may take a while, please wait...', 'Working on it');
1181
1182 const response = await fetch('/api/backends/text-completions/ollama/download', {
1183 method: 'POST',
1184 headers: getRequestHeaders(),
1185 body: JSON.stringify({
1186 name: name,
1187 api_server: serverUrl,
1188 }),
1189 });
1190
1191 if (!response.ok) {
1192 throw new Error(response.statusText);
1193 }
1194
1195 // Force refresh the model list
1196 toastr.success('Download complete. Please select the model from the dropdown.');
1197 $('#api_button_textgenerationwebui').trigger('click');
1198 } catch (err) {
1199 console.error(err);
1200 toastr.error('Failed to download Ollama model. Please try again.');
1201 }
1202}
1203
1204async function downloadTabbyModel() {
1205 try {
1206 const serverUrl = textgen_settings.server_urls[textgen_types.TABBY];
1207
1208 if (online_status === 'no_connection' || !serverUrl) {
1209 toastr.info('Please connect to a TabbyAPI server first.');
1210 return;
1211 }
1212
1213 const downloadHtml = $(await renderTemplateAsync('tabbyDownloader'));
1214 const popupResult = await callGenericPopup(downloadHtml, POPUP_TYPE.CONFIRM, '', { okButton: 'Download', cancelButton: 'Cancel' });
1215
1216 // User cancelled the download
1217 if (!popupResult) {
1218 return;
1219 }
1220
1221 const repoId = downloadHtml.find('input[name="hf_repo_id"]').val().toString();
1222 if (!repoId) {
1223 toastr.error('A HuggingFace repo ID must be provided. Skipping Download.');
1224 return;
1225 }
1226
1227 if (repoId.split('/').length !== 2) {
1228 toastr.error('A HuggingFace repo ID must be formatted as Author/Name. Please try again.');
1229 return;
1230 }
1231
1232 const params = {
1233 repo_id: repoId,
1234 folder_name: downloadHtml.find('input[name="folder_name"]').val() || undefined,
1235 revision: downloadHtml.find('input[name="revision"]').val() || undefined,
1236 token: downloadHtml.find('input[name="hf_token"]').val() || undefined,
1237 };
1238
1239 for (const suffix of ['include', 'exclude']) {
1240 const patterns = downloadHtml.find(`textarea[name="tabby_download_${suffix}"]`).val().toString();
1241 if (patterns) {
1242 params[suffix] = patterns.split('\n');
1243 }
1244 }
1245
1246 // Params for the server side of ST
1247 params.api_server = serverUrl;
1248 params.api_type = textgen_settings.type;
1249
1250 toastr.info('Downloading. Check the Tabby console for progress reports.');
1251
1252 const response = await fetch('/api/backends/text-completions/tabby/download', {
1253 method: 'POST',
1254 headers: getRequestHeaders(),
1255 body: JSON.stringify(params),
1256 });
1257
1258 if (response.status === 403) {
1259 toastr.error('The provided key has invalid permissions. Please use an admin key for downloading.');
1260 return;
1261 } else if (!response.ok) {
1262 throw new Error(response.statusText);
1263 }
1264
1265 toastr.success('Download complete.');
1266 } catch (err) {
1267 console.error(err);
1268 toastr.error('Failed to download HuggingFace model in TabbyAPI. Please try again.');
1269 }
1270}
1271
1272function calculateOpenRouterCost() {
1273 if (textgen_settings.type !== textgen_types.OPENROUTER) {
1274 return;
1275 }
1276
1277 let cost = 'Unknown';
1278 const model = openRouterModels.find(x => x.id === textgen_settings.openrouter_model);
1279
1280 if (model?.pricing) {
1281 const completionCost = Number(model.pricing.completion);
1282 const promptCost = Number(model.pricing.prompt);
1283 const completionTokens = amount_gen;
1284 const promptTokens = (max_context - completionTokens);
1285 const totalCost = (completionCost * completionTokens) + (promptCost * promptTokens);
1286 if (!isNaN(totalCost)) {
1287 cost = '$' + totalCost.toFixed(3);
1288 }
1289 }
1290
1291 $('#or_prompt_cost').text(cost);
1292
1293 // Schedule an update when settings change
1294 eventSource.removeListener(event_types.SETTINGS_UPDATED, calculateOpenRouterCost);
1295 eventSource.once(event_types.SETTINGS_UPDATED, calculateOpenRouterCost);
1296}
1297
1298export function getCurrentOpenRouterModelTokenizer() {
1299 const modelId = textgen_settings.openrouter_model;
1300 const model = openRouterModels.find(x => x.id === modelId);
1301 if (modelId?.includes('jamba')) {
1302 return tokenizers.JAMBA;
1303 }
1304 switch (model?.architecture?.tokenizer) {
1305 case 'Llama2':
1306 return tokenizers.LLAMA;
1307 case 'Llama3':
1308 return tokenizers.LLAMA3;
1309 case 'Yi':
1310 return tokenizers.YI;
1311 case 'Mistral':
1312 return tokenizers.MISTRAL;
1313 case 'Gemini':
1314 return tokenizers.GEMMA;
1315 case 'Claude':
1316 return tokenizers.CLAUDE;
1317 case 'Cohere':
1318 return tokenizers.COMMAND_R;
1319 case 'Qwen':
1320 return tokenizers.QWEN2;
1321 default:
1322 return tokenizers.OPENAI;
1323 }
1324}
1325
1326export function getCurrentDreamGenModelTokenizer() {
1327 const modelId = textgen_settings.dreamgen_model;
1328 const model = dreamGenModels.find(x => x.id === modelId);
1329 if (model.id.startsWith('lucid-v1-medium') || model.id.startsWith('lucid-v1-base')) {
1330 return tokenizers.MISTRAL;
1331 } else if (model.id.startsWith('lucid-v1-extra-large') || model.id.startsWith('lucid-v1-max')) {
1332 return tokenizers.LLAMA3;
1333 } else {
1334 return tokenizers.MISTRAL;
1335 }
1336}
1337
1338export function initTextGenModels() {
1339 $('#mancer_model').on('change', onMancerModelSelect);
1340 $('#model_togetherai_select').on('change', onTogetherModelSelect);
1341 $('#model_infermaticai_select').on('change', onInfermaticAIModelSelect);
1342 $('#model_dreamgen_select').on('change', onDreamGenModelSelect);
1343 $('#ollama_model').on('change', onOllamaModelSelect);
1344 $('#openrouter_model').on('change', onOpenRouterModelSelect);
1345 $('#ollama_download_model').on('click', downloadOllamaModel);
1346 $('#vllm_model').on('change', onVllmModelSelect);
1347 $('#aphrodite_model').on('change', onAphroditeModelSelect);
1348 $('#tabby_download_model').on('click', downloadTabbyModel);
1349 $('#tabby_model').on('change', onTabbyModelSelect);
1350 $('#llamacpp_model').on('change', onLlamaCppModelSelect);
1351 $('#featherless_model').on('change', () => onFeatherlessModelSelect(String($('#featherless_model').val())));
1352
1353 const providersSelect = $('.openrouter_providers');
1354 for (const provider of OPENROUTER_PROVIDERS) {
1355 providersSelect.append($('<option>', {
1356 value: provider,
1357 text: provider,
1358 }));
1359 }
1360
1361 const nanoGptProvidersSelect = $('#nanogpt_provider');
1362 for (const provider of NANOGPT_PROVIDERS) {
1363 nanoGptProvidersSelect.append($('<option>', {
1364 value: provider.id,
1365 text: provider.label,
1366 }));
1367 }
1368
1369 if (!isMobile()) {
1370 $('#mancer_model').select2({
1371 placeholder: t`Select a model`,
1372 searchInputPlaceholder: t`Search models...`,
1373 searchInputCssClass: 'text_pole',
1374 width: '100%',
1375 templateResult: getMancerModelTemplate,
1376 });
1377 $('#model_togetherai_select').select2({
1378 placeholder: t`Select a model`,
1379 searchInputPlaceholder: t`Search models...`,
1380 searchInputCssClass: 'text_pole',
1381 width: '100%',
1382 templateResult: getTogetherModelTemplate,
1383 });
1384 $('#ollama_model').select2({
1385 placeholder: t`Select a model`,
1386 searchInputPlaceholder: t`Search models...`,
1387 searchInputCssClass: 'text_pole',
1388 width: '100%',
1389 });
1390 $('#tabby_model').select2({
1391 placeholder: t`[Currently loaded]`,
1392 searchInputPlaceholder: t`Search models...`,
1393 searchInputCssClass: 'text_pole',
1394 width: '100%',
1395 allowClear: true,
1396 });
1397 $('#llamacpp_model').select2({
1398 placeholder: t`[Currently loaded]`,
1399 searchInputPlaceholder: t`Search models...`,
1400 searchInputCssClass: 'text_pole',
1401 width: '100%',
1402 allowClear: true,
1403 });
1404 $('#model_infermaticai_select').select2({
1405 placeholder: t`Select a model`,
1406 searchInputPlaceholder: t`Search models...`,
1407 searchInputCssClass: 'text_pole',
1408 width: '100%',
1409 templateResult: getInfermaticAIModelTemplate,
1410 });
1411 $('#model_dreamgen_select').select2({
1412 placeholder: t`Select a model`,
1413 searchInputPlaceholder: t`Search models...`,
1414 searchInputCssClass: 'text_pole',
1415 width: '100%',
1416 templateResult: getDreamGenModelTemplate,
1417 });
1418 $('#openrouter_model').select2({
1419 placeholder: t`Select a model`,
1420 searchInputPlaceholder: t`Search models...`,
1421 searchInputCssClass: 'text_pole',
1422 width: '100%',
1423 templateResult: getOpenRouterModelTemplate,
1424 matcher: textValueMatcher,
1425 });
1426 $('#vllm_model').select2({
1427 placeholder: t`Select a model`,
1428 searchInputPlaceholder: t`Search models...`,
1429 searchInputCssClass: 'text_pole',
1430 width: '100%',
1431 templateResult: getVllmModelTemplate,
1432 });
1433 $('#aphrodite_model').select2({
1434 placeholder: t`Select a model`,
1435 searchInputPlaceholder: t`Search models...`,
1436 searchInputCssClass: 'text_pole',
1437 width: '100%',
1438 templateResult: getAphroditeModelTemplate,
1439 });
1440 $('.openrouter_quantizations').select2({
1441 closeOnSelect: false,
1442 placeholder: t`Select quantizations. No selection = all quantizations.`,
1443 searchInputCssClass: 'text_pole',
1444 searchInputPlaceholder: t`Search quantizations...`,
1445 width: '100%',
1446 });
1447 providersSelect.select2({
1448 sorter: data => data.sort((a, b) => a.text.localeCompare(b.text)),
1449 placeholder: t`Select providers. No selection = all providers.`,
1450 searchInputPlaceholder: t`Search providers...`,
1451 searchInputCssClass: 'text_pole',
1452 width: '100%',
1453 closeOnSelect: false,
1454 });
1455 providersSelect.on('select2:select', function (/** @type {any} */ evt) {
1456 const element = evt.params.data.element;
1457 const $element = $(element);
1458
1459 $element.detach();
1460 $(this).append($element);
1461 $(this).trigger('change');
1462 });
1463 nanoGptProvidersSelect.select2({
1464 sorter: data => data.sort((a, b) => a.text.localeCompare(b.text)),
1465 placeholder: t`Select providers. No selection = all providers.`,
1466 searchInputPlaceholder: t`Search providers...`,
1467 searchInputCssClass: 'text_pole',
1468 width: '100%',
1469 allowClear: true,
1470 });
1471 }
1472}