Merge branch 'staging' into connection-manager
| @@ -95,6 +95,9 @@ requestOverrides: [] | ||
| 95 | 95 | enableExtensions: true |
| 96 | 96 | # Automatically update extensions when a release version changes |
| 97 | 97 | enableExtensionsAutoUpdate: true |
| 98 | +# Additional model tokenizers can be downloaded on demand. | |
| 99 | +# Disabling will fallback to another locally available tokenizer. | |
| 100 | +enableDownloadableTokenizers: true | |
| 98 | 101 | # Extension settings |
| 99 | 102 | extras: |
| 100 | 103 | # Disables automatic model download from HuggingFace |
| @@ -1820,12 +1820,12 @@ | ||
| 1820 | 1820 | <span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span> |
| 1821 | 1821 | <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="claude_assistant_prefill" title="Expand the editor" data-i18n="[title]Expand the editor"></i> |
| 1822 | 1822 | </div> |
| 1823 | 1823 | <textarea id="claude_assistant_prefill" class="text_pole textarea_compact autoSetHeight" name="assistant_prefill" rows="62" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea> |
| 1824 | 1824 | <div class="flex-container alignItemsCenter"> |
| 1825 | 1825 | <span id="claude_assistant_impersonation_text" data-i18n="Assistant Impersonation Prefill">Assistant Impersonation Prefill</span> |
| 1826 | 1826 | <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="claude_assistant_impersonation" title="Expand the editor" data-i18n="[title]Expand the editor"></i> |
| 1827 | 1827 | </div> |
| 1828 | 1828 | <textarea id="claude_assistant_impersonation" class="text_pole textarea_compact autoSetHeight" name="assistant_impersonation" rows="62" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea> |
| 1829 | 1829 | </div> |
| 1830 | 1830 | <label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand"> |
| 1831 | 1831 | <input id="claude_use_sysprompt" type="checkbox" /> |
| @@ -1845,7 +1845,7 @@ | ||
| 1845 | 1845 | <div class="fa-solid fa-clock-rotate-left"></div> |
| 1846 | 1846 | </div> |
| 1847 | 1847 | </div> |
| 1848 | 1848 | <textarea id="claude_human_sysprompt_textarea" class="text_pole textarea_compact autoSetHeight" rows="42" data-i18n="[placeholder]Human message" placeholder="Human message, instruction, etc. Adds nothing when empty, i.e. requires a new prompt with the role 'user'."></textarea> |
| 1849 | 1849 | </div> |
| 1850 | 1850 | </div> |
| 1851 | 1851 | </div> |
| @@ -3289,6 +3289,8 @@ | ||
| 3289 | 3289 | <option value="12">Llama 3</option> |
| 3290 | 3290 | <option value="13">Gemma / Gemini</option> |
| 3291 | 3291 | <option value="14">Jamba</option> |
| 3292 | + <option value="15">Qwen2</option> | |
| 3293 | + <option value="16">Command-R</option> | |
| 3292 | 3294 | <option value="4">NerdStash (NovelAI Clio)</option> |
| 3293 | 3295 | <option value="5">NerdStash v2 (NovelAI Kayra)</option> |
| 3294 | 3296 | <option value="7">Mistral</option> |
| @@ -8504,22 +8504,23 @@ for (const chatCompletionSource of Object.values(chat_completion_sources)) { | ||
| 8504 | 8504 | }; |
| 8505 | 8505 | } |
| 8506 | 8506 | |
| 8507 | 8507 | async function selectContextCallback(_args, name) { |
| 8508 | 8508 | if (!name) { |
| 8509 | 8509 | return power_user.context.preset; |
| 8510 | 8510 | } |
| 8511 | 8511 | |
| 8512 | + const quiet = isTrueBoolean(args?.quiet); | |
| 8512 | 8513 | const contextNames = context_presets.map(preset => preset.name); |
| 8513 | 8514 | const fuse = new Fuse(contextNames); |
| 8514 | 8515 | const result = fuse.search(name); |
| 8515 | 8516 | |
| 8516 | 8517 | if (result.length === 0) { |
| 8517 | 8518 | !quiet && toastr.warning(`Context template "${name}" not found`); |
| 8518 | 8519 | return ''; |
| 8519 | 8520 | } |
| 8520 | 8521 | |
| 8521 | 8522 | const foundName = result[0].item; |
| 8522 | 8523 | selectContextPreset(foundName, quiet); |
| 8523 | 8524 | return foundName; |
| 8524 | 8525 | } |
| 8525 | 8526 | |
| @@ -8528,16 +8529,16 @@ async function selectInstructCallback(args, name) { | ||
| 8528 | 8529 | return power_user.instruct.preset; |
| 8529 | 8530 | } |
| 8530 | 8531 | |
| 8532 | + const quiet = isTrueBoolean(args?.quiet); | |
| 8531 | 8533 | const instructNames = instruct_presets.map(preset => preset.name); |
| 8532 | 8534 | const fuse = new Fuse(instructNames); |
| 8533 | 8535 | const result = fuse.search(name); |
| 8534 | 8536 | |
| 8535 | 8537 | if (result.length === 0) { |
| 8536 | 8538 | !quiet && toastr.warning(`Instruct template "${name}" not found`); |
| 8537 | 8539 | return ''; |
| 8538 | 8540 | } |
| 8539 | 8541 | |
| 8540 | - const quiet = isTrueBoolean(args?.quiet); | |
| 8541 | 8542 | const foundName = result[0].item; |
| 8542 | 8543 | selectInstructPreset(foundName, quiet); |
| 8543 | 8544 | return foundName; |
| @@ -9283,6 +9284,15 @@ jQuery(async function () { | ||
| 9283 | 9284 | name: 'context', |
| 9284 | 9285 | callback: selectContextCallback, |
| 9285 | 9286 | returns: 'template name', |
| 9287 | + namedArgumentList: [ | |
| 9288 | + SlashCommandNamedArgument.fromProps({ | |
| 9289 | + name: 'quiet', | |
| 9290 | + description: 'Suppress the toast message on template change', | |
| 9291 | + typeList: [ARGUMENT_TYPE.BOOLEAN], | |
| 9292 | + defaultValue: 'false', | |
| 9293 | + enumList: commonEnumProviders.boolean('trueFalse')(), | |
| 9294 | + }), | |
| 9295 | + ], | |
| 9286 | 9296 | unnamedArgumentList: [ |
| 9287 | 9297 | SlashCommandArgument.fromProps({ |
| 9288 | 9298 | description: 'context template name', |
| @@ -724,7 +724,7 @@ function onChatChanged() { | ||
| 724 | 724 | } |
| 725 | 725 | |
| 726 | 726 | async function adjustElementScrollHeight() { |
| 727 | 727 | if (CSS.supports('field-sizing', 'content') || !$('.sd_settings').is(':visible')) { |
| 728 | 728 | return; |
| 729 | 729 | } |
| 730 | 730 | |
| @@ -737,17 +737,19 @@ async function adjustElementScrollHeight() { | ||
| 737 | 737 | async function onCharacterPromptInput() { |
| 738 | 738 | const key = getCharaFilename(this_chid); |
| 739 | 739 | extension_settings.sd.character_prompts[key] = $('#sd_character_prompt').val(); |
| 740 | - await resetScrollHeight($(this)); | |
| 741 | 740 | saveSettingsDebounced(); |
| 742 | 741 | writePromptFieldsDebounced(this_chid); |
| 742 | + if (CSS.supports('field-sizing', 'content')) return; | |
| 743 | + await resetScrollHeight($(this)); | |
| 743 | 744 | } |
| 744 | 745 | |
| 745 | 746 | async function onCharacterNegativePromptInput() { |
| 746 | 747 | const key = getCharaFilename(this_chid); |
| 747 | 748 | extension_settings.sd.character_negative_prompts[key] = $('#sd_character_negative_prompt').val(); |
| 748 | - await resetScrollHeight($(this)); | |
| 749 | 749 | saveSettingsDebounced(); |
| 750 | 750 | writePromptFieldsDebounced(this_chid); |
| 751 | + if (CSS.supports('field-sizing', 'content')) return; | |
| 752 | + await resetScrollHeight($(this)); | |
| 751 | 753 | } |
| 752 | 754 | |
| 753 | 755 | function getCharacterPrefix() { |
| @@ -856,14 +858,16 @@ function onStepsInput() { | ||
| 856 | 858 | |
| 857 | 859 | async function onPromptPrefixInput() { |
| 858 | 860 | extension_settings.sd.prompt_prefix = $('#sd_prompt_prefix').val(); |
| 859 | - await resetScrollHeight($(this)); | |
| 860 | 861 | saveSettingsDebounced(); |
| 862 | + if (CSS.supports('field-sizing', 'content')) return; | |
| 863 | + await resetScrollHeight($(this)); | |
| 861 | 864 | } |
| 862 | 865 | |
| 863 | 866 | async function onNegativePromptInput() { |
| 864 | 867 | extension_settings.sd.negative_prompt = $('#sd_negative_prompt').val(); |
| 865 | - await resetScrollHeight($(this)); | |
| 866 | 868 | saveSettingsDebounced(); |
| 869 | + if (CSS.supports('field-sizing', 'content')) return; | |
| 870 | + await resetScrollHeight($(this)); | |
| 867 | 871 | } |
| 868 | 872 | |
| 869 | 873 | function onSamplerChange() { |
| @@ -3911,12 +3915,14 @@ jQuery(async () => { | ||
| 3911 | 3915 | $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange); |
| 3912 | 3916 | $('#sd_huggingface_model_id').on('input', onHFModelInput); |
| 3913 | 3917 | |
| 3918 | + if (!CSS.supports('field-sizing', 'content')) { | |
| 3914 | 3919 | $('.sd_settings .inline-drawer-toggle').on('click', function () { |
| 3915 | 3920 | initScrollHeight($('#sd_prompt_prefix')); |
| 3916 | 3921 | initScrollHeight($('#sd_negative_prompt')); |
| 3917 | 3922 | initScrollHeight($('#sd_character_prompt')); |
| 3918 | 3923 | initScrollHeight($('#sd_character_negative_prompt')); |
| 3919 | 3924 | }); |
| 3925 | + } | |
| 3920 | 3926 | |
| 3921 | 3927 | for (const [key, value] of Object.entries(resolutionOptions)) { |
| 3922 | 3928 | const option = document.createElement('option'); |
| @@ -408,16 +408,16 @@ | ||
| 408 | 408 | </div> |
| 409 | 409 | </div> |
| 410 | 410 | <label for="sd_prompt_prefix" data-i18n="Common prompt prefix">Common prompt prefix</label> |
| 411 | 411 | <textarea id="sd_prompt_prefix" class="text_pole textarea_compact autoSetHeight" data-i18n="[placeholder]sd_prompt_prefix_placeholder" placeholder="Use {prompt} to specify where the generated prompt will be inserted"></textarea> |
| 412 | 412 | <label for="sd_negative_prompt" data-i18n="Negative common prompt prefix">Negative common prompt prefix</label> |
| 413 | 413 | <textarea id="sd_negative_prompt" class="text_pole textarea_compact autoSetHeight"></textarea> |
| 414 | 414 | <div id="sd_character_prompt_block"> |
| 415 | 415 | <label for="sd_character_prompt" data-i18n="Character-specific prompt prefix">Character-specific prompt prefix</label> |
| 416 | 416 | <small data-i18n="Won't be used in groups.">Won't be used in groups.</small> |
| 417 | 417 | <textarea id="sd_character_prompt" class="text_pole textarea_compact autoSetHeight" data-i18n="[placeholder]sd_character_prompt_placeholder" placeholder="Any characteristics that describe the currently selected character. Will be added after a common prompt prefix. Example: female, green eyes, brown hair, pink shirt"></textarea> |
| 418 | 418 | <label for="sd_character_negative_prompt" data-i18n="Character-specific negative prompt prefix">Character-specific negative prompt prefix</label> |
| 419 | 419 | <small data-i18n="Won't be used in groups.">Won't be used in groups.</small> |
| 420 | 420 | <textarea id="sd_character_negative_prompt" class="text_pole textarea_compact autoSetHeight" data-i18n="[placeholder]sd_character_negative_prompt_placeholder" placeholder="Any characteristics that should not appear for the selected character. Will be added after a negative common prompt prefix. Example: jewellery, shoes, glasses"></textarea> |
| 421 | 421 | <label for="sd_character_prompt_share" class="checkbox_label flexWrap marginTop5"> |
| 422 | 422 | <input id="sd_character_prompt_share" type="checkbox" /> |
| 423 | 423 | <span data-i18n="Shareable"> |
| @@ -59,8 +59,10 @@ async function doTokenCounter() { | ||
| 59 | 59 | $('#tokenized_chunks_display').text('—'); |
| 60 | 60 | } |
| 61 | 61 | |
| 62 | + if (!CSS.supports('field-sizing', 'content')) { | |
| 62 | 63 | await resetScrollHeight($('#token_counter_textarea')); |
| 63 | 64 | await resetScrollHeight($('#token_counter_ids')); |
| 65 | + } | |
| 64 | 66 | }, debounce_timeout.relaxed); |
| 65 | 67 | dialog.find('#token_counter_textarea').on('input', () => countDebounced()); |
| 66 | 68 | |
| @@ -4,3 +4,8 @@ | ||
| 4 | 4 | padding: 2px; |
| 5 | 5 | display: inline-block; |
| 6 | 6 | } |
| 7 | + | |
| 8 | +#token_counter_textarea, | |
| 9 | +#token_counter_ids { | |
| 10 | + field-sizing: content; | |
| 11 | +} | |
| @@ -1414,9 +1414,12 @@ function toggleHiddenControls(group, generationMode = null) { | ||
| 1414 | 1414 | const isJoin = [group_generation_mode.APPEND, group_generation_mode.APPEND_DISABLED].includes(generationMode ?? group?.generation_mode); |
| 1415 | 1415 | $('#rm_group_generation_mode_join_prefix').parent().toggle(isJoin); |
| 1416 | 1416 | $('#rm_group_generation_mode_join_suffix').parent().toggle(isJoin); |
| 1417 | + | |
| 1418 | + if (!CSS.supports('field-sizing', 'content')) { | |
| 1417 | 1419 | initScrollHeight($('#rm_group_generation_mode_join_prefix')); |
| 1418 | 1420 | initScrollHeight($('#rm_group_generation_mode_join_suffix')); |
| 1419 | 1421 | } |
| 1422 | +} | |
| 1420 | 1423 | |
| 1421 | 1424 | function select_group_chats(groupId, skipAnimation) { |
| 1422 | 1425 | openGroupId = groupId; |
| @@ -130,19 +130,20 @@ function highlightDefaultPreset() { | ||
| 130 | 130 | /** |
| 131 | 131 | * Select context template if not already selected. |
| 132 | 132 | * @param {string} preset Preset name. |
| 133 | + * @param {boolean} quiet Suppress info message. | |
| 133 | 134 | */ |
| 134 | 135 | export function selectContextPreset(preset, quiet) { |
| 135 | 136 | // If context template is not already selected, select it |
| 136 | 137 | if (preset !== power_user.context.preset) { |
| 137 | 138 | $('#context_presets').val(preset).trigger('change'); |
| 138 | 139 | !quiet && toastr.info(`Context Template: preset "${preset}" auto-selected`); |
| 139 | 140 | } |
| 140 | 141 | |
| 141 | 142 | // If instruct mode is disabled, enable it, except for default context template |
| 142 | 143 | if (!power_user.instruct.enabled && preset !== power_user.default_context) { |
| 143 | 144 | power_user.instruct.enabled = true; |
| 144 | 145 | $('#instruct_enabled').prop('checked', true).trigger('change'); |
| 145 | 146 | !quiet && toastr.info('Instruct Mode enabled'); |
| 146 | 147 | } |
| 147 | 148 | |
| 148 | 149 | saveSettingsDebounced(); |
| @@ -590,6 +590,9 @@ function calculateOpenRouterCost() { | ||
| 590 | 590 | export function getCurrentOpenRouterModelTokenizer() { |
| 591 | 591 | const modelId = textgen_settings.openrouter_model; |
| 592 | 592 | const model = openRouterModels.find(x => x.id === modelId); |
| 593 | + if (modelId?.includes('jamba')) { | |
| 594 | + return tokenizers.JAMBA; | |
| 595 | + } | |
| 593 | 596 | switch (model?.architecture?.tokenizer) { |
| 594 | 597 | case 'Llama2': |
| 595 | 598 | return tokenizers.LLAMA; |
| @@ -603,6 +606,10 @@ export function getCurrentOpenRouterModelTokenizer() { | ||
| 603 | 606 | return tokenizers.GEMMA; |
| 604 | 607 | case 'Claude': |
| 605 | 608 | return tokenizers.CLAUDE; |
| 609 | + case 'Cohere': | |
| 610 | + return tokenizers.COMMAND_R; | |
| 611 | + case 'Qwen': | |
| 612 | + return tokenizers.QWEN2; | |
| 606 | 613 | default: |
| 607 | 614 | return tokenizers.OPENAI; |
| 608 | 615 | } |
| @@ -28,6 +28,8 @@ export const tokenizers = { | ||
| 28 | 28 | LLAMA3: 12, |
| 29 | 29 | GEMMA: 13, |
| 30 | 30 | JAMBA: 14, |
| 31 | + QWEN2: 15, | |
| 32 | + COMMAND_R: 16, | |
| 31 | 33 | BEST_MATCH: 99, |
| 32 | 34 | }; |
| 33 | 35 | |
| @@ -105,6 +107,16 @@ const TOKENIZER_URLS = { | ||
| 105 | 107 | decode: '/api/tokenizers/jamba/decode', |
| 106 | 108 | count: '/api/tokenizers/jamba/encode', |
| 107 | 109 | }, |
| 110 | + [tokenizers.QWEN2]: { | |
| 111 | + encode: '/api/tokenizers/qwen2/encode', | |
| 112 | + decode: '/api/tokenizers/qwen2/decode', | |
| 113 | + count: '/api/tokenizers/qwen2/encode', | |
| 114 | + }, | |
| 115 | + [tokenizers.COMMAND_R]: { | |
| 116 | + encode: '/api/tokenizers/command-r/encode', | |
| 117 | + decode: '/api/tokenizers/command-r/decode', | |
| 118 | + count: '/api/tokenizers/command-r/encode', | |
| 119 | + }, | |
| 108 | 120 | [tokenizers.API_TEXTGENERATIONWEBUI]: { |
| 109 | 121 | encode: '/api/tokenizers/remote/textgenerationwebui/encode', |
| 110 | 122 | count: '/api/tokenizers/remote/textgenerationwebui/encode', |
| @@ -293,6 +305,12 @@ export function getTokenizerBestMatch(forApi) { | ||
| 293 | 305 | if (model.includes('jamba')) { |
| 294 | 306 | return tokenizers.JAMBA; |
| 295 | 307 | } |
| 308 | + if (model.includes('command-r')) { | |
| 309 | + return tokenizers.COMMAND_R; | |
| 310 | + } | |
| 311 | + if (model.includes('qwen2')) { | |
| 312 | + return tokenizers.QWEN2; | |
| 313 | + } | |
| 296 | 314 | } |
| 297 | 315 | |
| 298 | 316 | return tokenizers.LLAMA; |
| @@ -511,6 +529,8 @@ export function getTokenizerModel() { | ||
| 511 | 529 | const yiTokenizer = 'yi'; |
| 512 | 530 | const gemmaTokenizer = 'gemma'; |
| 513 | 531 | const jambaTokenizer = 'jamba'; |
| 532 | + const qwen2Tokenizer = 'qwen2'; | |
| 533 | + const commandRTokenizer = 'command-r'; | |
| 514 | 534 | |
| 515 | 535 | // Assuming no one would use it for different models.. right? |
| 516 | 536 | if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) { |
| @@ -558,6 +578,12 @@ export function getTokenizerModel() { | ||
| 558 | 578 | else if (model?.architecture?.tokenizer === 'Gemini') { |
| 559 | 579 | return gemmaTokenizer; |
| 560 | 580 | } |
| 581 | + else if (model?.architecture?.tokenizer === 'Qwen') { | |
| 582 | + return qwen2Tokenizer; | |
| 583 | + } | |
| 584 | + else if (model?.architecture?.tokenizer === 'Cohere') { | |
| 585 | + return commandRTokenizer; | |
| 586 | + } | |
| 561 | 587 | else if (oai_settings.openrouter_model.includes('gpt-4o')) { |
| 562 | 588 | return gpt4oTokenizer; |
| 563 | 589 | } |
| @@ -581,6 +607,10 @@ export function getTokenizerModel() { | ||
| 581 | 607 | } |
| 582 | 608 | } |
| 583 | 609 | |
| 610 | + if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) { | |
| 611 | + return commandRTokenizer; | |
| 612 | + } | |
| 613 | + | |
| 584 | 614 | if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) { |
| 585 | 615 | return gemmaTokenizer; |
| 586 | 616 | } |
| @@ -182,6 +182,10 @@ body.movingUI ::-webkit-scrollbar-thumb:vertical { | ||
| 182 | 182 | min-width: 40px; |
| 183 | 183 | } |
| 184 | 184 | |
| 185 | +::-webkit-scrollbar-corner { | |
| 186 | + background-color: transparent; | |
| 187 | +} | |
| 188 | + | |
| 185 | 189 | table.responsiveTable { |
| 186 | 190 | width: 100%; |
| 187 | 191 | margin: 10px 0; |
| @@ -4675,10 +4679,6 @@ body:not(.sd) .mes_img_swipes { | ||
| 4675 | 4679 | width: 100%; |
| 4676 | 4680 | } |
| 4677 | 4681 | |
| 4678 | -.img_enlarged_holder::-webkit-scrollbar-corner { | |
| 4679 | - background-color: transparent; | |
| 4680 | -} | |
| 4681 | - | |
| 4682 | 4682 | .img_enlarged_container pre code { |
| 4683 | 4683 | position: relative; |
| 4684 | 4684 | display: block; |
| @@ -4,13 +4,12 @@ const express = require('express'); | ||
| 4 | 4 | const { SentencePieceProcessor } = require('@agnai/sentencepiece-js'); |
| 5 | 5 | const tiktoken = require('tiktoken'); |
| 6 | 6 | const { Tokenizer } = require('@agnai/web-tokenizers'); |
| 7 | 7 | const { convertClaudePrompt, convertGooglePrompt } = require('../prompt-converters'); |
| 8 | -const { readSecret, SECRET_KEYS } = require('./secrets'); | |
| 9 | 8 | const { TEXTGEN_TYPES } = require('../constants'); |
| 10 | 9 | const { jsonParser } = require('../express-common'); |
| 11 | 10 | const { setAdditionalHeaders } = require('../additional-headers'); |
| 12 | - | |
| 11 | +const { getConfigValue, isValidUrl } = require('../util'); | |
| 13 | -const API_MAKERSUITE = 'https://generativelanguage.googleapis.com'; | |
| 12 | +const writeFileAtomicSync = require('write-file-atomic').sync; | |
| 14 | 13 | |
| 15 | 14 | /** |
| 16 | 15 | * @typedef { (req: import('express').Request, res: import('express').Response) => Promise<any> } TokenizationHandler |
| @@ -53,6 +52,65 @@ const TEXT_COMPLETION_MODELS = [ | ||
| 53 | 52 | ]; |
| 54 | 53 | |
| 55 | 54 | const CHARS_PER_TOKEN = 3.35; |
| 55 | +const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true); | |
| 56 | + | |
| 57 | +/** | |
| 58 | + * Gets a path to the tokenizer model. Downloads the model if it's a URL. | |
| 59 | + * @param {string} model Model URL or path | |
| 60 | + * @param {string|undefined} fallbackModel Fallback model path\ | |
| 61 | + * @returns {Promise<string>} Path to the tokenizer model | |
| 62 | + */ | |
| 63 | +async function getPathToTokenizer(model, fallbackModel) { | |
| 64 | + if (!isValidUrl(model)) { | |
| 65 | + return model; | |
| 66 | + } | |
| 67 | + | |
| 68 | + try { | |
| 69 | + const url = new URL(model); | |
| 70 | + | |
| 71 | + if (!['https:', 'http:'].includes(url.protocol)) { | |
| 72 | + throw new Error('Invalid URL protocol'); | |
| 73 | + } | |
| 74 | + | |
| 75 | + const fileName = url.pathname.split('/').pop(); | |
| 76 | + | |
| 77 | + if (!fileName) { | |
| 78 | + throw new Error('Failed to extract the file name from the URL'); | |
| 79 | + } | |
| 80 | + | |
| 81 | + const CACHE_PATH = path.join(global.DATA_ROOT, '_cache'); | |
| 82 | + if (!fs.existsSync(CACHE_PATH)) { | |
| 83 | + fs.mkdirSync(CACHE_PATH, { recursive: true }); | |
| 84 | + } | |
| 85 | + | |
| 86 | + const cachedFile = path.join(CACHE_PATH, fileName); | |
| 87 | + if (fs.existsSync(cachedFile)) { | |
| 88 | + return cachedFile; | |
| 89 | + } | |
| 90 | + | |
| 91 | + if (!IS_DOWNLOAD_ALLOWED) { | |
| 92 | + throw new Error('Downloading tokenizers is disabled, the model is not cached'); | |
| 93 | + } | |
| 94 | + | |
| 95 | + console.log('Downloading tokenizer model:', model); | |
| 96 | + const response = await fetch(model); | |
| 97 | + if (!response.ok) { | |
| 98 | + throw new Error(`Failed to fetch the model: ${response.status} ${response.statusText}`); | |
| 99 | + } | |
| 100 | + | |
| 101 | + const arrayBuffer = await response.arrayBuffer(); | |
| 102 | + writeFileAtomicSync(cachedFile, Buffer.from(arrayBuffer)); | |
| 103 | + return cachedFile; | |
| 104 | + } catch (error) { | |
| 105 | + const getLastSegment = str => str?.split('/')?.pop() || ''; | |
| 106 | + if (fallbackModel) { | |
| 107 | + console.log(`Could not get a tokenizer from ${getLastSegment(model)}. Reason: ${error.message}. Using a fallback model: ${getLastSegment(fallbackModel)}.`); | |
| 108 | + return fallbackModel; | |
| 109 | + } | |
| 110 | + | |
| 111 | + throw new Error(`Failed to instantiate a tokenizer and fallback is not provided. Reason: ${error.message}`); | |
| 112 | + } | |
| 113 | +} | |
| 56 | 114 | |
| 57 | 115 | /** |
| 58 | 116 | * Sentencepiece tokenizer for tokenizing text. |
| @@ -66,13 +124,19 @@ class SentencePieceTokenizer { | ||
| 66 | 124 | * @type {string} Path to the tokenizer model |
| 67 | 125 | */ |
| 68 | 126 | #model; |
| 127 | + /** | |
| 128 | + * @type {string|undefined} Path to the fallback model | |
| 129 | + */ | |
| 130 | + #fallbackModel; | |
| 69 | 131 | |
| 70 | 132 | /** |
| 71 | 133 | * Creates a new Sentencepiece tokenizer. |
| 72 | 134 | * @param {string} model Path to the tokenizer model |
| 135 | + * @param {string} [fallbackModel] Path to the fallback model | |
| 73 | 136 | */ |
| 74 | 137 | constructor(model, fallbackModel) { |
| 75 | 138 | this.#model = model; |
| 139 | + this.#fallbackModel = fallbackModel; | |
| 76 | 140 | } |
| 77 | 141 | |
| 78 | 142 | /** |
| @@ -85,9 +149,10 @@ class SentencePieceTokenizer { | ||
| 85 | 149 | } |
| 86 | 150 | |
| 87 | 151 | try { |
| 152 | + const pathToModel = await getPathToTokenizer(this.#model, this.#fallbackModel); | |
| 88 | 153 | this.#instance = new SentencePieceProcessor(); |
| 89 | 154 | await this.#instance.load(this.#modelpathToModel); |
| 90 | 155 | console.log('Instantiated the tokenizer for', path.parse(this.#modelpathToModel).name); |
| 91 | 156 | return this.#instance; |
| 92 | 157 | } catch (error) { |
| 93 | 158 | console.error('Sentencepiece tokenizer failed to load: ' + this.#model, error); |
| @@ -108,13 +173,19 @@ class WebTokenizer { | ||
| 108 | 173 | * @type {string} Path to the tokenizer model |
| 109 | 174 | */ |
| 110 | 175 | #model; |
| 176 | + /** | |
| 177 | + * @type {string|undefined} Path to the fallback model | |
| 178 | + */ | |
| 179 | + #fallbackModel; | |
| 111 | 180 | |
| 112 | 181 | /** |
| 113 | 182 | * Creates a new Web tokenizer. |
| 114 | 183 | * @param {string} model Path to the tokenizer model |
| 184 | + * @param {string} [fallbackModel] Path to the fallback model | |
| 115 | 185 | */ |
| 116 | 186 | constructor(model, fallbackModel) { |
| 117 | 187 | this.#model = model; |
| 188 | + this.#fallbackModel = fallbackModel; | |
| 118 | 189 | } |
| 119 | 190 | |
| 120 | 191 | /** |
| @@ -127,9 +198,10 @@ class WebTokenizer { | ||
| 127 | 198 | } |
| 128 | 199 | |
| 129 | 200 | try { |
| 130 | 201 | const arrayBufferpathToModel = fs.readFileSyncawait getPathToTokenizer(this.#model), this.buffer#fallbackModel); |
| 202 | + const arrayBuffer = fs.readFileSync(pathToModel).buffer; | |
| 131 | 203 | this.#instance = await Tokenizer.fromJSON(arrayBuffer); |
| 132 | 204 | console.log('Instantiated the tokenizer for', path.parse(this.#modelpathToModel).name); |
| 133 | 205 | return this.#instance; |
| 134 | 206 | } catch (error) { |
| 135 | 207 | console.error('Web tokenizer failed to load: ' + this.#model, error); |
| @@ -147,6 +219,8 @@ const spp_gemma = new SentencePieceTokenizer('src/tokenizers/gemma.model'); | ||
| 147 | 219 | const spp_jamba = new SentencePieceTokenizer('src/tokenizers/jamba.model'); |
| 148 | 220 | const claude_tokenizer = new WebTokenizer('src/tokenizers/claude.json'); |
| 149 | 221 | const llama3_tokenizer = new WebTokenizer('src/tokenizers/llama3.json'); |
| 222 | +const commandTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/command-r.json', 'src/tokenizers/llama3.json'); | |
| 223 | +const qwen2Tokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/qwen2.json', 'src/tokenizers/llama3.json'); | |
| 150 | 224 | |
| 151 | 225 | const sentencepieceTokenizers = [ |
| 152 | 226 | 'llama', |
| @@ -332,6 +406,14 @@ function getTokenizerModel(requestModel) { | ||
| 332 | 406 | return 'jamba'; |
| 333 | 407 | } |
| 334 | 408 | |
| 409 | + if (requestModel.includes('qwen2')) { | |
| 410 | + return 'qwen2'; | |
| 411 | + } | |
| 412 | + | |
| 413 | + if (requestModel.includes('command-r')) { | |
| 414 | + return 'command-r'; | |
| 415 | + } | |
| 416 | + | |
| 335 | 417 | // default |
| 336 | 418 | return 'gpt-3.5-turbo'; |
| 337 | 419 | } |
| @@ -557,6 +639,8 @@ router.post('/jamba/encode', jsonParser, createSentencepieceEncodingHandler(spp_ | ||
| 557 | 639 | router.post('/gpt2/encode', jsonParser, createTiktokenEncodingHandler('gpt2')); |
| 558 | 640 | router.post('/claude/encode', jsonParser, createWebTokenizerEncodingHandler(claude_tokenizer)); |
| 559 | 641 | router.post('/llama3/encode', jsonParser, createWebTokenizerEncodingHandler(llama3_tokenizer)); |
| 642 | +router.post('/qwen2/encode', jsonParser, createWebTokenizerEncodingHandler(qwen2Tokenizer)); | |
| 643 | +router.post('/command-r/encode', jsonParser, createWebTokenizerEncodingHandler(commandTokenizer)); | |
| 560 | 644 | router.post('/llama/decode', jsonParser, createSentencepieceDecodingHandler(spp_llama)); |
| 561 | 645 | router.post('/nerdstash/decode', jsonParser, createSentencepieceDecodingHandler(spp_nerd)); |
| 562 | 646 | router.post('/nerdstash_v2/decode', jsonParser, createSentencepieceDecodingHandler(spp_nerd_v2)); |
| @@ -567,6 +651,8 @@ router.post('/jamba/decode', jsonParser, createSentencepieceDecodingHandler(spp_ | ||
| 567 | 651 | router.post('/gpt2/decode', jsonParser, createTiktokenDecodingHandler('gpt2')); |
| 568 | 652 | router.post('/claude/decode', jsonParser, createWebTokenizerDecodingHandler(claude_tokenizer)); |
| 569 | 653 | router.post('/llama3/decode', jsonParser, createWebTokenizerDecodingHandler(llama3_tokenizer)); |
| 654 | +router.post('/qwen2/decode', jsonParser, createWebTokenizerDecodingHandler(qwen2Tokenizer)); | |
| 655 | +router.post('/command-r/decode', jsonParser, createWebTokenizerDecodingHandler(commandTokenizer)); | |
| 570 | 656 | |
| 571 | 657 | router.post('/openai/encode', jsonParser, async function (req, res) { |
| 572 | 658 | try { |
| @@ -607,6 +693,16 @@ router.post('/openai/encode', jsonParser, async function (req, res) { | ||
| 607 | 693 | return handler(req, res); |
| 608 | 694 | } |
| 609 | 695 | |
| 696 | + if (queryModel.includes('qwen2')) { | |
| 697 | + const handler = createWebTokenizerEncodingHandler(qwen2Tokenizer); | |
| 698 | + return handler(req, res); | |
| 699 | + } | |
| 700 | + | |
| 701 | + if (queryModel.includes('command-r')) { | |
| 702 | + const handler = createWebTokenizerEncodingHandler(commandTokenizer); | |
| 703 | + return handler(req, res); | |
| 704 | + } | |
| 705 | + | |
| 610 | 706 | const model = getTokenizerModel(queryModel); |
| 611 | 707 | const handler = createTiktokenEncodingHandler(model); |
| 612 | 708 | return handler(req, res); |
| @@ -655,6 +751,16 @@ router.post('/openai/decode', jsonParser, async function (req, res) { | ||
| 655 | 751 | return handler(req, res); |
| 656 | 752 | } |
| 657 | 753 | |
| 754 | + if (queryModel.includes('qwen2')) { | |
| 755 | + const handler = createWebTokenizerDecodingHandler(qwen2Tokenizer); | |
| 756 | + return handler(req, res); | |
| 757 | + } | |
| 758 | + | |
| 759 | + if (queryModel.includes('command-r')) { | |
| 760 | + const handler = createWebTokenizerDecodingHandler(commandTokenizer); | |
| 761 | + return handler(req, res); | |
| 762 | + } | |
| 763 | + | |
| 658 | 764 | const model = getTokenizerModel(queryModel); |
| 659 | 765 | const handler = createTiktokenDecodingHandler(model); |
| 660 | 766 | return handler(req, res); |
| @@ -711,6 +817,20 @@ router.post('/openai/count', jsonParser, async function (req, res) { | ||
| 711 | 817 | return res.send({ 'token_count': num_tokens }); |
| 712 | 818 | } |
| 713 | 819 | |
| 820 | + if (model === 'qwen2') { | |
| 821 | + const instance = await qwen2Tokenizer.get(); | |
| 822 | + if (!instance) throw new Error('Failed to load the Qwen2 tokenizer'); | |
| 823 | + num_tokens = countWebTokenizerTokens(instance, req.body); | |
| 824 | + return res.send({ 'token_count': num_tokens }); | |
| 825 | + } | |
| 826 | + | |
| 827 | + if (model === 'command-r') { | |
| 828 | + const instance = await commandTokenizer.get(); | |
| 829 | + if (!instance) throw new Error('Failed to load the Command-R tokenizer'); | |
| 830 | + num_tokens = countWebTokenizerTokens(instance, req.body); | |
| 831 | + return res.send({ 'token_count': num_tokens }); | |
| 832 | + } | |
| 833 | + | |
| 714 | 834 | const tokensPerName = queryModel.includes('gpt-3.5-turbo-0301') ? -1 : 1; |
| 715 | 835 | const tokensPerMessage = queryModel.includes('gpt-3.5-turbo-0301') ? 4 : 3; |
| 716 | 836 | const tokensPadding = 3; |
| @@ -647,6 +647,20 @@ function getSeparator(n) { | ||
| 647 | 647 | return '='.repeat(n); |
| 648 | 648 | } |
| 649 | 649 | |
| 650 | +/** | |
| 651 | + * Checks if the string is a valid URL. | |
| 652 | + * @param {string} url String to check | |
| 653 | + * @returns {boolean} If the URL is valid | |
| 654 | + */ | |
| 655 | +function isValidUrl(url) { | |
| 656 | + try { | |
| 657 | + new URL(url); | |
| 658 | + return true; | |
| 659 | + } catch (error) { | |
| 660 | + return false; | |
| 661 | + } | |
| 662 | +} | |
| 663 | + | |
| 650 | 664 | module.exports = { |
| 651 | 665 | getConfig, |
| 652 | 666 | getConfigValue, |
| @@ -676,4 +690,5 @@ module.exports = { | ||
| 676 | 690 | makeHttp2Request, |
| 677 | 691 | removeColorFormatting, |
| 678 | 692 | getSeparator, |
| 693 | + isValidUrl, | |
| 679 | 694 | }; |