Merge branch 'staging' into continue-from-reasoning

96d79ac4e963f7dcab07929b7162737720f3862b

Cohee <18619528+Cohee1207@users.noreply.github.com>

13 files changed, +303 -31Ignore whitespace
public/index.html+10 -2
@@ -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>
public/script.js+16 -9
@@ -271,7 +271,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
271import { initBulkEdit } from './scripts/bulk-edit.js';271import { initBulkEdit } from './scripts/bulk-edit.js';
272import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';272import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
273import { getContext } from './scripts/st-context.js';273import { getContext } from './scripts/st-context.js';
274import { extractReasoningFromData, initReasoning, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';274import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
275import { accountStorage } from './scripts/util/AccountStorage.js';275import { accountStorage } from './scripts/util/AccountStorage.js';
276276
277// API OBJECT FOR EXTERNAL WIRING277// API OBJECT FOR EXTERNAL WIRING
@@ -3346,15 +3346,18 @@ class StreamingProcessor {
33463346
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 }
33683371
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 }
61186122
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 }
public/scripts/extensions.js+1 -1
@@ -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}
10751075
1076/**1076/**
public/scripts/extensions/vectors/index.js+135 -9
@@ -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';
23import { collapseNewlines, registerDebugFunction } from '../../power-user.js';24import { collapseNewlines, registerDebugFunction } from '../../power-user.js';
24import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';25import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
@@ -34,6 +35,7 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm
34import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';35import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
35import { callGenericPopup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';36import { callGenericPopup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';
36import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';37import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
38import { WebLlmVectorProvider } from './webllm.js';
3739
38/**40/**
39 * @typedef {object} HashedMessage41 * @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};
104107
105const moduleWorker = new ModuleWorkerWrapper(synchronizeChat);108const moduleWorker = new ModuleWorkerWrapper(synchronizeChat);
106109const webllmProvider = new WebLlmVectorProvider();
107const cachedSummaries = new Map();110const cachedSummaries = new Map();
108111
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) {
747752
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 */
752function getVectorsRequestBody() {758function 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}
785794
786/**795/**
796 * Gets additional arguments for vector requests.
797 * @param {string[]} items Items to embed
798 * @returns {Promise<object>} Additional arguments
799 */
800async 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 collection811 * Gets the saved hashes for a collection
788* @param {string} collectionId812* @param {string} collectionId
789* @returns {Promise<number[]>} Saved hashes813* @returns {Promise<number[]>} Saved hashes
@@ -816,11 +840,12 @@ async function getSavedHashes(collectionId) {
816async function insertVectorItems(collectionId, items) {840async function insertVectorItems(collectionId, items) {
817 throwIfSourceInvalid();841 throwIfSourceInvalid();
818842
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}
862891
863/**892/**
@@ -890,11 +919,12 @@ async function deleteVectorItems(collectionId, hashes) {
890 * @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results919 * @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results
891 */920 */
892async function queryCollection(collectionId, searchText, topK) {921async 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 IDs949 * @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs
920 */950 */
921async function queryMultipleCollections(collectionIds, searchText, topK, threshold) {951async 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 */
1085async 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 */
1108function 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 */
1130async 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}
10431140
1044async function onPurgeClick() {1141async function onPurgeClick() {
@@ -1567,6 +1664,30 @@ jQuery(async () => {
1567 $('#dialogue_popup_input').val(presetModel);1664 $('#dialogue_popup_input').val(presetModel);
1568 });1665 });
15691666
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]);
15711692
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 });
15811707
1582 SlashCommandParser.addCommandObject(SlashCommand.fromProps({1708 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1583 name: 'db-ingest',1709 name: 'db-ingest',
public/scripts/extensions/vectors/settings.html+16 -0
@@ -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 Model44 Vectorization Model
public/scripts/extensions/vectors/webllm.js+64 -0
@@ -0,0 +1,64 @@
1export 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}
public/scripts/openai.js+11 -7
@@ -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 }
20292029
2030 // Remove logit bias, logprobs and stop strings if it's not supported by the model2030 // 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 }
20362040
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 }
32523256
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 }
32563260
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() {
42084212
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 }
42144218
@@ -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 }
44914495
public/scripts/reasoning.js+26 -0
@@ -1104,6 +1104,32 @@ function parseReasoningFromString(str, { strict = true } = {}) {
1104 }1104 }
1105}1105}
11061106
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 */
1113export 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
1107function registerReasoningAppEvents() {1133function 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) {
public/scripts/tool-calling.js+1 -0
@@ -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 }
src/endpoints/backends/chat-completions.js+8 -1
@@ -499,6 +499,12 @@ async function sendMakerSuiteRequest(request, response) {
499async function sendAI21Request(request, response) {499async function sendAI21Request(request, response) {
500 if (!request.body) return response.sendStatus(400);500 if (!request.body) return response.sendStatus(400);
501501
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,
src/endpoints/characters.js+2 -0
@@ -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 },
src/endpoints/users-admin.js+2 -2
@@ -4,7 +4,7 @@ import storage from 'node-persist';
4import express from 'express';4import express from 'express';
5import lodash from 'lodash';5import lodash from 'lodash';
6import { jsonParser } from '../express-common.js';6import { jsonParser } from '../express-common.js';
7import { checkForNewContent } from './content-manager.js';7import { checkForNewContent, CONTENT_TYPES } from './content-manager.js';
8import {8import {
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);
src/endpoints/vectors.js+11 -0
@@ -31,6 +31,7 @@ const SOURCES = [
31 'ollama',31 'ollama',
32 'llamacpp',32 'llamacpp',
33 'vllm',33 'vllm',
34 'webllm',
34];35];
3536
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 }
6871
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 }