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 @@
23482348 <small data-i18n="Example: 127.0.0.1:5000">Example: http://127.0.0.1:5000</small>
23492349 <input id="tabby_api_url_text" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" data-server-history="tabby">
23502350 </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>
23512361 </div>
23522362 <div data-tg-type="koboldcpp">
23532363 <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>
61 \ No newline at end of file
public/scripts/textgen-models.js+72 -1
@@ -1,7 +1,9 @@
11import { isMobile } from './RossAscends-mods.js';
22import { amount_gen, callPopup, eventSource, event_types, getRequestHeaders, max_context, online_status, setGenerationParamsFromPreset } from '../script.js';
33import { textgenerationwebui_settings as textgen_settings, textgen_types } from './textgen-settings.js';
44import { tokenizers } from './tokenizers.js';
5+import { renderTemplateAsync } from './templates.js';
6+import { POPUP_TYPE, callGenericPopup } from './popup.js';
57
68let mancerModels = [];
79let togetherModels = [];
@@ -470,6 +472,74 @@ async function downloadOllamaModel() {
470472 }
471473}
472474
475+async 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+
473543function calculateOpenRouterCost() {
474544 if (textgen_settings.type !== textgen_types.OPENROUTER) {
475545 return;
@@ -538,6 +608,7 @@ jQuery(function () {
538608 $('#vllm_model').on('change', onVllmModelSelect);
539609 $('#aphrodite_model').on('change', onAphroditeModelSelect);
540610 $('#featherless_model').on('change', onFeatherlessModelSelect);
611+ $('#tabby_download_model').on('click', downloadTabbyModel);
541612
542613 const providersSelect = $('.openrouter_providers');
543614 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) {
588588 }
589589});
590590
591+const tabby = express.Router();
592+
593+tabby.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+
591636router.use('/ollama', ollama);
592637router.use('/llamacpp', llamacpp);
638+router.use('/tabby', tabby);
593639
594640module.exports = { router };