Merge branch 'staging' into sysprompt-divorce
| @@ -83,6 +83,8 @@ skipContentCheck: false | ||
| 83 | 83 | disableChatBackup: false |
| 84 | 84 | # Number of backups to keep for each chat and settings file |
| 85 | 85 | numberOfBackups: 50 |
| 86 | +# Interval in milliseconds to throttle chat backups per user | |
| 87 | +chatBackupThrottleInterval: 10000 | |
| 86 | 88 | # Allowed hosts for card downloads |
| 87 | 89 | whitelistImportDomains: |
| 88 | 90 | - localhost |
Binary file
| @@ -2896,6 +2896,7 @@ | ||
| 2896 | 2896 | <option value="mistral-large-latest">mistral-large-latest</option> |
| 2897 | 2897 | <option value="codestral-latest">codestral-latest</option> |
| 2898 | 2898 | <option value="codestral-mamba-latest">codestral-mamba-latest</option> |
| 2899 | + <option value="pixtral-latest">pixtral-latest</option> | |
| 2899 | 2900 | </optgroup> |
| 2900 | 2901 | <optgroup label="Sub-versions"> |
| 2901 | 2902 | <option value="open-mistral-nemo-2407">open-mistral-nemo-2407</option> |
| @@ -2903,11 +2904,13 @@ | ||
| 2903 | 2904 | <option value="mistral-tiny-2312">mistral-tiny-2312</option> |
| 2904 | 2905 | <option value="mistral-small-2312">mistral-small-2312</option> |
| 2905 | 2906 | <option value="mistral-small-2402">mistral-small-2402</option> |
| 2907 | + <option value="mistral-small-2409">mistral-small-2409</option> | |
| 2906 | 2908 | <option value="mistral-medium-2312">mistral-medium-2312</option> |
| 2907 | 2909 | <option value="mistral-large-2402">mistral-large-2402</option> |
| 2908 | 2910 | <option value="mistral-large-2407">mistral-large-2407</option> |
| 2909 | 2911 | <option value="codestral-2405">codestral-2405</option> |
| 2910 | 2912 | <option value="codestral-mamba-2407">codestral-mamba-2407</option> |
| 2913 | + <option value="pixtral-12b-2409">pixtral-12b-2409</option> | |
| 2911 | 2914 | </optgroup> |
| 2912 | 2915 | </select> |
| 2913 | 2916 | </div> |
| @@ -510,6 +510,7 @@ let saveCharactersPage = 0; | ||
| 510 | 510 | export const default_avatar = 'img/ai4.png'; |
| 511 | 511 | export const system_avatar = 'img/five.png'; |
| 512 | 512 | export const comment_avatar = 'img/quill.png'; |
| 513 | +export const default_user_avatar = 'img/user-default.png'; | |
| 513 | 514 | export let CLIENT_VERSION = 'SillyTavern:UNKNOWN:Cohee#1207'; // For Horde header |
| 514 | 515 | let optionsPopper = Popper.createPopper(document.getElementById('options_button'), document.getElementById('options'), { |
| 515 | 516 | placement: 'top-start', |
| @@ -3335,7 +3336,6 @@ function removeLastMessage() { | ||
| 3335 | 3336 | */ |
| 3336 | 3337 | export async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName } = {}, dryRun = false) { |
| 3337 | 3338 | console.log('Generate entered'); |
| 3338 | - await eventSource.emit(event_types.GENERATION_STARTED, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage }, dryRun); | |
| 3339 | 3339 | setGenerationProgress(0); |
| 3340 | 3340 | generation_started = new Date(); |
| 3341 | 3341 | |
| @@ -3358,6 +3358,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 3358 | 3358 | } |
| 3359 | 3359 | } |
| 3360 | 3360 | |
| 3361 | + await eventSource.emit(event_types.GENERATION_STARTED, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage }, dryRun); | |
| 3362 | + | |
| 3361 | 3363 | if (main_api == 'kobold' && kai_settings.streaming_kobold && !kai_flags.can_use_streaming) { |
| 3362 | 3364 | toastr.error('Streaming is enabled, but the version of Kobold used does not support token streaming.', undefined, { timeOut: 10000, preventDuplicates: true }); |
| 3363 | 3365 | unblockGeneration(type); |
| @@ -29,7 +29,6 @@ export function saveMetadataDebounced() { | ||
| 29 | 29 | const characterId = context.characterId; |
| 30 | 30 | |
| 31 | 31 | if (saveMetadataTimeout) { |
| 32 | - console.debug('Clearing save metadata timeout'); | |
| 33 | 32 | clearTimeout(saveMetadataTimeout); |
| 34 | 33 | } |
| 35 | 34 | |
| @@ -403,6 +403,7 @@ jQuery(async function () { | ||
| 403 | 403 | (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) || |
| 404 | 404 | (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) || |
| 405 | 405 | (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'zerooneai' && secret_state[SECRET_KEYS.ZEROONEAI]) || |
| 406 | + (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'mistral' && (secret_state[SECRET_KEYS.MISTRALAI] || extension_settings.caption.allow_reverse_proxy)) || | |
| 406 | 407 | (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) || |
| 407 | 408 | (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && (secret_state[SECRET_KEYS.CLAUDE] || extension_settings.caption.allow_reverse_proxy)) || |
| 408 | 409 | (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) || |
| @@ -23,6 +23,7 @@ | ||
| 23 | 23 | <option value="google">Google AI Studio</option> |
| 24 | 24 | <option value="koboldcpp">KoboldCpp</option> |
| 25 | 25 | <option value="llamacpp">llama.cpp</option> |
| 26 | + <option value="mistral">MistralAI</option> | |
| 26 | 27 | <option value="ollama">Ollama</option> |
| 27 | 28 | <option value="openai">OpenAI</option> |
| 28 | 29 | <option value="openrouter">OpenRouter</option> |
| @@ -33,6 +34,8 @@ | ||
| 33 | 34 | <div class="flex1 flex-container flexFlowColumn flexNoGap"> |
| 34 | 35 | <label for="caption_multimodal_model" data-i18n="Model">Model</label> |
| 35 | 36 | <select id="caption_multimodal_model" class="flex1 text_pole"> |
| 37 | + <option data-type="mistral" value="pixtral-latest">pixtral-latest</option> | |
| 38 | + <option data-type="mistral" value="pixtral-12b-2409">pixtral-12b-2409</option> | |
| 36 | 39 | <option data-type="zerooneai" value="yi-vision">yi-vision</option> |
| 37 | 40 | <option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option> |
| 38 | 41 | <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option> |
| @@ -96,7 +99,7 @@ | ||
| 96 | 99 | <div data-type="ollama"> |
| 97 | 100 | The model must be downloaded first! Do it with the <code>ollama pull</code> command or <a href="#" id="caption_ollama_pull">click here</a>. |
| 98 | 101 | </div> |
| 99 | 102 | <label data-type="openai,anthropic,google,mistral" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid."> |
| 100 | 103 | <input id="caption_allow_reverse_proxy" type="checkbox" class="checkbox"> |
| 101 | 104 | <span data-i18n="Allow reverse proxy">Allow reverse proxy</span> |
| 102 | 105 | </label> |
| @@ -13,7 +13,7 @@ import { createThumbnail, isValidUrl } from '../utils.js'; | ||
| 13 | 13 | */ |
| 14 | 14 | export async function getMultimodalCaption(base64Img, prompt) { |
| 15 | 15 | const useReverseProxy = |
| 16 | 16 | (['openai', 'anthropic', 'google', 'mistral'].includes(extension_settings.caption.multimodal_api)) |
| 17 | 17 | && extension_settings.caption.allow_reverse_proxy |
| 18 | 18 | && oai_settings.reverse_proxy |
| 19 | 19 | && isValidUrl(oai_settings.reverse_proxy); |
| @@ -36,7 +36,7 @@ export async function getMultimodalCaption(base64Img, prompt) { | ||
| 36 | 36 | const isVllm = extension_settings.caption.multimodal_api === 'vllm'; |
| 37 | 37 | const base64Bytes = base64Img.length * 0.75; |
| 38 | 38 | const compressionLimit = 2 * 1024 * 1024; |
| 39 | 39 | if ((['google', 'openrouter', 'mistral'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) { |
| 40 | 40 | const maxSide = 1024; |
| 41 | 41 | base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg'); |
| 42 | 42 | } |
| @@ -139,6 +139,10 @@ function throwIfInvalidModel(useReverseProxy) { | ||
| 139 | 139 | throw new Error('Google AI Studio API key is not set.'); |
| 140 | 140 | } |
| 141 | 141 | |
| 142 | + if (extension_settings.caption.multi_modal_api === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] && !useReverseProxy) { | |
| 143 | + throw new Error('Mistral AI API key is not set.'); | |
| 144 | + } | |
| 145 | + | |
| 142 | 146 | if (extension_settings.caption.multimodal_api === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) { |
| 143 | 147 | throw new Error('Ollama server URL is not set.'); |
| 144 | 148 | } |
| @@ -1,10 +1,10 @@ | ||
| 1 | 1 | import { |
| 2 | 2 | saveSettingsDebouncedamount_gen, |
| 3 | 3 | callPopup, |
| 4 | - setGenerationProgress, | |
| 5 | 4 | getRequestHeaders, |
| 6 | 5 | max_context, |
| 7 | 6 | amount_gensaveSettingsDebounced, |
| 7 | + setGenerationProgress, | |
| 8 | 8 | } from '../script.js'; |
| 9 | 9 | import { SECRET_KEYS, writeSecret } from './secrets.js'; |
| 10 | 10 | import { delay } from './utils.js'; |
| @@ -45,8 +45,7 @@ async function getWorkers(force) { | ||
| 45 | 45 | headers: getRequestHeaders(), |
| 46 | 46 | body: JSON.stringify({ force }), |
| 47 | 47 | }); |
| 48 | 48 | const data =return await response.json(); |
| 49 | - return data; | |
| 50 | 49 | } |
| 51 | 50 | |
| 52 | 51 | /** |
| @@ -61,9 +60,11 @@ async function getModels(force) { | ||
| 61 | 60 | body: JSON.stringify({ force }), |
| 62 | 61 | }); |
| 63 | 62 | const data = await response.json(); |
| 63 | + console.log('getModels', data); | |
| 64 | 64 | return data; |
| 65 | 65 | } |
| 66 | 66 | |
| 67 | + | |
| 67 | 68 | /** |
| 68 | 69 | * Gets the status of a Horde task. |
| 69 | 70 | * @param {string} taskId Task ID |
| @@ -80,8 +81,7 @@ async function getTaskStatus(taskId) { | ||
| 80 | 81 | throw new Error(`Failed to get task status: ${response.statusText}`); |
| 81 | 82 | } |
| 82 | 83 | |
| 83 | 84 | const data =return await response.json(); |
| 84 | - return data; | |
| 85 | 85 | } |
| 86 | 86 | |
| 87 | 87 | /** |
| @@ -148,7 +148,7 @@ async function adjustHordeGenerationParams(max_context_length, max_length) { | ||
| 148 | 148 | |
| 149 | 149 | for (const model of selectedModels) { |
| 150 | 150 | for (const worker of workers) { |
| 151 | 151 | if (model.cluster === worker.cluster && worker.models.includes(model.name)) { |
| 152 | 152 | // Skip workers that are not trusted if the option is enabled |
| 153 | 153 | if (horde_settings.trusted_workers_only && !worker.trusted) { |
| 154 | 154 | continue; |
| @@ -250,12 +250,10 @@ async function generateHorde(prompt, params, signal, reportProgress) { | ||
| 250 | 250 | console.log(generatedText); |
| 251 | 251 | console.log(`Generated by Horde Worker: ${WorkerName} [${WorkerModel}]`); |
| 252 | 252 | return { text: generatedText, workerName: `Generated by Horde worker: ${WorkerName} [${WorkerModel}]` }; |
| 253 | - } | |
| 253 | + } else if (!queue_position_first) { | |
| 254 | - else if (!queue_position_first) { | |
| 255 | 254 | queue_position_first = statusCheckJson.queue_position; |
| 256 | 255 | reportProgress && setGenerationProgress(0); |
| 257 | - } | |
| 256 | + } else if (statusCheckJson.queue_position >= 0) { | |
| 258 | - else if (statusCheckJson.queue_position >= 0) { | |
| 259 | 257 | let queue_position = statusCheckJson.queue_position; |
| 260 | 258 | const progress = Math.round(100 - (queue_position / queue_position_first * 100)); |
| 261 | 259 | reportProgress && setGenerationProgress(progress); |
| @@ -268,17 +266,24 @@ async function generateHorde(prompt, params, signal, reportProgress) { | ||
| 268 | 266 | throw new Error('Horde timeout'); |
| 269 | 267 | } |
| 270 | 268 | |
| 269 | + | |
| 271 | 270 | /** |
| 272 | 271 | * Displays the available models in the Horde model selection dropdown. |
| 273 | 272 | * @param {boolean} force Force refresh of the models |
| 274 | 273 | */ |
| 275 | 274 | async function getHordeModels(force) { |
| 275 | + const sortByPerformance = (a, b) => b.performance - a.performance; | |
| 276 | + const sortByWhitelisted = (a, b) => b.is_whitelisted - a.is_whitelisted; | |
| 277 | + const sortByPopular = (a, b) => b.tags?.includes('popular') - a.tags?.includes('popular'); | |
| 278 | + | |
| 276 | 279 | $('#horde_model').empty(); |
| 277 | 280 | models = (await getModels(force)).sort((a, b) => b.performance - a.performance);{ |
| 281 | + return sortByWhitelisted(a, b) || sortByPopular(a, b) || sortByPerformance(a, b); | |
| 282 | + }); | |
| 278 | 283 | for (const model of models) { |
| 279 | 284 | const option = document.createElement('option'); |
| 280 | 285 | option.value = model.name; |
| 281 | - option.innerText = `${model.name} (ETA: ${model.eta}s, Speed: ${model.performance}, Queue: ${model.queued}, Workers: ${model.count})`; | |
| 286 | + option.innerText = hordeModelTextString(model); | |
| 282 | 287 | option.selected = horde_settings.models.includes(model.name); |
| 283 | 288 | $('#horde_model').append(option); |
| 284 | 289 | } |
| @@ -323,8 +328,66 @@ async function showKudos() { | ||
| 323 | 328 | toastr.info(`Kudos: ${data.kudos}`, data.username); |
| 324 | 329 | } |
| 325 | 330 | |
| 331 | +function hordeModelTextString(model) { | |
| 332 | + const q = hordeModelQueueStateString(model); | |
| 333 | + return `${model.name} (${q})`; | |
| 334 | +} | |
| 335 | + | |
| 336 | +function hordeModelQueueStateString(model) { | |
| 337 | + return `ETA: ${model.eta}s, Speed: ${model.performance}, Queue: ${model.queued}, Workers: ${model.count}`; | |
| 338 | +} | |
| 339 | + | |
| 340 | +function getHordeModelTemplate(option) { | |
| 341 | + const model = models.find(x => x.name === option?.element?.value); | |
| 342 | + | |
| 343 | + if (!option.id || !model) { | |
| 344 | + console.debug('No model found for option', option, option?.element?.value); | |
| 345 | + console.debug('Models', models); | |
| 346 | + return option.text; | |
| 347 | + } | |
| 348 | + | |
| 349 | + const strip = html => { | |
| 350 | + const tmp = document.createElement('DIV'); | |
| 351 | + tmp.innerHTML = html || ''; | |
| 352 | + return tmp.textContent || tmp.innerText || ''; | |
| 353 | + }; | |
| 354 | + | |
| 355 | + // how much do we trust the metadata from the models repo? about this much | |
| 356 | + const displayName = strip(model.display_name || model.name).replace(/.*\//g, ''); | |
| 357 | + const description = strip(model.description); | |
| 358 | + const tags = model.tags ? model.tags.map(strip) : []; | |
| 359 | + const url = strip(model.url); | |
| 360 | + const style = strip(model.style); | |
| 361 | + | |
| 362 | + const workerInfo = hordeModelQueueStateString(model); | |
| 363 | + const isPopular = model.tags?.includes('popular'); | |
| 364 | + const descriptionDiv = description ? `<div class="horde-model-description">${description}</div>` : ''; | |
| 365 | + const tagSpans = tags.length > 0 && | |
| 366 | + `${tags.map(tag => `<span class="tag tag_name">${tag}</span>`).join('')}</span>` || ''; | |
| 367 | + | |
| 368 | + const modelDetailsLink = url && `<a href="${url}" target="_blank" rel="noopener noreferrer" class="model-details-link fa-solid fa-circle-question"> </a>`; | |
| 369 | + const capitalize = s => s ? s[0].toUpperCase() + s.slice(1) : ''; | |
| 370 | + const innerContent = [ | |
| 371 | + `<strong>${displayName}</strong> ${modelDetailsLink}`, | |
| 372 | + style ? `${capitalize(style)}` : '', | |
| 373 | + tagSpans ? `<span class="tags tags_inline inline-flex margin-r2">${tagSpans}</span>` : '', | |
| 374 | + ].filter(Boolean).join(' | '); | |
| 375 | + | |
| 376 | + return $((` | |
| 377 | + <div class="flex-container flexFlowColumn"> | |
| 378 | + <div> | |
| 379 | + ${isPopular ? '<span class="fa-fw fa-solid fa-star" title="Popular"></span>' : ''} | |
| 380 | + ${innerContent} | |
| 381 | + </div> | |
| 382 | + ${descriptionDiv} | |
| 383 | + <div><small>${workerInfo}</small></div> | |
| 384 | + </div> | |
| 385 | + `)); | |
| 386 | +} | |
| 387 | + | |
| 326 | 388 | jQuery(function () { |
| 327 | 389 | $('#horde_model').on('mousedown change', async function (e) { |
| 390 | + console.log('Horde model change', e); | |
| 328 | 391 | horde_settings.models = $('#horde_model').val(); |
| 329 | 392 | console.log('Updated Horde models', horde_settings.models); |
| 330 | 393 | |
| @@ -374,10 +437,7 @@ jQuery(function () { | ||
| 374 | 437 | // Customize the pillbox text by shortening the full text |
| 375 | 438 | return data.id; |
| 376 | 439 | }, |
| 377 | - templateResult: function (data) { | |
| 440 | + templateResult: getHordeModelTemplate, | |
| 378 | - // Return the full text for the dropdown | |
| 379 | - return data.text; | |
| 380 | - }, | |
| 381 | 441 | }); |
| 382 | 442 | } |
| 383 | 443 | }); |
| @@ -2490,7 +2490,7 @@ class Message { | ||
| 2490 | 2490 | * @returns {Promise<string>} Compressed image as a Data URL. |
| 2491 | 2491 | */ |
| 2492 | 2492 | async compressImage(image) { |
| 2493 | 2493 | if ([chat_completion_sources.OPENROUTER, chat_completion_sources.MAKERSUITE, chat_completion_sources.MISTRALAI].includes(oai_settings.chat_completion_source)) { |
| 2494 | 2494 | const sizeThreshold = 2 * 1024 * 1024; |
| 2495 | 2495 | const dataSize = image.length * 0.75; |
| 2496 | 2496 | const maxSide = 1024; |
| @@ -4221,6 +4221,8 @@ async function onModelChange() { | ||
| 4221 | 4221 | $('#openai_max_context').attr('max', max_128k); |
| 4222 | 4222 | } else if (oai_settings.mistralai_model.includes('mixtral-8x22b')) { |
| 4223 | 4223 | $('#openai_max_context').attr('max', max_64k); |
| 4224 | + } else if (oai_settings.mistralai_model.includes('pixtral')) { | |
| 4225 | + $('#openai_max_context').attr('max', max_128k); | |
| 4224 | 4226 | } else { |
| 4225 | 4227 | $('#openai_max_context').attr('max', max_32k); |
| 4226 | 4228 | } |
| @@ -4770,6 +4772,8 @@ export function isImageInliningSupported() { | ||
| 4770 | 4772 | 'gpt-4o-mini', |
| 4771 | 4773 | 'chatgpt-4o-latest', |
| 4772 | 4774 | 'yi-vision', |
| 4775 | + 'pixtral-latest', | |
| 4776 | + 'pixtral-12b-2409', | |
| 4773 | 4777 | ]; |
| 4774 | 4778 | |
| 4775 | 4779 | switch (oai_settings.chat_completion_source) { |
| @@ -4785,6 +4789,8 @@ export function isImageInliningSupported() { | ||
| 4785 | 4789 | return true; |
| 4786 | 4790 | case chat_completion_sources.ZEROONEAI: |
| 4787 | 4791 | return visionSupportedModels.some(model => oai_settings.zerooneai_model.includes(model)); |
| 4792 | + case chat_completion_sources.MISTRALAI: | |
| 4793 | + return visionSupportedModels.some(model => oai_settings.mistralai_model.includes(model)); | |
| 4788 | 4794 | default: |
| 4789 | 4795 | return false; |
| 4790 | 4796 | } |
| @@ -2,7 +2,7 @@ import { | ||
| 2 | 2 | characters, |
| 3 | 3 | chat, |
| 4 | 4 | chat_metadata, |
| 5 | 5 | default_avatardefault_user_avatar, |
| 6 | 6 | eventSource, |
| 7 | 7 | event_types, |
| 8 | 8 | getRequestHeaders, |
| @@ -357,7 +357,7 @@ async function createDummyPersona() { | ||
| 357 | 357 | // Date + name (only ASCII) to make it unique |
| 358 | 358 | const avatarId = `${Date.now()}-${personaName.replace(/[^a-zA-Z0-9]/g, '')}.png`; |
| 359 | 359 | initPersona(avatarId, personaName, ''); |
| 360 | 360 | await uploadUserAvatar(default_avatardefault_user_avatar, avatarId); |
| 361 | 361 | } |
| 362 | 362 | |
| 363 | 363 | /** |
| @@ -944,7 +944,7 @@ async function onPersonasRestoreInput(e) { | ||
| 944 | 944 | // If the avatar is missing, upload it |
| 945 | 945 | if (!avatarsList.includes(key)) { |
| 946 | 946 | warnings.push(`Persona image "${key}" (${value}) is missing, uploading default avatar`); |
| 947 | 947 | await uploadUserAvatar(default_avatardefault_user_avatar, key); |
| 948 | 948 | } |
| 949 | 949 | } |
| 950 | 950 | |
| @@ -15,7 +15,7 @@ import { BIAS_CACHE, createNewLogitBiasEntry, displayLogitBias, getLogitBiasList | ||
| 15 | 15 | import { power_user, registerDebugFunction } from './power-user.js'; |
| 16 | 16 | import { getEventSourceStream } from './sse-stream.js'; |
| 17 | 17 | import { getCurrentDreamGenModelTokenizer, getCurrentOpenRouterModelTokenizer } from './textgen-models.js'; |
| 18 | 18 | import { SENTENCEPIECE_TOKENIZERSENCODE_TOKENIZERS, TEXTGEN_TOKENIZERS, getTextTokens, tokenizers } from './tokenizers.js'; |
| 19 | 19 | import { getSortableDelay, onlyUnique } from './utils.js'; |
| 20 | 20 | |
| 21 | 21 | export { |
| @@ -353,7 +353,7 @@ function getTokenizerForTokenIds() { | ||
| 353 | 353 | return tokenizers.API_CURRENT; |
| 354 | 354 | } |
| 355 | 355 | |
| 356 | 356 | if (SENTENCEPIECE_TOKENIZERSENCODE_TOKENIZERS.includes(power_user.tokenizer)) { |
| 357 | 357 | return power_user.tokenizer; |
| 358 | 358 | } |
| 359 | 359 | |
| @@ -33,18 +33,22 @@ export const tokenizers = { | ||
| 33 | 33 | BEST_MATCH: 99, |
| 34 | 34 | }; |
| 35 | 35 | |
| 36 | -export const SENTENCEPIECE_TOKENIZERS = [ | |
| 36 | +// A list of local tokenizers that support encoding and decoding token ids. | |
| 37 | +export const ENCODE_TOKENIZERS = [ | |
| 37 | 38 | tokenizers.LLAMA, |
| 38 | 39 | tokenizers.MISTRAL, |
| 39 | 40 | tokenizers.YI, |
| 40 | 41 | tokenizers.LLAMA3, |
| 41 | 42 | tokenizers.GEMMA, |
| 42 | 43 | tokenizers.JAMBA, |
| 44 | + tokenizers.QWEN2, | |
| 45 | + tokenizers.COMMAND_R, | |
| 43 | 46 | // uncomment when NovelAI releases Kayra and Clio weights, lol |
| 44 | 47 | //tokenizers.NERD, |
| 45 | 48 | //tokenizers.NERD2, |
| 46 | 49 | ]; |
| 47 | 50 | |
| 51 | +// A list of Text Completion sources that support remote tokenization. | |
| 48 | 52 | export const TEXTGEN_TOKENIZERS = [OOBA, TABBY, KOBOLDCPP, LLAMACPP, VLLM, APHRODITE]; |
| 49 | 53 | |
| 50 | 54 | const TOKENIZER_URLS = { |
| @@ -1908,13 +1908,13 @@ export function select2ChoiceClickSubscribe(control, action, { buttonStyle = fal | ||
| 1908 | 1908 | * @returns {string} The html representation of the highlighted regex |
| 1909 | 1909 | */ |
| 1910 | 1910 | export function highlightRegex(regexStr) { |
| 1911 | 1911 | // Function to escape HTML special characters for safety or readability |
| 1912 | 1912 | const escapeHtmlescape = (str) => str.replace(/[&<>"'\x01]/g, match => ({ |
| 1913 | 1913 | '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''', '\x01': '\\x01', |
| 1914 | 1914 | })[match]); |
| 1915 | 1915 | |
| 1916 | 1916 | // Replace special characters with their HTML-escaped forms |
| 1917 | 1917 | regexStr = escapeHtmlescape(regexStr); |
| 1918 | 1918 | |
| 1919 | 1919 | // Patterns that we want to highlight only if they are not escaped |
| 1920 | 1920 | function getPatterns() { |
| @@ -3518,6 +3518,14 @@ grammarly-extension { | ||
| 3518 | 3518 | column-gap: 20px; |
| 3519 | 3519 | } |
| 3520 | 3520 | |
| 3521 | +.horde-model-description { | |
| 3522 | + -webkit-line-clamp: 3; | |
| 3523 | + line-clamp: 3; | |
| 3524 | + font-size: 0.9em; | |
| 3525 | + overflow: hidden; | |
| 3526 | + text-overflow: ellipsis; | |
| 3527 | +} | |
| 3528 | + | |
| 3521 | 3529 | .drag-handle { |
| 3522 | 3530 | cursor: grab; |
| 3523 | 3531 | /* Make the drag handle not selectable in most browsers */ |
| @@ -5,7 +5,6 @@ const PUBLIC_DIRECTORIES = { | ||
| 5 | 5 | extensions: 'public/scripts/extensions', |
| 6 | 6 | }; |
| 7 | 7 | |
| 8 | -const DEFAULT_AVATAR = '/img/ai4.png'; | |
| 9 | 8 | const SETTINGS_FILE = 'settings.json'; |
| 10 | 9 | |
| 11 | 10 | /** |
| @@ -423,7 +422,6 @@ const VLLM_KEYS = [ | ||
| 423 | 422 | |
| 424 | 423 | module.exports = { |
| 425 | 424 | DEFAULT_USER, |
| 426 | - DEFAULT_AVATAR, | |
| 427 | 425 | SETTINGS_FILE, |
| 428 | 426 | PUBLIC_DIRECTORIES, |
| 429 | 427 | USER_DIRECTORY_TEMPLATE, |
| @@ -726,13 +726,12 @@ router.post('/create', urlencodedParser, async function (request, response) { | ||
| 726 | 726 | const char = JSON.stringify(charaFormatData(request.body, request.user.directories)); |
| 727 | 727 | const internalName = getPngName(request.body.ch_name, request.user.directories); |
| 728 | 728 | const avatarName = `${internalName}.png`; |
| 729 | - const defaultAvatar = './public/img/ai4.png'; | |
| 730 | 729 | const chatsPath = path.join(request.user.directories.chats, internalName); |
| 731 | 730 | |
| 732 | 731 | if (!fs.existsSync(chatsPath)) fs.mkdirSync(chatsPath); |
| 733 | 732 | |
| 734 | 733 | if (!request.file) { |
| 735 | 734 | await writeCharacterData(defaultAvatardefaultAvatarPath, char, internalName, request); |
| 736 | 735 | return response.send(avatarName); |
| 737 | 736 | } else { |
| 738 | 737 | const crop = tryParse(request.query.crop); |
| @@ -4,6 +4,7 @@ const readline = require('readline'); | ||
| 4 | 4 | const express = require('express'); |
| 5 | 5 | const sanitize = require('sanitize-filename'); |
| 6 | 6 | const writeFileAtomicSync = require('write-file-atomic').sync; |
| 7 | +const _ = require('lodash'); | |
| 7 | 8 | |
| 8 | 9 | const { jsonParser, urlencodedParser } = require('../express-common'); |
| 9 | 10 | const { getConfigValue, humanizedISO8601DateTime, tryParse, generateTimestamp, removeOldBackups } = require('../util'); |
| @@ -34,6 +35,27 @@ function backupChat(directory, name, chat) { | ||
| 34 | 35 | } |
| 35 | 36 | } |
| 36 | 37 | |
| 38 | +const backupFunctions = new Map(); | |
| 39 | + | |
| 40 | +/** | |
| 41 | + * Gets a backup function for a user. | |
| 42 | + * @param {string} handle User handle | |
| 43 | + * @returns {function(string, string, string): void} Backup function | |
| 44 | + */ | |
| 45 | +function getBackupFunction(handle) { | |
| 46 | + const throttleInterval = getConfigValue('chatBackupThrottleInterval', 10_000); | |
| 47 | + if (!backupFunctions.has(handle)) { | |
| 48 | + backupFunctions.set(handle, _.throttle(backupChat, throttleInterval, { leading: true, trailing: true })); | |
| 49 | + } | |
| 50 | + return backupFunctions.get(handle); | |
| 51 | +} | |
| 52 | + | |
| 53 | +process.on('exit', () => { | |
| 54 | + for (const func of backupFunctions.values()) { | |
| 55 | + func.flush(); | |
| 56 | + } | |
| 57 | +}); | |
| 58 | + | |
| 37 | 59 | /** |
| 38 | 60 | * Imports a chat from Ooba's format. |
| 39 | 61 | * @param {string} userName User name |
| @@ -147,7 +169,7 @@ router.post('/save', jsonParser, function (request, response) { | ||
| 147 | 169 | const fileName = `${String(request.body.file_name)}.jsonl`; |
| 148 | 170 | const filePath = path.join(request.user.directories.chats, directoryName, sanitize(fileName)); |
| 149 | 171 | writeFileAtomicSync(filePath, jsonlData, 'utf8'); |
| 150 | 172 | backupChatgetBackupFunction(request.user.profile.handle)(request.user.directories.backups, directoryName, jsonlData); |
| 151 | 173 | return response.send({ result: 'ok' }); |
| 152 | 174 | } catch (error) { |
| 153 | 175 | response.send(error); |
| @@ -446,7 +468,7 @@ router.post('/group/save', jsonParser, (request, response) => { | ||
| 446 | 468 | let chat_data = request.body.chat; |
| 447 | 469 | let jsonlData = chat_data.map(JSON.stringify).join('\n'); |
| 448 | 470 | writeFileAtomicSync(pathToFile, jsonlData, 'utf8'); |
| 449 | 471 | backupChatgetBackupFunction(request.user.profile.handle)(request.user.directories.backups, String(id), jsonlData); |
| 450 | 472 | return response.send({ ok: true }); |
| 451 | 473 | }); |
| 452 | 474 | |
| @@ -6,6 +6,7 @@ const { readSecret, SECRET_KEYS } = require('./secrets'); | ||
| 6 | 6 | const { jsonParser } = require('../express-common'); |
| 7 | 7 | |
| 8 | 8 | const ANONYMOUS_KEY = '0000000000'; |
| 9 | +const HORDE_TEXT_MODEL_METADATA_URL = 'https://raw.githubusercontent.com/db0/AI-Horde-text-model-reference/main/db.json'; | |
| 9 | 10 | const cache = new Cache(60 * 1000); |
| 10 | 11 | const router = express.Router(); |
| 11 | 12 | |
| @@ -23,10 +24,9 @@ async function getClientAgent() { | ||
| 23 | 24 | * @returns {Promise<AIHorde>} AIHorde client |
| 24 | 25 | */ |
| 25 | 26 | async function getHordeClient() { |
| 26 | 27 | const ai_horde =return new AIHorde({ |
| 27 | 28 | client_agent: await getClientAgent(), |
| 28 | 29 | }); |
| 29 | - return ai_horde; | |
| 30 | 30 | } |
| 31 | 31 | |
| 32 | 32 | /** |
| @@ -79,10 +79,24 @@ router.post('/text-workers', jsonParser, async (request, response) => { | ||
| 79 | 79 | } |
| 80 | 80 | }); |
| 81 | 81 | |
| 82 | +async function getHordeTextModelMetadata() { | |
| 83 | + const response = await fetch(HORDE_TEXT_MODEL_METADATA_URL); | |
| 84 | + return await response.json(); | |
| 85 | +} | |
| 86 | + | |
| 87 | +async function mergeModelsAndMetadata(models, metadata) { | |
| 88 | + return models.map(model => { | |
| 89 | + const metadataModel = metadata[model.name]; | |
| 90 | + if (!metadataModel) { | |
| 91 | + return { ...model, is_whitelisted: false }; | |
| 92 | + } | |
| 93 | + return { ...model, ...metadataModel, is_whitelisted: true }; | |
| 94 | + }); | |
| 95 | +} | |
| 96 | + | |
| 82 | 97 | router.post('/text-models', jsonParser, async (request, response) => { |
| 83 | 98 | try { |
| 84 | 99 | const cachedModels = cache.get('models'); |
| 85 | - | |
| 86 | 100 | if (cachedModels && !request.body.force) { |
| 87 | 101 | return response.send(cachedModels); |
| 88 | 102 | } |
| @@ -94,7 +108,17 @@ router.post('/text-models', jsonParser, async (request, response) => { | ||
| 94 | 108 | }, |
| 95 | 109 | }); |
| 96 | 110 | |
| 97 | 111 | constlet data = await fetchResult.json(); |
| 112 | + | |
| 113 | + // attempt to fetch and merge models metadata | |
| 114 | + try { | |
| 115 | + const metadata = await getHordeTextModelMetadata(); | |
| 116 | + data = await mergeModelsAndMetadata(data, metadata); | |
| 117 | + } | |
| 118 | + catch (error) { | |
| 119 | + console.error('Failed to fetch metadata:', error); | |
| 120 | + } | |
| 121 | + | |
| 98 | 122 | cache.set('models', data); |
| 99 | 123 | return response.send(data); |
| 100 | 124 | } catch (error) { |
| @@ -310,6 +334,7 @@ router.post('/generate-image', jsonParser, async (request, response) => { | ||
| 310 | 334 | console.log('Stable Horde request:', request.body); |
| 311 | 335 | |
| 312 | 336 | const ai_horde = await getHordeClient(); |
| 337 | + // noinspection JSCheckFunctionSignatures -- see @ts-ignore - use_gfpgan | |
| 313 | 338 | const generation = await ai_horde.postAsyncImageGenerate( |
| 314 | 339 | { |
| 315 | 340 | prompt: `${request.body.prompt} ### ${request.body.negative_prompt}`, |
| @@ -51,6 +51,10 @@ router.post('/caption-image', jsonParser, async (request, response) => { | ||
| 51 | 51 | key = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI); |
| 52 | 52 | } |
| 53 | 53 | |
| 54 | + if (request.body.api === 'mistral') { | |
| 55 | + key = readSecret(request.user.directories, SECRET_KEYS.MISTRALAI); | |
| 56 | + } | |
| 57 | + | |
| 54 | 58 | if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp', 'vllm'].includes(request.body.api) === false) { |
| 55 | 59 | console.log('No key found for API', request.body.api); |
| 56 | 60 | return response.sendStatus(400); |
| @@ -107,6 +111,10 @@ router.post('/caption-image', jsonParser, async (request, response) => { | ||
| 107 | 111 | apiUrl = 'https://api.01.ai/v1/chat/completions'; |
| 108 | 112 | } |
| 109 | 113 | |
| 114 | + if (request.body.api === 'mistral') { | |
| 115 | + apiUrl = 'https://api.mistral.ai/v1/chat/completions'; | |
| 116 | + } | |
| 117 | + | |
| 110 | 118 | if (request.body.api === 'ooba') { |
| 111 | 119 | apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`; |
| 112 | 120 | const imgMessage = body.messages.pop(); |
| @@ -11,7 +11,7 @@ const mime = require('mime-types'); | ||
| 11 | 11 | const archiver = require('archiver'); |
| 12 | 12 | const writeFileAtomicSync = require('write-file-atomic').sync; |
| 13 | 13 | |
| 14 | 14 | const { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, DEFAULT_AVATAR, SETTINGS_FILE } = require('./constants'); |
| 15 | 15 | const { getConfigValue, color, delay, setConfigValue, generateTimestamp } = require('./util'); |
| 16 | 16 | const { readSecret, writeSecret } = require('./endpoints/secrets'); |
| 17 | 17 | |
| @@ -25,6 +25,7 @@ const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); | ||
| 25 | 25 | * @type {Map<string, UserDirectoryList>} |
| 26 | 26 | */ |
| 27 | 27 | const DIRECTORIES_CACHE = new Map(); |
| 28 | +const PUBLIC_USER_AVATAR = '/img/default-user.png'; | |
| 28 | 29 | |
| 29 | 30 | const STORAGE_KEYS = { |
| 30 | 31 | csrfSecret: 'csrfSecret', |
| @@ -510,11 +511,11 @@ async function getUserAvatar(handle) { | ||
| 510 | 511 | const settings = fs.existsSync(pathToSettings) ? JSON.parse(fs.readFileSync(pathToSettings, 'utf8')) : {}; |
| 511 | 512 | const avatarFile = settings?.power_user?.default_persona || settings?.user_avatar; |
| 512 | 513 | if (!avatarFile) { |
| 513 | 514 | return DEFAULT_AVATARPUBLIC_USER_AVATAR; |
| 514 | 515 | } |
| 515 | 516 | const avatarPath = path.join(directory.avatars, avatarFile); |
| 516 | 517 | if (!fs.existsSync(avatarPath)) { |
| 517 | 518 | return DEFAULT_AVATARPUBLIC_USER_AVATAR; |
| 518 | 519 | } |
| 519 | 520 | const mimeType = mime.lookup(avatarPath); |
| 520 | 521 | const base64Content = fs.readFileSync(avatarPath, 'base64'); |
| @@ -522,7 +523,7 @@ async function getUserAvatar(handle) { | ||
| 522 | 523 | } |
| 523 | 524 | catch { |
| 524 | 525 | // Ignore errors |
| 525 | 526 | return DEFAULT_AVATARPUBLIC_USER_AVATAR; |
| 526 | 527 | } |
| 527 | 528 | } |
| 528 | 529 | |