Merge branch 'staging' into continue-from-reasoning
| @@ -1962,7 +1962,7 @@ | |||
| 1962 | </span> | 1962 | </span> |
| 1963 | </div> | 1963 | </div> |
| 1964 | </div> | 1964 | </div> |
| 1965 | <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek,makersuite"> | 1965 | <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek,makersuite,ai21"> |
| 1966 | <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand"> | 1966 | <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand"> |
| 1967 | <input id="openai_function_calling" type="checkbox" /> | 1967 | <input id="openai_function_calling" type="checkbox" /> |
| 1968 | <span data-i18n="Enable function calling">Enable function calling</span> | 1968 | <span data-i18n="Enable function calling">Enable function calling</span> |
| @@ -3078,7 +3078,15 @@ | |||
| 3078 | <div> | 3078 | <div> |
| 3079 | <h4 data-i18n="AI21 Model">AI21 Model</h4> | 3079 | <h4 data-i18n="AI21 Model">AI21 Model</h4> |
| 3080 | <select id="model_ai21_select"> | 3080 | <select id="model_ai21_select"> |
| 3081 | <optgroup label="Jamba 1.5"> | 3081 | <optgroup label="Jamba (Latest)"> |
| 3082 | <option value="jamba-mini">jamba-mini</option> | ||
| 3083 | <option value="jamba-large">jamba-large</option> | ||
| 3084 | </optgroup> | ||
| 3085 | <optgroup label="Jamba 1.6"> | ||
| 3086 | <option value="jamba-1.6-mini">jamba-1.6-mini</option> | ||
| 3087 | <option value="jamba-1.6-large">jamba-1.6-large</option> | ||
| 3088 | </optgroup> | ||
| 3089 | <optgroup label="Jamba 1.5 (Deprecated)"> | ||
| 3082 | <option value="jamba-1.5-mini">jamba-1.5-mini</option> | 3090 | <option value="jamba-1.5-mini">jamba-1.5-mini</option> |
| 3083 | <option value="jamba-1.5-large">jamba-1.5-large</option> | 3091 | <option value="jamba-1.5-large">jamba-1.5-large</option> |
| 3084 | </optgroup> | 3092 | </optgroup> |
| @@ -271,7 +271,7 @@ import { initSettingsSearch } from './scripts/setting-search.js'; | |||
| 271 | import { initBulkEdit } from './scripts/bulk-edit.js'; | 271 | import { initBulkEdit } from './scripts/bulk-edit.js'; |
| 272 | import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js'; | 272 | import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js'; |
| 273 | import { getContext } from './scripts/st-context.js'; | 273 | import { getContext } from './scripts/st-context.js'; |
| 274 | import { extractReasoningFromData, initReasoning, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js'; | 274 | import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js'; |
| 275 | import { accountStorage } from './scripts/util/AccountStorage.js'; | 275 | import { accountStorage } from './scripts/util/AccountStorage.js'; |
| 276 | 276 | ||
| 277 | // API OBJECT FOR EXTERNAL WIRING | 277 | // API OBJECT FOR EXTERNAL WIRING |
| @@ -3346,15 +3346,18 @@ class StreamingProcessor { | |||
| 3346 | 3346 | ||
| 3347 | if (Array.isArray(this.swipes) && this.swipes.length > 0) { | 3347 | if (Array.isArray(this.swipes) && this.swipes.length > 0) { |
| 3348 | const message = chat[messageId]; | 3348 | const message = chat[messageId]; |
| 3349 | const swipeInfoExtra = structuredClone(message.extra ?? {}); | ||
| 3350 | delete swipeInfoExtra.token_count; | ||
| 3351 | delete swipeInfoExtra.reasoning; | ||
| 3352 | delete swipeInfoExtra.reasoning_duration; | ||
| 3349 | const swipeInfo = { | 3353 | const swipeInfo = { |
| 3350 | send_date: message.send_date, | 3354 | send_date: message.send_date, |
| 3351 | gen_started: message.gen_started, | 3355 | gen_started: message.gen_started, |
| 3352 | gen_finished: message.gen_finished, | 3356 | gen_finished: message.gen_finished, |
| 3353 | extra: structuredClone(message.extra), | 3357 | extra: swipeInfoExtra, |
| 3354 | }; | 3358 | }; |
| 3355 | const swipeInfoArray = []; | 3359 | const swipeInfoArray = Array(this.swipes.length).fill().map(() => structuredClone(swipeInfo)); |
| 3356 | swipeInfoArray.length = this.swipes.length; | 3360 | parseReasoningInSwipes(this.swipes, swipeInfoArray, message.extra?.reasoning_duration); |
| 3357 | swipeInfoArray.fill(swipeInfo); | ||
| 3358 | chat[messageId].swipes.push(...this.swipes); | 3361 | chat[messageId].swipes.push(...this.swipes); |
| 3359 | chat[messageId].swipe_info.push(...swipeInfoArray); | 3362 | chat[messageId].swipe_info.push(...swipeInfoArray); |
| 3360 | } | 3363 | } |
| @@ -3366,6 +3369,7 @@ class StreamingProcessor { | |||
| 3366 | await eventSource.emit(event_types.IMPERSONATE_READY, text); | 3369 | await eventSource.emit(event_types.IMPERSONATE_READY, text); |
| 3367 | } | 3370 | } |
| 3368 | 3371 | ||
| 3372 | syncMesToSwipe(messageId); | ||
| 3369 | saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage); | 3373 | saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage); |
| 3370 | await saveChatConditional(); | 3374 | await saveChatConditional(); |
| 3371 | unblockGeneration(); | 3375 | unblockGeneration(); |
| @@ -6117,15 +6121,18 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, | |||
| 6117 | } | 6121 | } |
| 6118 | 6122 | ||
| 6119 | if (Array.isArray(swipes) && swipes.length > 0) { | 6123 | if (Array.isArray(swipes) && swipes.length > 0) { |
| 6124 | const swipeInfoExtra = structuredClone(item.extra ?? {}); | ||
| 6125 | delete swipeInfoExtra.token_count; | ||
| 6126 | delete swipeInfoExtra.reasoning; | ||
| 6127 | delete swipeInfoExtra.reasoning_duration; | ||
| 6120 | const swipeInfo = { | 6128 | const swipeInfo = { |
| 6121 | send_date: item.send_date, | 6129 | send_date: item.send_date, |
| 6122 | gen_started: item.gen_started, | 6130 | gen_started: item.gen_started, |
| 6123 | gen_finished: item.gen_finished, | 6131 | gen_finished: item.gen_finished, |
| 6124 | extra: structuredClone(item.extra), | 6132 | extra: swipeInfoExtra, |
| 6125 | }; | 6133 | }; |
| 6126 | const swipeInfoArray = []; | 6134 | const swipeInfoArray = Array(swipes.length).fill().map(() => structuredClone(swipeInfo)); |
| 6127 | swipeInfoArray.length = swipes.length; | 6135 | parseReasoningInSwipes(swipes, swipeInfoArray, item.extra?.reasoning_duration); |
| 6128 | swipeInfoArray.fill(swipeInfo, 0, swipes.length); | ||
| 6129 | item.swipes.push(...swipes); | 6136 | item.swipes.push(...swipes); |
| 6130 | item.swipe_info.push(...swipeInfoArray); | 6137 | item.swipe_info.push(...swipeInfoArray); |
| 6131 | } | 6138 | } |
| @@ -1070,7 +1070,7 @@ export async function installExtension(url, global) { | |||
| 1070 | toastr.success(t`Extension '${response.display_name}' by ${response.author} (version ${response.version}) has been installed successfully!`, t`Extension installation successful`); | 1070 | toastr.success(t`Extension '${response.display_name}' by ${response.author} (version ${response.version}) has been installed successfully!`, t`Extension installation successful`); |
| 1071 | console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`); | 1071 | console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`); |
| 1072 | await loadExtensionSettings({}, false, false); | 1072 | await loadExtensionSettings({}, false, false); |
| 1073 | await eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED); | 1073 | await eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED, response); |
| 1074 | } | 1074 | } |
| 1075 | 1075 | ||
| 1076 | /** | 1076 | /** |
| @@ -19,6 +19,7 @@ import { | |||
| 19 | modules, | 19 | modules, |
| 20 | renderExtensionTemplateAsync, | 20 | renderExtensionTemplateAsync, |
| 21 | doExtrasFetch, getApiUrl, | 21 | doExtrasFetch, getApiUrl, |
| 22 | openThirdPartyExtensionMenu, | ||
| 22 | } from '../../extensions.js'; | 23 | } from '../../extensions.js'; |
| 23 | import { collapseNewlines, registerDebugFunction } from '../../power-user.js'; | 24 | import { collapseNewlines, registerDebugFunction } from '../../power-user.js'; |
| 24 | import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js'; | 25 | import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js'; |
| @@ -34,6 +35,7 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm | |||
| 34 | import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; | 35 | import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; |
| 35 | import { callGenericPopup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js'; | 36 | import { callGenericPopup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js'; |
| 36 | import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js'; | 37 | import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js'; |
| 38 | import { WebLlmVectorProvider } from './webllm.js'; | ||
| 37 | 39 | ||
| 38 | /** | 40 | /** |
| 39 | * @typedef {object} HashedMessage | 41 | * @typedef {object} HashedMessage |
| @@ -60,6 +62,7 @@ const settings = { | |||
| 60 | ollama_model: 'mxbai-embed-large', | 62 | ollama_model: 'mxbai-embed-large', |
| 61 | ollama_keep: false, | 63 | ollama_keep: false, |
| 62 | vllm_model: '', | 64 | vllm_model: '', |
| 65 | webllm_model: '', | ||
| 63 | summarize: false, | 66 | summarize: false, |
| 64 | summarize_sent: false, | 67 | summarize_sent: false, |
| 65 | summary_source: 'main', | 68 | summary_source: 'main', |
| @@ -103,7 +106,7 @@ const settings = { | |||
| 103 | }; | 106 | }; |
| 104 | 107 | ||
| 105 | const moduleWorker = new ModuleWorkerWrapper(synchronizeChat); | 108 | const moduleWorker = new ModuleWorkerWrapper(synchronizeChat); |
| 106 | 109 | const webllmProvider = new WebLlmVectorProvider(); | |
| 107 | const cachedSummaries = new Map(); | 110 | const cachedSummaries = new Map(); |
| 108 | 111 | ||
| 109 | /** | 112 | /** |
| @@ -373,6 +376,8 @@ async function synchronizeChat(batchSize = 5) { | |||
| 373 | return 'Vectorization Source Model is required, but not set.'; | 376 | return 'Vectorization Source Model is required, but not set.'; |
| 374 | case 'extras_module_missing': | 377 | case 'extras_module_missing': |
| 375 | return 'Extras API must provide an "embeddings" module.'; | 378 | return 'Extras API must provide an "embeddings" module.'; |
| 379 | case 'webllm_not_supported': | ||
| 380 | return 'WebLLM extension is not installed or the model is not set.'; | ||
| 376 | default: | 381 | default: |
| 377 | return 'Check server console for more details'; | 382 | return 'Check server console for more details'; |
| 378 | } | 383 | } |
| @@ -747,14 +752,15 @@ async function getQueryText(chat, initiator) { | |||
| 747 | 752 | ||
| 748 | /** | 753 | /** |
| 749 | * Gets common body parameters for vector requests. | 754 | * Gets common body parameters for vector requests. |
| 750 | * @returns {object} | 755 | * @param {object} args Additional arguments |
| 756 | * @returns {object} Request body | ||
| 751 | */ | 757 | */ |
| 752 | function getVectorsRequestBody() { | 758 | function getVectorsRequestBody(args = {}) { |
| 753 | const body = {}; | 759 | const body = Object.assign({}, args); |
| 754 | switch (settings.source) { | 760 | switch (settings.source) { |
| 755 | case 'extras': | 761 | case 'extras': |
| 756 | body.extrasUrl = extension_settings.apiUrl; | 762 | body.extrasUrl = extension_settings.apiUrl; |
| 757 | body.extrasKey = extension_settings.apiKey; | 763 | body.extrasKey = extension_settings.apiKey; |
| 758 | break; | 764 | break; |
| 759 | case 'togetherai': | 765 | case 'togetherai': |
| 760 | body.model = extension_settings.vectors.togetherai_model; | 766 | body.model = extension_settings.vectors.togetherai_model; |
| @@ -777,6 +783,9 @@ function getVectorsRequestBody() { | |||
| 777 | body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.VLLM]; | 783 | body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.VLLM]; |
| 778 | body.model = extension_settings.vectors.vllm_model; | 784 | body.model = extension_settings.vectors.vllm_model; |
| 779 | break; | 785 | break; |
| 786 | case 'webllm': | ||
| 787 | body.model = extension_settings.vectors.webllm_model; | ||
| 788 | break; | ||
| 780 | default: | 789 | default: |
| 781 | break; | 790 | break; |
| 782 | } | 791 | } |
| @@ -784,6 +793,21 @@ function getVectorsRequestBody() { | |||
| 784 | } | 793 | } |
| 785 | 794 | ||
| 786 | /** | 795 | /** |
| 796 | * Gets additional arguments for vector requests. | ||
| 797 | * @param {string[]} items Items to embed | ||
| 798 | * @returns {Promise<object>} Additional arguments | ||
| 799 | */ | ||
| 800 | async function getAdditionalArgs(items) { | ||
| 801 | const args = {}; | ||
| 802 | switch (settings.source) { | ||
| 803 | case 'webllm': | ||
| 804 | args.embeddings = await createWebLlmEmbeddings(items); | ||
| 805 | break; | ||
| 806 | } | ||
| 807 | return args; | ||
| 808 | } | ||
| 809 | |||
| 810 | /** | ||
| 787 | * Gets the saved hashes for a collection | 811 | * Gets the saved hashes for a collection |
| 788 | * @param {string} collectionId | 812 | * @param {string} collectionId |
| 789 | * @returns {Promise<number[]>} Saved hashes | 813 | * @returns {Promise<number[]>} Saved hashes |
| @@ -816,11 +840,12 @@ async function getSavedHashes(collectionId) { | |||
| 816 | async function insertVectorItems(collectionId, items) { | 840 | async function insertVectorItems(collectionId, items) { |
| 817 | throwIfSourceInvalid(); | 841 | throwIfSourceInvalid(); |
| 818 | 842 | ||
| 843 | const args = await getAdditionalArgs(items.map(x => x.text)); | ||
| 819 | const response = await fetch('/api/vector/insert', { | 844 | const response = await fetch('/api/vector/insert', { |
| 820 | method: 'POST', | 845 | method: 'POST', |
| 821 | headers: getRequestHeaders(), | 846 | headers: getRequestHeaders(), |
| 822 | body: JSON.stringify({ | 847 | body: JSON.stringify({ |
| 823 | ...getVectorsRequestBody(), | 848 | ...getVectorsRequestBody(args), |
| 824 | collectionId: collectionId, | 849 | collectionId: collectionId, |
| 825 | items: items, | 850 | items: items, |
| 826 | source: settings.source, | 851 | source: settings.source, |
| @@ -858,6 +883,10 @@ function throwIfSourceInvalid() { | |||
| 858 | if (settings.source === 'extras' && !modules.includes('embeddings')) { | 883 | if (settings.source === 'extras' && !modules.includes('embeddings')) { |
| 859 | throw new Error('Vectors: Embeddings module missing', { cause: 'extras_module_missing' }); | 884 | throw new Error('Vectors: Embeddings module missing', { cause: 'extras_module_missing' }); |
| 860 | } | 885 | } |
| 886 | |||
| 887 | if (settings.source === 'webllm' && (!isWebLlmSupported() || !settings.webllm_model)) { | ||
| 888 | throw new Error('Vectors: WebLLM is not supported', { cause: 'webllm_not_supported' }); | ||
| 889 | } | ||
| 861 | } | 890 | } |
| 862 | 891 | ||
| 863 | /** | 892 | /** |
| @@ -890,11 +919,12 @@ async function deleteVectorItems(collectionId, hashes) { | |||
| 890 | * @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results | 919 | * @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results |
| 891 | */ | 920 | */ |
| 892 | async function queryCollection(collectionId, searchText, topK) { | 921 | async function queryCollection(collectionId, searchText, topK) { |
| 922 | const args = await getAdditionalArgs([searchText]); | ||
| 893 | const response = await fetch('/api/vector/query', { | 923 | const response = await fetch('/api/vector/query', { |
| 894 | method: 'POST', | 924 | method: 'POST', |
| 895 | headers: getRequestHeaders(), | 925 | headers: getRequestHeaders(), |
| 896 | body: JSON.stringify({ | 926 | body: JSON.stringify({ |
| 897 | ...getVectorsRequestBody(), | 927 | ...getVectorsRequestBody(args), |
| 898 | collectionId: collectionId, | 928 | collectionId: collectionId, |
| 899 | searchText: searchText, | 929 | searchText: searchText, |
| 900 | topK: topK, | 930 | topK: topK, |
| @@ -919,11 +949,12 @@ async function queryCollection(collectionId, searchText, topK) { | |||
| 919 | * @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs | 949 | * @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs |
| 920 | */ | 950 | */ |
| 921 | async function queryMultipleCollections(collectionIds, searchText, topK, threshold) { | 951 | async function queryMultipleCollections(collectionIds, searchText, topK, threshold) { |
| 952 | const args = await getAdditionalArgs([searchText]); | ||
| 922 | const response = await fetch('/api/vector/query-multi', { | 953 | const response = await fetch('/api/vector/query-multi', { |
| 923 | method: 'POST', | 954 | method: 'POST', |
| 924 | headers: getRequestHeaders(), | 955 | headers: getRequestHeaders(), |
| 925 | body: JSON.stringify({ | 956 | body: JSON.stringify({ |
| 926 | ...getVectorsRequestBody(), | 957 | ...getVectorsRequestBody(args), |
| 927 | collectionIds: collectionIds, | 958 | collectionIds: collectionIds, |
| 928 | searchText: searchText, | 959 | searchText: searchText, |
| 929 | topK: topK, | 960 | topK: topK, |
| @@ -1039,6 +1070,72 @@ function toggleSettings() { | |||
| 1039 | $('#llamacpp_vectorsModel').toggle(settings.source === 'llamacpp'); | 1070 | $('#llamacpp_vectorsModel').toggle(settings.source === 'llamacpp'); |
| 1040 | $('#vllm_vectorsModel').toggle(settings.source === 'vllm'); | 1071 | $('#vllm_vectorsModel').toggle(settings.source === 'vllm'); |
| 1041 | $('#nomicai_apiKey').toggle(settings.source === 'nomicai'); | 1072 | $('#nomicai_apiKey').toggle(settings.source === 'nomicai'); |
| 1073 | $('#webllm_vectorsModel').toggle(settings.source === 'webllm'); | ||
| 1074 | if (settings.source === 'webllm') { | ||
| 1075 | loadWebLlmModels(); | ||
| 1076 | } | ||
| 1077 | } | ||
| 1078 | |||
| 1079 | /** | ||
| 1080 | * Executes a function with WebLLM error handling. | ||
| 1081 | * @param {function(): Promise<T>} func Function to execute | ||
| 1082 | * @returns {Promise<T>} | ||
| 1083 | * @template T | ||
| 1084 | */ | ||
| 1085 | async function executeWithWebLlmErrorHandling(func) { | ||
| 1086 | try { | ||
| 1087 | return await func(); | ||
| 1088 | } catch (error) { | ||
| 1089 | console.log('Vectors: Failed to load WebLLM models', error); | ||
| 1090 | if (!(error instanceof Error)) { | ||
| 1091 | return; | ||
| 1092 | } | ||
| 1093 | switch (error.cause) { | ||
| 1094 | case 'webllm-not-available': | ||
| 1095 | toastr.warning('WebLLM is not available. Please install the extension.', 'WebLLM not installed'); | ||
| 1096 | break; | ||
| 1097 | case 'webllm-not-updated': | ||
| 1098 | toastr.warning('The installed extension version does not support embeddings.', 'WebLLM update required'); | ||
| 1099 | break; | ||
| 1100 | } | ||
| 1101 | } | ||
| 1102 | } | ||
| 1103 | |||
| 1104 | /** | ||
| 1105 | * Loads and displays WebLLM models in the settings. | ||
| 1106 | * @returns {Promise<void>} | ||
| 1107 | */ | ||
| 1108 | function loadWebLlmModels() { | ||
| 1109 | return executeWithWebLlmErrorHandling(() => { | ||
| 1110 | const models = webllmProvider.getModels(); | ||
| 1111 | $('#vectors_webllm_model').empty(); | ||
| 1112 | for (const model of models) { | ||
| 1113 | $('#vectors_webllm_model').append($('<option>', { value: model.id, text: model.toString() })); | ||
| 1114 | } | ||
| 1115 | if (!settings.webllm_model || !models.some(x => x.id === settings.webllm_model)) { | ||
| 1116 | if (models.length) { | ||
| 1117 | settings.webllm_model = models[0].id; | ||
| 1118 | } | ||
| 1119 | } | ||
| 1120 | $('#vectors_webllm_model').val(settings.webllm_model); | ||
| 1121 | return Promise.resolve(); | ||
| 1122 | }); | ||
| 1123 | } | ||
| 1124 | |||
| 1125 | /** | ||
| 1126 | * Creates WebLLM embeddings for a list of items. | ||
| 1127 | * @param {string[]} items Items to embed | ||
| 1128 | * @returns {Promise<Record<string, number[]>>} Calculated embeddings | ||
| 1129 | */ | ||
| 1130 | async function createWebLlmEmbeddings(items) { | ||
| 1131 | return executeWithWebLlmErrorHandling(async () => { | ||
| 1132 | const embeddings = await webllmProvider.embedTexts(items, settings.webllm_model); | ||
| 1133 | const result = /** @type {Record<string, number[]>} */ ({}); | ||
| 1134 | for (let i = 0; i < items.length; i++) { | ||
| 1135 | result[items[i]] = embeddings[i]; | ||
| 1136 | } | ||
| 1137 | return result; | ||
| 1138 | }); | ||
| 1042 | } | 1139 | } |
| 1043 | 1140 | ||
| 1044 | async function onPurgeClick() { | 1141 | async function onPurgeClick() { |
| @@ -1567,6 +1664,30 @@ jQuery(async () => { | |||
| 1567 | $('#dialogue_popup_input').val(presetModel); | 1664 | $('#dialogue_popup_input').val(presetModel); |
| 1568 | }); | 1665 | }); |
| 1569 | 1666 | ||
| 1667 | $('#vectors_webllm_install').on('click', (e) => { | ||
| 1668 | e.preventDefault(); | ||
| 1669 | e.stopPropagation(); | ||
| 1670 | |||
| 1671 | if (Object.hasOwn(SillyTavern, 'llm')) { | ||
| 1672 | toastr.info('WebLLM is already installed'); | ||
| 1673 | return; | ||
| 1674 | } | ||
| 1675 | |||
| 1676 | openThirdPartyExtensionMenu('https://github.com/SillyTavern/Extension-WebLLM'); | ||
| 1677 | }); | ||
| 1678 | |||
| 1679 | $('#vectors_webllm_model').on('input', () => { | ||
| 1680 | settings.webllm_model = String($('#vectors_webllm_model').val()); | ||
| 1681 | Object.assign(extension_settings.vectors, settings); | ||
| 1682 | saveSettingsDebounced(); | ||
| 1683 | }); | ||
| 1684 | |||
| 1685 | $('#vectors_webllm_load').on('click', async () => { | ||
| 1686 | if (!settings.webllm_model) return; | ||
| 1687 | await webllmProvider.loadModel(settings.webllm_model); | ||
| 1688 | toastr.success('WebLLM model loaded'); | ||
| 1689 | }); | ||
| 1690 | |||
| 1570 | $('#api_key_nomicai').toggleClass('success', !!secret_state[SECRET_KEYS.NOMICAI]); | 1691 | $('#api_key_nomicai').toggleClass('success', !!secret_state[SECRET_KEYS.NOMICAI]); |
| 1571 | 1692 | ||
| 1572 | toggleSettings(); | 1693 | toggleSettings(); |
| @@ -1578,6 +1699,11 @@ jQuery(async () => { | |||
| 1578 | eventSource.on(event_types.CHAT_DELETED, purgeVectorIndex); | 1699 | eventSource.on(event_types.CHAT_DELETED, purgeVectorIndex); |
| 1579 | eventSource.on(event_types.GROUP_CHAT_DELETED, purgeVectorIndex); | 1700 | eventSource.on(event_types.GROUP_CHAT_DELETED, purgeVectorIndex); |
| 1580 | eventSource.on(event_types.FILE_ATTACHMENT_DELETED, purgeFileVectorIndex); | 1701 | eventSource.on(event_types.FILE_ATTACHMENT_DELETED, purgeFileVectorIndex); |
| 1702 | eventSource.on(event_types.EXTENSION_SETTINGS_LOADED, async (manifest) => { | ||
| 1703 | if (settings.source === 'webllm' && manifest?.display_name === 'WebLLM') { | ||
| 1704 | await loadWebLlmModels(); | ||
| 1705 | } | ||
| 1706 | }); | ||
| 1581 | 1707 | ||
| 1582 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 1708 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 1583 | name: 'db-ingest', | 1709 | name: 'db-ingest', |
| @@ -21,8 +21,24 @@ | |||
| 21 | <option value="openai">OpenAI</option> | 21 | <option value="openai">OpenAI</option> |
| 22 | <option value="togetherai">TogetherAI</option> | 22 | <option value="togetherai">TogetherAI</option> |
| 23 | <option value="vllm">vLLM</option> | 23 | <option value="vllm">vLLM</option> |
| 24 | <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option> | ||
| 24 | </select> | 25 | </select> |
| 25 | </div> | 26 | </div> |
| 27 | <div class="flex-container flexFlowColumn" id="webllm_vectorsModel"> | ||
| 28 | <label for="vectors_webllm_model" data-i18n="Vectorization Model"> | ||
| 29 | Vectorization Model | ||
| 30 | </label> | ||
| 31 | <div class="flex-container"> | ||
| 32 | <select id="vectors_webllm_model" class="text_pole flex1"> | ||
| 33 | </select> | ||
| 34 | <div id="vectors_webllm_load" class="menu_button menu_button_icon" title="Verify and load the selected model."> | ||
| 35 | <i class="fa-solid fa-check-to-slot"></i> | ||
| 36 | </div> | ||
| 37 | </div> | ||
| 38 | <div> | ||
| 39 | Requires the WebLLM extension to be installed. Click <a href="#" id="vectors_webllm_install">here</a> to install. | ||
| 40 | </div> | ||
| 41 | </div> | ||
| 26 | <div class="flex-container flexFlowColumn" id="ollama_vectorsModel"> | 42 | <div class="flex-container flexFlowColumn" id="ollama_vectorsModel"> |
| 27 | <label for="vectors_ollama_model" data-i18n="Vectorization Model"> | 43 | <label for="vectors_ollama_model" data-i18n="Vectorization Model"> |
| 28 | Vectorization Model | 44 | Vectorization Model |
| @@ -0,0 +1,64 @@ | |||
| 1 | export class WebLlmVectorProvider { | ||
| 2 | /** @type {object?} WebLLM engine */ | ||
| 3 | #engine = null; | ||
| 4 | |||
| 5 | constructor() { | ||
| 6 | this.#engine = null; | ||
| 7 | } | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Check if WebLLM is available and up-to-date | ||
| 11 | * @throws {Error} If WebLLM is not available or not up-to-date | ||
| 12 | */ | ||
| 13 | #checkWebLlm() { | ||
| 14 | if (!Object.hasOwn(SillyTavern, 'llm')) { | ||
| 15 | throw new Error('WebLLM is not available', { cause: 'webllm-not-available' }); | ||
| 16 | } | ||
| 17 | |||
| 18 | if (typeof SillyTavern.llm.generateEmbedding !== 'function') { | ||
| 19 | throw new Error('WebLLM is not updated', { cause: 'webllm-not-updated' }); | ||
| 20 | } | ||
| 21 | } | ||
| 22 | |||
| 23 | /** | ||
| 24 | * Initialize the engine with a model. | ||
| 25 | * @param {string} modelId Model ID to initialize the engine with | ||
| 26 | * @returns {Promise<void>} Promise that resolves when the engine is initialized | ||
| 27 | */ | ||
| 28 | #initEngine(modelId) { | ||
| 29 | this.#checkWebLlm(); | ||
| 30 | if (!this.#engine) { | ||
| 31 | this.#engine = SillyTavern.llm.getEngine(); | ||
| 32 | } | ||
| 33 | |||
| 34 | return this.#engine.loadModel(modelId); | ||
| 35 | } | ||
| 36 | |||
| 37 | /** | ||
| 38 | * Get available models. | ||
| 39 | * @returns {{id:string, toString: function(): string}[]} Array of available models | ||
| 40 | */ | ||
| 41 | getModels() { | ||
| 42 | this.#checkWebLlm(); | ||
| 43 | return SillyTavern.llm.getEmbeddingModels(); | ||
| 44 | } | ||
| 45 | |||
| 46 | /** | ||
| 47 | * Generate embeddings for a list of texts. | ||
| 48 | * @param {string[]} texts Array of texts to generate embeddings for | ||
| 49 | * @param {string} modelId Model to use for generating embeddings | ||
| 50 | * @returns {Promise<number[][]>} Array of embeddings for each text | ||
| 51 | */ | ||
| 52 | async embedTexts(texts, modelId) { | ||
| 53 | await this.#initEngine(modelId); | ||
| 54 | return this.#engine.generateEmbedding(texts); | ||
| 55 | } | ||
| 56 | |||
| 57 | /** | ||
| 58 | * Loads a model into the engine. | ||
| 59 | * @param {string} modelId Model ID to load | ||
| 60 | */ | ||
| 61 | async loadModel(modelId) { | ||
| 62 | await this.#initEngine(modelId); | ||
| 63 | } | ||
| 64 | } | ||
| @@ -337,7 +337,7 @@ const default_settings = { | |||
| 337 | openai_model: 'gpt-4-turbo', | 337 | openai_model: 'gpt-4-turbo', |
| 338 | claude_model: 'claude-3-5-sonnet-20240620', | 338 | claude_model: 'claude-3-5-sonnet-20240620', |
| 339 | google_model: 'gemini-1.5-pro', | 339 | google_model: 'gemini-1.5-pro', |
| 340 | ai21_model: 'jamba-1.5-large', | 340 | ai21_model: 'jamba-1.6-large', |
| 341 | mistralai_model: 'mistral-large-latest', | 341 | mistralai_model: 'mistral-large-latest', |
| 342 | cohere_model: 'command-r-plus', | 342 | cohere_model: 'command-r-plus', |
| 343 | perplexity_model: 'sonar-pro', | 343 | perplexity_model: 'sonar-pro', |
| @@ -417,7 +417,7 @@ const oai_settings = { | |||
| 417 | openai_model: 'gpt-4-turbo', | 417 | openai_model: 'gpt-4-turbo', |
| 418 | claude_model: 'claude-3-5-sonnet-20240620', | 418 | claude_model: 'claude-3-5-sonnet-20240620', |
| 419 | google_model: 'gemini-1.5-pro', | 419 | google_model: 'gemini-1.5-pro', |
| 420 | ai21_model: 'jamba-1.5-large', | 420 | ai21_model: 'jamba-1.6-large', |
| 421 | mistralai_model: 'mistral-large-latest', | 421 | mistralai_model: 'mistral-large-latest', |
| 422 | cohere_model: 'command-r-plus', | 422 | cohere_model: 'command-r-plus', |
| 423 | perplexity_model: 'sonar-pro', | 423 | perplexity_model: 'sonar-pro', |
| @@ -2027,12 +2027,16 @@ async function sendOpenAIRequest(type, messages, signal) { | |||
| 2027 | generate_data['logprobs'] = 5; | 2027 | generate_data['logprobs'] = 5; |
| 2028 | } | 2028 | } |
| 2029 | 2029 | ||
| 2030 | // Remove logit bias, logprobs and stop strings if it's not supported by the model | 2030 | // Remove logit bias/logprobs/stop-strings if not supported by the model |
| 2031 | if (isOAI && oai_settings.openai_model.includes('vision') || isOpenRouter && oai_settings.openrouter_model.includes('vision') || isOAI && oai_settings.openai_model.includes('gpt-4.5-preview')) { | 2031 | const isVision = (m) => ['gpt', 'vision'].every(x => m.includes(x)); |
| 2032 | if (isOAI && isVision(oai_settings.openai_model) || isOpenRouter && isVision(oai_settings.openrouter_model)) { | ||
| 2032 | delete generate_data.logit_bias; | 2033 | delete generate_data.logit_bias; |
| 2033 | delete generate_data.stop; | 2034 | delete generate_data.stop; |
| 2034 | delete generate_data.logprobs; | 2035 | delete generate_data.logprobs; |
| 2035 | } | 2036 | } |
| 2037 | if (isOAI && oai_settings.openai_model.includes('gpt-4.5-preview') || isOpenRouter && oai_settings.openrouter_model.includes('gpt-4.5-preview')) { | ||
| 2038 | delete generate_data.logprobs; | ||
| 2039 | } | ||
| 2036 | 2040 | ||
| 2037 | if (isClaude) { | 2041 | if (isClaude) { |
| 2038 | generate_data['top_k'] = Number(oai_settings.top_k_openai); | 2042 | generate_data['top_k'] = Number(oai_settings.top_k_openai); |
| @@ -3251,7 +3255,7 @@ function loadOpenAISettings(data, settings) { | |||
| 3251 | } | 3255 | } |
| 3252 | 3256 | ||
| 3253 | if (oai_settings.ai21_model.startsWith('j2-')) { | 3257 | if (oai_settings.ai21_model.startsWith('j2-')) { |
| 3254 | oai_settings.ai21_model = 'jamba-1.5-large'; | 3258 | oai_settings.ai21_model = 'jamba-1.6-large'; |
| 3255 | } | 3259 | } |
| 3256 | 3260 | ||
| 3257 | if (settings.wrap_in_quotes !== undefined) oai_settings.wrap_in_quotes = !!settings.wrap_in_quotes; | 3261 | if (settings.wrap_in_quotes !== undefined) oai_settings.wrap_in_quotes = !!settings.wrap_in_quotes; |
| @@ -4208,7 +4212,7 @@ async function onModelChange() { | |||
| 4208 | 4212 | ||
| 4209 | if ($(this).is('#model_ai21_select')) { | 4213 | if ($(this).is('#model_ai21_select')) { |
| 4210 | if (value === '' || value.startsWith('j2-')) { | 4214 | if (value === '' || value.startsWith('j2-')) { |
| 4211 | value = 'jamba-1.5-large'; | 4215 | value = 'jamba-1.6-large'; |
| 4212 | $('#model_ai21_select').val(value); | 4216 | $('#model_ai21_select').val(value); |
| 4213 | } | 4217 | } |
| 4214 | 4218 | ||
| @@ -4485,7 +4489,7 @@ async function onModelChange() { | |||
| 4485 | if (oai_settings.chat_completion_source == chat_completion_sources.AI21) { | 4489 | if (oai_settings.chat_completion_source == chat_completion_sources.AI21) { |
| 4486 | if (oai_settings.max_context_unlocked) { | 4490 | if (oai_settings.max_context_unlocked) { |
| 4487 | $('#openai_max_context').attr('max', unlocked_max); | 4491 | $('#openai_max_context').attr('max', unlocked_max); |
| 4488 | } else if (oai_settings.ai21_model.includes('jamba-1.5') || oai_settings.ai21_model.includes('jamba-instruct')) { | 4492 | } else if (oai_settings.ai21_model.startsWith('jamba-')) { |
| 4489 | $('#openai_max_context').attr('max', max_256k); | 4493 | $('#openai_max_context').attr('max', max_256k); |
| 4490 | } | 4494 | } |
| 4491 | 4495 | ||
| @@ -1104,6 +1104,32 @@ function parseReasoningFromString(str, { strict = true } = {}) { | |||
| 1104 | } | 1104 | } |
| 1105 | } | 1105 | } |
| 1106 | 1106 | ||
| 1107 | /** | ||
| 1108 | * Parse reasoning in an array of swipe strings if auto-parsing is enabled. | ||
| 1109 | * @param {string[]} swipes Array of swipe strings | ||
| 1110 | * @param {{extra: {reasoning: string, reasoning_duration: number}}[]} swipeInfoArray Array of swipe info objects | ||
| 1111 | * @param {number?} duration Duration of the reasoning | ||
| 1112 | */ | ||
| 1113 | export function parseReasoningInSwipes(swipes, swipeInfoArray, duration) { | ||
| 1114 | if (!power_user.reasoning.auto_parse) { | ||
| 1115 | return; | ||
| 1116 | } | ||
| 1117 | |||
| 1118 | // Something ain't right, don't parse | ||
| 1119 | if (!Array.isArray(swipes) || !Array.isArray(swipeInfoArray) || swipes.length !== swipeInfoArray.length) { | ||
| 1120 | return; | ||
| 1121 | } | ||
| 1122 | |||
| 1123 | for (let index = 0; index < swipes.length; index++) { | ||
| 1124 | const parsedReasoning = parseReasoningFromString(swipes[index]); | ||
| 1125 | if (parsedReasoning) { | ||
| 1126 | swipes[index] = parsedReasoning.content; | ||
| 1127 | swipeInfoArray[index].extra.reasoning = parsedReasoning.reasoning; | ||
| 1128 | swipeInfoArray[index].extra.reasoning_duration = duration; | ||
| 1129 | } | ||
| 1130 | } | ||
| 1131 | } | ||
| 1132 | |||
| 1107 | function registerReasoningAppEvents() { | 1133 | function registerReasoningAppEvents() { |
| 1108 | const eventHandler = (/** @type {string} */ type, /** @type {number} */ idx) => { | 1134 | const eventHandler = (/** @type {string} */ type, /** @type {number} */ idx) => { |
| 1109 | if (!power_user.reasoning.auto_parse) { | 1135 | if (!power_user.reasoning.auto_parse) { |
| @@ -585,6 +585,7 @@ export class ToolManager { | |||
| 585 | chat_completion_sources.COHERE, | 585 | chat_completion_sources.COHERE, |
| 586 | chat_completion_sources.DEEPSEEK, | 586 | chat_completion_sources.DEEPSEEK, |
| 587 | chat_completion_sources.MAKERSUITE, | 587 | chat_completion_sources.MAKERSUITE, |
| 588 | chat_completion_sources.AI21, | ||
| 588 | ]; | 589 | ]; |
| 589 | return supportedSources.includes(oai_settings.chat_completion_source); | 590 | return supportedSources.includes(oai_settings.chat_completion_source); |
| 590 | } | 591 | } |
| @@ -499,6 +499,12 @@ async function sendMakerSuiteRequest(request, response) { | |||
| 499 | async function sendAI21Request(request, response) { | 499 | async function sendAI21Request(request, response) { |
| 500 | if (!request.body) return response.sendStatus(400); | 500 | if (!request.body) return response.sendStatus(400); |
| 501 | 501 | ||
| 502 | const apiKey = readSecret(request.user.directories, SECRET_KEYS.AI21); | ||
| 503 | if (!apiKey) { | ||
| 504 | console.warn('AI21 API key is missing.'); | ||
| 505 | return response.status(400).send({ error: true }); | ||
| 506 | } | ||
| 507 | |||
| 502 | const controller = new AbortController(); | 508 | const controller = new AbortController(); |
| 503 | console.debug(request.body.messages); | 509 | console.debug(request.body.messages); |
| 504 | request.socket.removeAllListeners('close'); | 510 | request.socket.removeAllListeners('close'); |
| @@ -514,13 +520,14 @@ async function sendAI21Request(request, response) { | |||
| 514 | top_p: request.body.top_p, | 520 | top_p: request.body.top_p, |
| 515 | stop: request.body.stop, | 521 | stop: request.body.stop, |
| 516 | stream: request.body.stream, | 522 | stream: request.body.stream, |
| 523 | tools: request.body.tools, | ||
| 517 | }; | 524 | }; |
| 518 | const options = { | 525 | const options = { |
| 519 | method: 'POST', | 526 | method: 'POST', |
| 520 | headers: { | 527 | headers: { |
| 521 | accept: 'application/json', | 528 | accept: 'application/json', |
| 522 | 'content-type': 'application/json', | 529 | 'content-type': 'application/json', |
| 523 | Authorization: `Bearer ${readSecret(request.user.directories, SECRET_KEYS.AI21)}`, | 530 | Authorization: `Bearer ${apiKey}`, |
| 524 | }, | 531 | }, |
| 525 | body: JSON.stringify(body), | 532 | body: JSON.stringify(body), |
| 526 | signal: controller.signal, | 533 | signal: controller.signal, |
| @@ -218,11 +218,13 @@ const toShallow = (character) => { | |||
| 218 | date_last_chat: character.date_last_chat, | 218 | date_last_chat: character.date_last_chat, |
| 219 | chat_size: character.chat_size, | 219 | chat_size: character.chat_size, |
| 220 | data_size: character.data_size, | 220 | data_size: character.data_size, |
| 221 | tags: character.tags, | ||
| 221 | data: { | 222 | data: { |
| 222 | name: _.get(character, 'data.name', ''), | 223 | name: _.get(character, 'data.name', ''), |
| 223 | character_version: _.get(character, 'data.character_version', ''), | 224 | character_version: _.get(character, 'data.character_version', ''), |
| 224 | creator: _.get(character, 'data.creator', ''), | 225 | creator: _.get(character, 'data.creator', ''), |
| 225 | creator_notes: _.get(character, 'data.creator_notes', ''), | 226 | creator_notes: _.get(character, 'data.creator_notes', ''), |
| 227 | tags: _.get(character, 'data.tags', []), | ||
| 226 | extensions: { | 228 | extensions: { |
| 227 | fav: _.get(character, 'data.extensions.fav', false), | 229 | fav: _.get(character, 'data.extensions.fav', false), |
| 228 | }, | 230 | }, |
| @@ -4,7 +4,7 @@ import storage from 'node-persist'; | |||
| 4 | import express from 'express'; | 4 | import express from 'express'; |
| 5 | import lodash from 'lodash'; | 5 | import lodash from 'lodash'; |
| 6 | import { jsonParser } from '../express-common.js'; | 6 | import { jsonParser } from '../express-common.js'; |
| 7 | import { checkForNewContent } from './content-manager.js'; | 7 | import { checkForNewContent, CONTENT_TYPES } from './content-manager.js'; |
| 8 | import { | 8 | import { |
| 9 | KEY_PREFIX, | 9 | KEY_PREFIX, |
| 10 | toKey, | 10 | toKey, |
| @@ -195,7 +195,7 @@ router.post('/create', requireAdminMiddleware, jsonParser, async (request, respo | |||
| 195 | console.info('Creating data directories for', newUser.handle); | 195 | console.info('Creating data directories for', newUser.handle); |
| 196 | await ensurePublicDirectoriesExist(); | 196 | await ensurePublicDirectoriesExist(); |
| 197 | const directories = getUserDirectories(newUser.handle); | 197 | const directories = getUserDirectories(newUser.handle); |
| 198 | await checkForNewContent([directories]); | 198 | await checkForNewContent([directories], [CONTENT_TYPES.SETTINGS]); |
| 199 | return response.json({ handle: newUser.handle }); | 199 | return response.json({ handle: newUser.handle }); |
| 200 | } catch (error) { | 200 | } catch (error) { |
| 201 | console.error('User create failed:', error); | 201 | console.error('User create failed:', error); |
| @@ -31,6 +31,7 @@ const SOURCES = [ | |||
| 31 | 'ollama', | 31 | 'ollama', |
| 32 | 'llamacpp', | 32 | 'llamacpp', |
| 33 | 'vllm', | 33 | 'vllm', |
| 34 | 'webllm', | ||
| 34 | ]; | 35 | ]; |
| 35 | 36 | ||
| 36 | /** | 37 | /** |
| @@ -64,6 +65,8 @@ async function getVector(source, sourceSettings, text, isQuery, directories) { | |||
| 64 | return getVllmVector(text, sourceSettings.apiUrl, sourceSettings.model, directories); | 65 | return getVllmVector(text, sourceSettings.apiUrl, sourceSettings.model, directories); |
| 65 | case 'ollama': | 66 | case 'ollama': |
| 66 | return getOllamaVector(text, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories); | 67 | return getOllamaVector(text, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories); |
| 68 | case 'webllm': | ||
| 69 | return sourceSettings.embeddings[text]; | ||
| 67 | } | 70 | } |
| 68 | 71 | ||
| 69 | throw new Error(`Unknown vector source ${source}`); | 72 | throw new Error(`Unknown vector source ${source}`); |
| @@ -114,6 +117,9 @@ async function getBatchVector(source, sourceSettings, texts, isQuery, directorie | |||
| 114 | case 'ollama': | 117 | case 'ollama': |
| 115 | results.push(...await getOllamaBatchVector(batch, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories)); | 118 | results.push(...await getOllamaBatchVector(batch, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories)); |
| 116 | break; | 119 | break; |
| 120 | case 'webllm': | ||
| 121 | results.push(...texts.map(x => sourceSettings.embeddings[x])); | ||
| 122 | break; | ||
| 117 | default: | 123 | default: |
| 118 | throw new Error(`Unknown vector source ${source}`); | 124 | throw new Error(`Unknown vector source ${source}`); |
| 119 | } | 125 | } |
| @@ -179,6 +185,11 @@ function getSourceSettings(source, request) { | |||
| 179 | return { | 185 | return { |
| 180 | model: 'nomic-embed-text-v1.5', | 186 | model: 'nomic-embed-text-v1.5', |
| 181 | }; | 187 | }; |
| 188 | case 'webllm': | ||
| 189 | return { | ||
| 190 | model: String(request.body.model), | ||
| 191 | embeddings: request.body.embeddings ?? {}, | ||
| 192 | }; | ||
| 182 | default: | 193 | default: |
| 183 | return {}; | 194 | return {}; |
| 184 | } | 195 | } |