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