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 @@
19621962 </span>
19631963 </div>
19641964 </div>
19651965 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek,makersuite,ai21">
19661966 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
19671967 <input id="openai_function_calling" type="checkbox" />
19681968 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -3078,7 +3078,15 @@
30783078 <div>
30793079 <h4 data-i18n="AI21 Model">AI21 Model</h4>
30803080 <select id="model_ai21_select">
30813081 <optgroup label="Jamba 1.5(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)">
30823090 <option value="jamba-1.5-mini">jamba-1.5-mini</option>
30833091 <option value="jamba-1.5-large">jamba-1.5-large</option>
30843092 </optgroup>
public/script.js+16 -9
@@ -271,7 +271,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
271271import { initBulkEdit } from './scripts/bulk-edit.js';
272272import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
273273import { getContext } from './scripts/st-context.js';
274274import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
275275import { accountStorage } from './scripts/util/AccountStorage.js';
276276
277277// API OBJECT FOR EXTERNAL WIRING
@@ -3346,15 +3346,18 @@ class StreamingProcessor {
33463346
33473347 if (Array.isArray(this.swipes) && this.swipes.length > 0) {
33483348 const message = chat[messageId];
3349+ const swipeInfoExtra = structuredClone(message.extra ?? {});
3350+ delete swipeInfoExtra.token_count;
3351+ delete swipeInfoExtra.reasoning;
3352+ delete swipeInfoExtra.reasoning_duration;
33493353 const swipeInfo = {
33503354 send_date: message.send_date,
33513355 gen_started: message.gen_started,
33523356 gen_finished: message.gen_finished,
33533357 extra: structuredClone(message.extra)swipeInfoExtra,
33543358 };
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);
33583361 chat[messageId].swipes.push(...this.swipes);
33593362 chat[messageId].swipe_info.push(...swipeInfoArray);
33603363 }
@@ -3366,6 +3369,7 @@ class StreamingProcessor {
33663369 await eventSource.emit(event_types.IMPERSONATE_READY, text);
33673370 }
33683371
3372+ syncMesToSwipe(messageId);
33693373 saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage);
33703374 await saveChatConditional();
33713375 unblockGeneration();
@@ -6117,15 +6121,18 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
61176121 }
61186122
61196123 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;
61206128 const swipeInfo = {
61216129 send_date: item.send_date,
61226130 gen_started: item.gen_started,
61236131 gen_finished: item.gen_finished,
61246132 extra: structuredClone(item.extra)swipeInfoExtra,
61256133 };
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);
61296136 item.swipes.push(...swipes);
61306137 item.swipe_info.push(...swipeInfoArray);
61316138 }
public/scripts/extensions.js+1 -1
@@ -1070,7 +1070,7 @@ export async function installExtension(url, global) {
10701070 toastr.success(t`Extension '${response.display_name}' by ${response.author} (version ${response.version}) has been installed successfully!`, t`Extension installation successful`);
10711071 console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`);
10721072 await loadExtensionSettings({}, false, false);
10731073 await eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED, response);
10741074}
10751075
10761076/**
public/scripts/extensions/vectors/index.js+135 -9
@@ -19,6 +19,7 @@ import {
1919 modules,
2020 renderExtensionTemplateAsync,
2121 doExtrasFetch, getApiUrl,
22+ openThirdPartyExtensionMenu,
2223} from '../../extensions.js';
2324import { collapseNewlines, registerDebugFunction } from '../../power-user.js';
2425import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
@@ -34,6 +35,7 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm
3435import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
3536import { callGenericPopup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';
3637import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
38+import { WebLlmVectorProvider } from './webllm.js';
3739
3840/**
3941 * @typedef {object} HashedMessage
@@ -60,6 +62,7 @@ const settings = {
6062 ollama_model: 'mxbai-embed-large',
6163 ollama_keep: false,
6264 vllm_model: '',
65+ webllm_model: '',
6366 summarize: false,
6467 summarize_sent: false,
6568 summary_source: 'main',
@@ -103,7 +106,7 @@ const settings = {
103106};
104107
105108const moduleWorker = new ModuleWorkerWrapper(synchronizeChat);
106-
109+const webllmProvider = new WebLlmVectorProvider();
107110const cachedSummaries = new Map();
108111
109112/**
@@ -373,6 +376,8 @@ async function synchronizeChat(batchSize = 5) {
373376 return 'Vectorization Source Model is required, but not set.';
374377 case 'extras_module_missing':
375378 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.';
376381 default:
377382 return 'Check server console for more details';
378383 }
@@ -747,14 +752,15 @@ async function getQueryText(chat, initiator) {
747752
748753/**
749754 * Gets common body parameters for vector requests.
750755 * @returnsparam {object} args Additional arguments
756+ * @returns {object} Request body
751757 */
752758function getVectorsRequestBody(args = {}) {
753759 const body = Object.assign({}, args);
754760 switch (settings.source) {
755761 case 'extras':
756762 body.extrasUrl = extension_settings.apiUrl;
757763 body.extrasKey = extension_settings.apiKey;
758764 break;
759765 case 'togetherai':
760766 body.model = extension_settings.vectors.togetherai_model;
@@ -777,6 +783,9 @@ function getVectorsRequestBody() {
777783 body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.VLLM];
778784 body.model = extension_settings.vectors.vllm_model;
779785 break;
786+ case 'webllm':
787+ body.model = extension_settings.vectors.webllm_model;
788+ break;
780789 default:
781790 break;
782791 }
@@ -784,6 +793,21 @@ function getVectorsRequestBody() {
784793}
785794
786795/**
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+/**
787811 * Gets the saved hashes for a collection
788812* @param {string} collectionId
789813* @returns {Promise<number[]>} Saved hashes
@@ -816,11 +840,12 @@ async function getSavedHashes(collectionId) {
816840async function insertVectorItems(collectionId, items) {
817841 throwIfSourceInvalid();
818842
843+ const args = await getAdditionalArgs(items.map(x => x.text));
819844 const response = await fetch('/api/vector/insert', {
820845 method: 'POST',
821846 headers: getRequestHeaders(),
822847 body: JSON.stringify({
823848 ...getVectorsRequestBody(args),
824849 collectionId: collectionId,
825850 items: items,
826851 source: settings.source,
@@ -858,6 +883,10 @@ function throwIfSourceInvalid() {
858883 if (settings.source === 'extras' && !modules.includes('embeddings')) {
859884 throw new Error('Vectors: Embeddings module missing', { cause: 'extras_module_missing' });
860885 }
886+
887+ if (settings.source === 'webllm' && (!isWebLlmSupported() || !settings.webllm_model)) {
888+ throw new Error('Vectors: WebLLM is not supported', { cause: 'webllm_not_supported' });
889+ }
861890}
862891
863892/**
@@ -890,11 +919,12 @@ async function deleteVectorItems(collectionId, hashes) {
890919 * @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results
891920 */
892921async function queryCollection(collectionId, searchText, topK) {
922+ const args = await getAdditionalArgs([searchText]);
893923 const response = await fetch('/api/vector/query', {
894924 method: 'POST',
895925 headers: getRequestHeaders(),
896926 body: JSON.stringify({
897927 ...getVectorsRequestBody(args),
898928 collectionId: collectionId,
899929 searchText: searchText,
900930 topK: topK,
@@ -919,11 +949,12 @@ async function queryCollection(collectionId, searchText, topK) {
919949 * @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs
920950 */
921951async function queryMultipleCollections(collectionIds, searchText, topK, threshold) {
952+ const args = await getAdditionalArgs([searchText]);
922953 const response = await fetch('/api/vector/query-multi', {
923954 method: 'POST',
924955 headers: getRequestHeaders(),
925956 body: JSON.stringify({
926957 ...getVectorsRequestBody(args),
927958 collectionIds: collectionIds,
928959 searchText: searchText,
929960 topK: topK,
@@ -1039,6 +1070,72 @@ function toggleSettings() {
10391070 $('#llamacpp_vectorsModel').toggle(settings.source === 'llamacpp');
10401071 $('#vllm_vectorsModel').toggle(settings.source === 'vllm');
10411072 $('#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+ });
10421139}
10431140
10441141async function onPurgeClick() {
@@ -1567,6 +1664,30 @@ jQuery(async () => {
15671664 $('#dialogue_popup_input').val(presetModel);
15681665 });
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+
15701691 $('#api_key_nomicai').toggleClass('success', !!secret_state[SECRET_KEYS.NOMICAI]);
15711692
15721693 toggleSettings();
@@ -1578,6 +1699,11 @@ jQuery(async () => {
15781699 eventSource.on(event_types.CHAT_DELETED, purgeVectorIndex);
15791700 eventSource.on(event_types.GROUP_CHAT_DELETED, purgeVectorIndex);
15801701 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
15821708 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
15831709 name: 'db-ingest',
public/scripts/extensions/vectors/settings.html+16 -0
@@ -21,8 +21,24 @@
2121 <option value="openai">OpenAI</option>
2222 <option value="togetherai">TogetherAI</option>
2323 <option value="vllm">vLLM</option>
24+ <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option>
2425 </select>
2526 </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>
2642 <div class="flex-container flexFlowColumn" id="ollama_vectorsModel">
2743 <label for="vectors_ollama_model" data-i18n="Vectorization Model">
2844 Vectorization Model
public/scripts/extensions/vectors/webllm.js+64 -0
@@ -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+}
public/scripts/openai.js+11 -7
@@ -337,7 +337,7 @@ const default_settings = {
337337 openai_model: 'gpt-4-turbo',
338338 claude_model: 'claude-3-5-sonnet-20240620',
339339 google_model: 'gemini-1.5-pro',
340340 ai21_model: 'jamba-1.56-large',
341341 mistralai_model: 'mistral-large-latest',
342342 cohere_model: 'command-r-plus',
343343 perplexity_model: 'sonar-pro',
@@ -417,7 +417,7 @@ const oai_settings = {
417417 openai_model: 'gpt-4-turbo',
418418 claude_model: 'claude-3-5-sonnet-20240620',
419419 google_model: 'gemini-1.5-pro',
420420 ai21_model: 'jamba-1.56-large',
421421 mistralai_model: 'mistral-large-latest',
422422 cohere_model: 'command-r-plus',
423423 perplexity_model: 'sonar-pro',
@@ -2027,12 +2027,16 @@ async function sendOpenAIRequest(type, messages, signal) {
20272027 generate_data['logprobs'] = 5;
20282028 }
20292029
20302030 // Remove logit bias, /logprobs and /stop -strings if it's 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)) {
20322033 delete generate_data.logit_bias;
20332034 delete generate_data.stop;
20342035 delete generate_data.logprobs;
20352036 }
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
20372041 if (isClaude) {
20382042 generate_data['top_k'] = Number(oai_settings.top_k_openai);
@@ -3251,7 +3255,7 @@ function loadOpenAISettings(data, settings) {
32513255 }
32523256
32533257 if (oai_settings.ai21_model.startsWith('j2-')) {
32543258 oai_settings.ai21_model = 'jamba-1.56-large';
32553259 }
32563260
32573261 if (settings.wrap_in_quotes !== undefined) oai_settings.wrap_in_quotes = !!settings.wrap_in_quotes;
@@ -4208,7 +4212,7 @@ async function onModelChange() {
42084212
42094213 if ($(this).is('#model_ai21_select')) {
42104214 if (value === '' || value.startsWith('j2-')) {
42114215 value = 'jamba-1.56-large';
42124216 $('#model_ai21_select').val(value);
42134217 }
42144218
@@ -4485,7 +4489,7 @@ async function onModelChange() {
44854489 if (oai_settings.chat_completion_source == chat_completion_sources.AI21) {
44864490 if (oai_settings.max_context_unlocked) {
44874491 $('#openai_max_context').attr('max', unlocked_max);
44884492 } else if (oai_settings.ai21_model.includes('jamba-1.5') || oai_settings.ai21_model.includesstartsWith('jamba-instruct')) {
44894493 $('#openai_max_context').attr('max', max_256k);
44904494 }
44914495
public/scripts/reasoning.js+26 -0
@@ -1104,6 +1104,32 @@ function parseReasoningFromString(str, { strict = true } = {}) {
11041104 }
11051105}
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+ */
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+
11071133function registerReasoningAppEvents() {
11081134 const eventHandler = (/** @type {string} */ type, /** @type {number} */ idx) => {
11091135 if (!power_user.reasoning.auto_parse) {
public/scripts/tool-calling.js+1 -0
@@ -585,6 +585,7 @@ export class ToolManager {
585585 chat_completion_sources.COHERE,
586586 chat_completion_sources.DEEPSEEK,
587587 chat_completion_sources.MAKERSUITE,
588+ chat_completion_sources.AI21,
588589 ];
589590 return supportedSources.includes(oai_settings.chat_completion_source);
590591 }
src/endpoints/backends/chat-completions.js+8 -1
@@ -499,6 +499,12 @@ async function sendMakerSuiteRequest(request, response) {
499499async function sendAI21Request(request, response) {
500500 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+
502508 const controller = new AbortController();
503509 console.debug(request.body.messages);
504510 request.socket.removeAllListeners('close');
@@ -514,13 +520,14 @@ async function sendAI21Request(request, response) {
514520 top_p: request.body.top_p,
515521 stop: request.body.stop,
516522 stream: request.body.stream,
523+ tools: request.body.tools,
517524 };
518525 const options = {
519526 method: 'POST',
520527 headers: {
521528 accept: 'application/json',
522529 'content-type': 'application/json',
523530 Authorization: `Bearer ${readSecret(request.user.directories, SECRET_KEYS.AI21)apiKey}`,
524531 },
525532 body: JSON.stringify(body),
526533 signal: controller.signal,
src/endpoints/characters.js+2 -0
@@ -218,11 +218,13 @@ const toShallow = (character) => {
218218 date_last_chat: character.date_last_chat,
219219 chat_size: character.chat_size,
220220 data_size: character.data_size,
221+ tags: character.tags,
221222 data: {
222223 name: _.get(character, 'data.name', ''),
223224 character_version: _.get(character, 'data.character_version', ''),
224225 creator: _.get(character, 'data.creator', ''),
225226 creator_notes: _.get(character, 'data.creator_notes', ''),
227+ tags: _.get(character, 'data.tags', []),
226228 extensions: {
227229 fav: _.get(character, 'data.extensions.fav', false),
228230 },
src/endpoints/users-admin.js+2 -2
@@ -4,7 +4,7 @@ import storage from 'node-persist';
44import express from 'express';
55import lodash from 'lodash';
66import { jsonParser } from '../express-common.js';
77import { checkForNewContent, CONTENT_TYPES } from './content-manager.js';
88import {
99 KEY_PREFIX,
1010 toKey,
@@ -195,7 +195,7 @@ router.post('/create', requireAdminMiddleware, jsonParser, async (request, respo
195195 console.info('Creating data directories for', newUser.handle);
196196 await ensurePublicDirectoriesExist();
197197 const directories = getUserDirectories(newUser.handle);
198198 await checkForNewContent([directories], [CONTENT_TYPES.SETTINGS]);
199199 return response.json({ handle: newUser.handle });
200200 } catch (error) {
201201 console.error('User create failed:', error);
src/endpoints/vectors.js+11 -0
@@ -31,6 +31,7 @@ const SOURCES = [
3131 'ollama',
3232 'llamacpp',
3333 'vllm',
34+ 'webllm',
3435];
3536
3637/**
@@ -64,6 +65,8 @@ async function getVector(source, sourceSettings, text, isQuery, directories) {
6465 return getVllmVector(text, sourceSettings.apiUrl, sourceSettings.model, directories);
6566 case 'ollama':
6667 return getOllamaVector(text, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories);
68+ case 'webllm':
69+ return sourceSettings.embeddings[text];
6770 }
6871
6972 throw new Error(`Unknown vector source ${source}`);
@@ -114,6 +117,9 @@ async function getBatchVector(source, sourceSettings, texts, isQuery, directorie
114117 case 'ollama':
115118 results.push(...await getOllamaBatchVector(batch, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories));
116119 break;
120+ case 'webllm':
121+ results.push(...texts.map(x => sourceSettings.embeddings[x]));
122+ break;
117123 default:
118124 throw new Error(`Unknown vector source ${source}`);
119125 }
@@ -179,6 +185,11 @@ function getSourceSettings(source, request) {
179185 return {
180186 model: 'nomic-embed-text-v1.5',
181187 };
188+ case 'webllm':
189+ return {
190+ model: String(request.body.model),
191+ embeddings: request.body.embeddings ?? {},
192+ };
182193 default:
183194 return {};
184195 }