Merge pull request #2489 from bdashore3/staging Add tabbyAPI model downloader

cde328a43d21b44c7f4b28ce5effebabe6241568

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
4 files changed, +189 -1Ignore whitespace
public/index.html+10 -0
@@ -2348,6 +2348,16 @@
2348 <small data-i18n="Example: 127.0.0.1:5000">Example: http://127.0.0.1:5000</small>2348 <small data-i18n="Example: 127.0.0.1:5000">Example: http://127.0.0.1:5000</small>
2349 <input id="tabby_api_url_text" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" data-server-history="tabby">2349 <input id="tabby_api_url_text" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" data-server-history="tabby">
2350 </div>2350 </div>
2351 <div class="flex1">
2352 <h4>
2353 <span data-i18n="Tabby Model">Tabby Model</span>
2354 </h4>
2355 </h4>
2356 <div id="tabby_download_model" class="menu_button menu_button_icon">
2357 <i class="fa-solid fa-download"></i>
2358 <span data-i18n="Download">Download</span>
2359 </div>
2360 </div>
2351 </div>2361 </div>
2352 <div data-tg-type="koboldcpp">2362 <div data-tg-type="koboldcpp">
2353 <div class="flex-container flexFlowColumn">2363 <div class="flex-container flexFlowColumn">
public/scripts/templates/tabbyDownloader.html+61 -0
@@ -0,0 +1,61 @@
1<div id="tabby_downloader_popup">
2 <div>
3 <h3><strong data-i18n="">Download Model</strong>
4 <a href="https://github.com/theroyallab/async-hf-downloader" class="notes-link" target="_blank">
5 <span class="note-link-span">?</span>
6 </a>
7 </h3>
8
9 <small class="flex-container extensions_info justifyCenter">
10 Download a HuggingFace model with TabbyAPI
11 </small>
12 <small class="flex-container extensions_info justifyCenter">
13 (Requires an admin key)
14 </small>
15 <hr />
16
17 <!-- Model parameter textboxes -->
18 Repo ID
19 <div class="flex-container">
20 <input name="hf_repo_id" class="text_pole" type="text" placeholder="Ex. turboderp/Llama-3-8B-exl2" />
21 </div>
22 <div class="range-block-title justifyCenter">
23 <span data-i18n="Downloader Options">Downloader Options</span>
24 <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]Extra parameters for downloading/HuggingFace API" title="Extra parameters for downloading/HuggingFace API.&#13;If unsure, leave these blank."></div>
25 </div>
26 <div class="flex-container">
27 <div class="flex1">
28 <label for="revision">
29 <small data-i18n="Revision">Revision</small>
30 </label>
31 <input name="revision" class="text_pole" type="text" placeholder="Ex. 6.0bpw" />
32 </div>
33 <div class="flex1">
34 <label for="folder_name">
35 <small data-i18n="Folder Name">Output Folder Name</small>
36 </label>
37 <input name="folder_name" class="text_pole" type="text" />
38 </div>
39 <div class="flex1">
40 <label for="hf_token">
41 <small data-i18n="HF Token">HF Token</small>
42 </label>
43 <input name="hf_token" class="text_pole" type="text" placeholder="For gated models" />
44 </div>
45 </div>
46 <div class="range-block-title justifyCenter">
47 <span data-i18n="Include Patterns">Include Patterns</span>
48 <div class="margin5 fa-solid fa-circle-info opacity50p" data-i18n="[title]Glob patterns of files to include in the download." title="Glob patterns of files to include in the download.&#13;Separate each pattern by a newline."></div>
49 </div>
50 <div class="flex-container">
51 <textarea class="text_pole textarea_compact" name="tabby_download_include" placeholder="Ex. *.txt"></textarea>
52 </div>
53 <div class="range-block-title justifyCenter">
54 <span data-i18n="Exclude Patterns">Exclude Patterns</span>
55 <div class="margin5 fa-solid fa-circle-info opacity50p" data-i18n="[title]Glob patterns of files to exclude in the download." title="Glob patterns of files to exclude in the download.&#13;Separate each pattern by a newline."></div>
56 </div>
57 <div class="flex-container">
58 <textarea class="text_pole textarea_compact" name="tabby_download_exclude" placeholder="Ex. *.txt"></textarea>
59 </div>
60 </div>
61</div>
\ No newline at end of file61 \ No newline at end of file
public/scripts/textgen-models.js+72 -1
@@ -1,7 +1,9 @@
1import { isMobile } from './RossAscends-mods.js';1import { isMobile } from './RossAscends-mods.js';
2import { amount_gen, callPopup, eventSource, event_types, getRequestHeaders, max_context, setGenerationParamsFromPreset } from '../script.js';2import { amount_gen, callPopup, eventSource, event_types, getRequestHeaders, max_context, online_status, setGenerationParamsFromPreset } from '../script.js';
3import { textgenerationwebui_settings as textgen_settings, textgen_types } from './textgen-settings.js';3import { textgenerationwebui_settings as textgen_settings, textgen_types } from './textgen-settings.js';
4import { tokenizers } from './tokenizers.js';4import { tokenizers } from './tokenizers.js';
5import { renderTemplateAsync } from './templates.js';
6import { POPUP_TYPE, callGenericPopup } from './popup.js';
57
6let mancerModels = [];8let mancerModels = [];
7let togetherModels = [];9let togetherModels = [];
@@ -470,6 +472,74 @@ async function downloadOllamaModel() {
470 }472 }
471}473}
472474
475async function downloadTabbyModel() {
476 try {
477 const serverUrl = textgen_settings.server_urls[textgen_types.TABBY];
478
479 if (online_status === 'no_connection' || !serverUrl) {
480 toastr.info('Please connect to a TabbyAPI server first.');
481 return;
482 }
483
484 const downloadHtml = $(await renderTemplateAsync('tabbyDownloader'));
485 const popupResult = await callGenericPopup(downloadHtml, POPUP_TYPE.CONFIRM, '', { okButton: 'Download', cancelButton: 'Cancel' });
486
487 // User cancelled the download
488 if (!popupResult) {
489 return;
490 }
491
492 const repoId = downloadHtml.find('input[name="hf_repo_id"]').val().toString()
493 if (!repoId) {
494 toastr.error('A HuggingFace repo ID must be provided. Skipping Download.');
495 return;
496 }
497
498 if (repoId.split("/").length !== 2) {
499 toastr.error('A HuggingFace repo ID must be formatted as Author/Name. Please try again.');
500 return;
501 }
502
503 const params = {
504 repo_id: repoId,
505 folder_name: downloadHtml.find('input[name="folder_name"]').val() || undefined,
506 revision: downloadHtml.find('input[name="revision"]').val() || undefined,
507 token: downloadHtml.find('input[name="hf_token"]').val() || undefined,
508 }
509
510 for (const suffix of ["include", "exclude"]) {
511 const patterns = downloadHtml.find(`textarea[name="tabby_download_${suffix}"]`).val().toString();
512 if (patterns) {
513 params[suffix] = patterns.split("\n");
514 }
515 }
516
517 // Params for the server side of ST
518 params['api_server'] = serverUrl;
519 params['api_type'] = textgen_settings.type
520
521 toastr.info('Downloading. Check the Tabby console for progress reports.');
522
523 const response = await fetch('/api/backends/text-completions/tabby/download', {
524 method: 'POST',
525 headers: getRequestHeaders(),
526 body: JSON.stringify(params),
527 });
528
529 if (response.status === 403) {
530 toastr.error("The provided key has invalid permissions. Please use an admin key for downloading.");
531 return;
532 } else if (!response.ok) {
533 throw new Error(response.statusText);
534 }
535
536 toastr.success('Download complete.');
537 } catch (err) {
538 console.error(err);
539 toastr.error('Failed to download HuggingFace model in TabbyAPI. Please try again.')
540 }
541}
542
473function calculateOpenRouterCost() {543function calculateOpenRouterCost() {
474 if (textgen_settings.type !== textgen_types.OPENROUTER) {544 if (textgen_settings.type !== textgen_types.OPENROUTER) {
475 return;545 return;
@@ -538,6 +608,7 @@ jQuery(function () {
538 $('#vllm_model').on('change', onVllmModelSelect);608 $('#vllm_model').on('change', onVllmModelSelect);
539 $('#aphrodite_model').on('change', onAphroditeModelSelect);609 $('#aphrodite_model').on('change', onAphroditeModelSelect);
540 $('#featherless_model').on('change', onFeatherlessModelSelect);610 $('#featherless_model').on('change', onFeatherlessModelSelect);
611 $('#tabby_download_model').on('click', downloadTabbyModel);
541612
542 const providersSelect = $('.openrouter_providers');613 const providersSelect = $('.openrouter_providers');
543 for (const provider of OPENROUTER_PROVIDERS) {614 for (const provider of OPENROUTER_PROVIDERS) {
src/endpoints/backends/text-completions.js+46 -0
@@ -588,7 +588,53 @@ llamacpp.post('/slots', jsonParser, async function (request, response) {
588 }588 }
589});589});
590590
591const tabby = express.Router();
592
593tabby.post('/download', jsonParser, async function (request, response) {
594 try {
595 const baseUrl = String(request.body.api_server).replace(/\/$/, '');
596
597 const args = {
598 method: 'POST',
599 headers: { 'Content-Type': 'application/json' },
600 body: JSON.stringify(request.body),
601 timeout: 0,
602 }
603
604 setAdditionalHeaders(request, args, baseUrl);
605
606 // Check key permissions
607 const permissionResponse = await fetch(`${baseUrl}/v1/auth/permission`, {
608 headers: args.headers
609 });
610
611 if (permissionResponse.ok) {
612 const permissionJson = await permissionResponse.json();
613
614 if (permissionJson['permission'] !== 'admin') {
615 return response.status(403).send({ error: true });
616 }
617 } else {
618 console.log('API Permission error:', permissionResponse.status, permissionResponse.statusText);
619 return response.status(permissionResponse.status).send({ error: true });
620 }
621
622 const fetchResponse = await fetch(`${baseUrl}/v1/download`, args);
623
624 if (!fetchResponse.ok) {
625 console.log('Download error:', fetchResponse.status, fetchResponse.statusText);
626 return response.status(fetchResponse.status).send({ error: true });
627 }
628
629 return response.send({ ok: true });
630 } catch (error) {
631 console.error(error);
632 return response.status(500);
633 }
634});
635
591router.use('/ollama', ollama);636router.use('/ollama', ollama);
592router.use('/llamacpp', llamacpp);637router.use('/llamacpp', llamacpp);
638router.use('/tabby', tabby);
593639
594module.exports = { router };640module.exports = { router };