Merge pull request #2489 from bdashore3/staging Add tabbyAPI model downloader
Signed| @@ -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"> |
| @@ -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. 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. 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. 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 file | 61 | \ No newline at end of file | |
| @@ -1,7 +1,9 @@ | |||
| 1 | import { isMobile } from './RossAscends-mods.js'; | 1 | import { isMobile } from './RossAscends-mods.js'; |
| 2 | import { amount_gen, callPopup, eventSource, event_types, getRequestHeaders, max_context, setGenerationParamsFromPreset } from '../script.js'; | 2 | import { amount_gen, callPopup, eventSource, event_types, getRequestHeaders, max_context, online_status, setGenerationParamsFromPreset } from '../script.js'; |
| 3 | import { textgenerationwebui_settings as textgen_settings, textgen_types } from './textgen-settings.js'; | 3 | import { textgenerationwebui_settings as textgen_settings, textgen_types } from './textgen-settings.js'; |
| 4 | import { tokenizers } from './tokenizers.js'; | 4 | import { tokenizers } from './tokenizers.js'; |
| 5 | import { renderTemplateAsync } from './templates.js'; | ||
| 6 | import { POPUP_TYPE, callGenericPopup } from './popup.js'; | ||
| 5 | 7 | ||
| 6 | let mancerModels = []; | 8 | let mancerModels = []; |
| 7 | let togetherModels = []; | 9 | let togetherModels = []; |
| @@ -470,6 +472,74 @@ async function downloadOllamaModel() { | |||
| 470 | } | 472 | } |
| 471 | } | 473 | } |
| 472 | 474 | ||
| 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 | |||
| 473 | function calculateOpenRouterCost() { | 543 | function 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); | ||
| 541 | 612 | ||
| 542 | const providersSelect = $('.openrouter_providers'); | 613 | const providersSelect = $('.openrouter_providers'); |
| 543 | for (const provider of OPENROUTER_PROVIDERS) { | 614 | for (const provider of OPENROUTER_PROVIDERS) { |
| @@ -588,7 +588,53 @@ llamacpp.post('/slots', jsonParser, async function (request, response) { | |||
| 588 | } | 588 | } |
| 589 | }); | 589 | }); |
| 590 | 590 | ||
| 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 | |||
| 591 | router.use('/ollama', ollama); | 636 | router.use('/ollama', ollama); |
| 592 | router.use('/llamacpp', llamacpp); | 637 | router.use('/llamacpp', llamacpp); |
| 638 | router.use('/tabby', tabby); | ||
| 593 | 639 | ||
| 594 | module.exports = { router }; | 640 | module.exports = { router }; |