Merge branch 'staging' into geminiStructured

7fd0f3e2bfc9fbdfb6518c1fe526bf95d52e5109

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

27 files changed, +1385 -54Showing whitespace changes
default/config.yaml+8 -2
@@ -1,8 +1,6 @@
1# -- DATA CONFIGURATION --1# -- DATA CONFIGURATION --
2# Root directory for user data storage2# Root directory for user data storage
3dataRoot: ./data3dataRoot: ./data
4# The maximum amount of memory that parsed character cards can use in MB
5cardsCacheCapacity: 100
6# -- SERVER CONFIGURATION --4# -- SERVER CONFIGURATION --
7# Listen for incoming connections5# Listen for incoming connections
8listen: false6listen: false
@@ -135,6 +133,14 @@ thumbnails:
135 # Maximum thumbnail dimensions per type [width, height]133 # Maximum thumbnail dimensions per type [width, height]
136 dimensions: { 'bg': [160, 90], 'avatar': [96, 144] }134 dimensions: { 'bg': [160, 90], 'avatar': [96, 144] }
137135
136# PERFORMANCE-RELATED CONFIGURATION
137performance:
138 # Enables lazy loading of character cards. Improves performances with large card libraries.
139 # May have compatibility issues with some extensions.
140 lazyLoadCharacters: false
141 # The maximum amount of memory that parsed character cards can use. Set to 0 to disable memory caching.
142 memoryCacheCapacity: '100mb'
143
138# Allow secret keys exposure via API144# Allow secret keys exposure via API
139allowKeysExposure: false145allowKeysExposure: false
140# Skip new default content checks146# Skip new default content checks
index.d.ts+31 -3
@@ -1,8 +1,36 @@
1import { UserDirectoryList, User } from "./src/users";1import { EventEmitter } from 'node:events';
2import { CommandLineArguments } from "./src/command-line";2import { CsrfSyncedToken } from 'csrf-sync';
3import { CsrfSyncedToken } from "csrf-sync";3import { UserDirectoryList, User } from './src/users.js';
4import { CommandLineArguments } from './src/command-line.js';
5import { EVENT_NAMES } from './src/server-events.js';
6
7/**
8 * Event payload for SERVER_STARTED event.
9 */
10export interface ServerStartedEvent {
11 /**
12 * The URL the server is listening on.
13 */
14 url: URL;
15}
16
17/**
18 * Map of all server events to their payload types.
19 */
20export interface ServerEventMap {
21 [EVENT_NAMES.SERVER_STARTED]: [ServerStartedEvent];
22}
423
5declare global {24declare global {
25 declare namespace NodeJS {
26 export interface Process {
27 /**
28 * A global instance of the server events emitter.
29 */
30 serverEvents: EventEmitter<ServerEventMap>;
31 }
32 }
33
6 declare namespace CookieSessionInterfaces {34 declare namespace CookieSessionInterfaces {
7 export interface CookieSessionObject {35 export interface CookieSessionObject {
8 /**36 /**
package-lock.json+12 -0
@@ -21,6 +21,7 @@
21 "bing-translate-api": "^4.0.2",21 "bing-translate-api": "^4.0.2",
22 "body-parser": "^1.20.2",22 "body-parser": "^1.20.2",
23 "bowser": "^2.11.0",23 "bowser": "^2.11.0",
24 "bytes": "^3.1.2",
24 "chalk": "^5.4.1",25 "chalk": "^5.4.1",
25 "command-exists": "^1.2.9",26 "command-exists": "^1.2.9",
26 "compression": "^1.8.0",27 "compression": "^1.8.0",
@@ -83,6 +84,7 @@
83 },84 },
84 "devDependencies": {85 "devDependencies": {
85 "@types/archiver": "^6.0.3",86 "@types/archiver": "^6.0.3",
87 "@types/bytes": "^3.1.5",
86 "@types/command-exists": "^1.2.3",88 "@types/command-exists": "^1.2.3",
87 "@types/compression": "^1.7.5",89 "@types/compression": "^1.7.5",
88 "@types/cookie-parser": "^1.4.8",90 "@types/cookie-parser": "^1.4.8",
@@ -1105,6 +1107,13 @@
1105 "@types/node": "*"1107 "@types/node": "*"
1106 }1108 }
1107 },1109 },
1110 "node_modules/@types/bytes": {
1111 "version": "3.1.5",
1112 "resolved": "https://registry.npmjs.org/@types/bytes/-/bytes-3.1.5.tgz",
1113 "integrity": "sha512-VgZkrJckypj85YxEsEavcMmmSOIzkUHqWmM4CCyia5dc54YwsXzJ5uT4fYxBQNEXx+oF1krlhgCbvfubXqZYsQ==",
1114 "dev": true,
1115 "license": "MIT"
1116 },
1108 "node_modules/@types/cacheable-request": {1117 "node_modules/@types/cacheable-request": {
1109 "version": "6.0.3",1118 "version": "6.0.3",
1110 "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",1119 "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
@@ -3399,6 +3408,9 @@
3399 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",3408 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
3400 "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",3409 "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
3401 "license": "MIT",3410 "license": "MIT",
3411 "dependencies": {
3412 "get-intrinsic": "^1.2.4"
3413 },
3402 "engines": {3414 "engines": {
3403 "node": ">= 0.4"3415 "node": ">= 0.4"
3404 }3416 }
package.json+4 -1
@@ -11,6 +11,7 @@
11 "bing-translate-api": "^4.0.2",11 "bing-translate-api": "^4.0.2",
12 "body-parser": "^1.20.2",12 "body-parser": "^1.20.2",
13 "bowser": "^2.11.0",13 "bowser": "^2.11.0",
14 "bytes": "^3.1.2",
14 "chalk": "^5.4.1",15 "chalk": "^5.4.1",
15 "command-exists": "^1.2.9",16 "command-exists": "^1.2.9",
16 "compression": "^1.8.0",17 "compression": "^1.8.0",
@@ -92,7 +93,8 @@
92 "version": "1.12.12",93 "version": "1.12.12",
93 "scripts": {94 "scripts": {
94 "start": "node server.js",95 "start": "node server.js",
95 "debug": "node server.js --inspect",96 "debug": "node --inspect server.js",
97 "electron": "electron ./src/electron",
96 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",98 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
97 "start:bun": "bun server.js",99 "start:bun": "bun server.js",
98 "start:no-csrf": "node server.js --disableCsrf",100 "start:no-csrf": "node server.js --disableCsrf",
@@ -112,6 +114,7 @@
112 "main": "server.js",114 "main": "server.js",
113 "devDependencies": {115 "devDependencies": {
114 "@types/archiver": "^6.0.3",116 "@types/archiver": "^6.0.3",
117 "@types/bytes": "^3.1.5",
115 "@types/command-exists": "^1.2.3",118 "@types/command-exists": "^1.2.3",
116 "@types/compression": "^1.7.5",119 "@types/compression": "^1.7.5",
117 "@types/cookie-parser": "^1.4.8",120 "@types/cookie-parser": "^1.4.8",
post-install.js+5 -0
@@ -96,6 +96,11 @@ const keyMigrationMap = [
96 newKey: 'logging.minLogLevel',96 newKey: 'logging.minLogLevel',
97 migrate: (value) => value,97 migrate: (value) => value,
98 },98 },
99 {
100 oldKey: 'cardsCacheCapacity',
101 newKey: 'performance.memoryCacheCapacity',
102 migrate: (value) => `${value}mb`,
103 },
99 // uncomment one release after 1.12.13104 // uncomment one release after 1.12.13
100 /*105 /*
101 {106 {
public/index.html+12 -1
@@ -1951,7 +1951,18 @@
1951 </span>1951 </span>
1952 </div>1952 </div>
1953 </div>1953 </div>
1954 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek">1954 <div class="range-block" data-source="makersuite,openrouter">
1955 <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">
1956 <input id="openai_enable_web_search" type="checkbox" />
1957 <span data-i18n="Enable web search">Enable web search</span>
1958 </label>
1959 <div class="flexBasis100p toggle-description justifyLeft">
1960 <span>
1961 Use search capabilities provided by the backend.
1962 </span>
1963 </div>
1964 </div>
1965 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek,makersuite">
1955 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">1966 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
1956 <input id="openai_function_calling" type="checkbox" />1967 <input id="openai_function_calling" type="checkbox" />
1957 <span data-i18n="Enable function calling">Enable function calling</span>1968 <span data-i18n="Enable function calling">Enable function calling</span>
public/script.js+40 -3
@@ -453,6 +453,8 @@ export const event_types = {
453 MESSAGE_DELETED: 'message_deleted',453 MESSAGE_DELETED: 'message_deleted',
454 MESSAGE_UPDATED: 'message_updated',454 MESSAGE_UPDATED: 'message_updated',
455 MESSAGE_FILE_EMBEDDED: 'message_file_embedded',455 MESSAGE_FILE_EMBEDDED: 'message_file_embedded',
456 MESSAGE_REASONING_EDITED: 'message_reasoning_edited',
457 MESSAGE_REASONING_DELETED: 'message_reasoning_deleted',
456 MORE_MESSAGES_LOADED: 'more_messages_loaded',458 MORE_MESSAGES_LOADED: 'more_messages_loaded',
457 IMPERSONATE_READY: 'impersonate_ready',459 IMPERSONATE_READY: 'impersonate_ready',
458 CHAT_CHANGED: 'chat_id_changed',460 CHAT_CHANGED: 'chat_id_changed',
@@ -1780,9 +1782,7 @@ export async function getCharacters() {
1780 const response = await fetch('/api/characters/all', {1782 const response = await fetch('/api/characters/all', {
1781 method: 'POST',1783 method: 'POST',
1782 headers: getRequestHeaders(),1784 headers: getRequestHeaders(),
1783 body: JSON.stringify({1785 body: JSON.stringify({}),
1784 '': '',
1785 }),
1786 });1786 });
1787 if (response.ok === true) {1787 if (response.ok === true) {
1788 characters.splice(0, characters.length);1788 characters.splice(0, characters.length);
@@ -3678,6 +3678,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3678 setGenerationProgress(0);3678 setGenerationProgress(0);
3679 generation_started = new Date();3679 generation_started = new Date();
36803680
3681 // Prevent generation from shallow characters
3682 await unshallowCharacter(this_chid);
3683
3681 // Occurs every time, even if the generation is aborted due to slash commands execution3684 // Occurs every time, even if the generation is aborted due to slash commands execution
3682 await eventSource.emit(event_types.GENERATION_STARTED, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage }, dryRun);3685 await eventSource.emit(event_types.GENERATION_STARTED, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage }, dryRun);
36833686
@@ -6724,9 +6727,43 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t
6724 }6727 }
6725}6728}
67266729
6730/**
6731 * Loads all the data of a shallow character.
6732 * @param {string|undefined} characterId Array index
6733 * @returns {Promise<void>} Promise that resolves when the character is unshallowed
6734 */
6735export async function unshallowCharacter(characterId) {
6736 if (characterId === undefined) {
6737 console.warn('Undefined character cannot be unshallowed');
6738 return;
6739 }
6740
6741 /** @type {import('./scripts/char-data.js').v1CharData} */
6742 const character = characters[characterId];
6743 if (!character) {
6744 console.warn('Character not found:', characterId);
6745 return;
6746 }
6747
6748 // Character is not shallow
6749 if (!character.shallow) {
6750 return;
6751 }
6752
6753 const avatar = character.avatar;
6754 if (!avatar) {
6755 console.warn('Character has no avatar field:', characterId);
6756 return;
6757 }
6758
6759 await getOneCharacter(avatar);
6760}
6761
6727export async function getChat() {6762export async function getChat() {
6728 //console.log('/api/chats/get -- entered for -- ' + characters[this_chid].name);6763 //console.log('/api/chats/get -- entered for -- ' + characters[this_chid].name);
6729 try {6764 try {
6765 await unshallowCharacter(this_chid);
6766
6730 const response = await $.ajax({6767 const response = await $.ajax({
6731 type: 'POST',6768 type: 'POST',
6732 url: '/api/chats/get',6769 url: '/api/chats/get',
public/scripts/RossAscends-mods.js+4 -1
@@ -316,7 +316,10 @@ export async function favsToHotswap() {
316 const entities = getEntitiesList({ doFilter: false });316 const entities = getEntitiesList({ doFilter: false });
317 const container = $('#right-nav-panel .hotswap');317 const container = $('#right-nav-panel .hotswap');
318318
319 const favs = entities.filter(x => x.item.fav || x.item.fav == 'true');319 // Hard limit is required because even if all hotswaps don't fit the screen, their images would still be loaded
320 // 25 is roughly calculated as the maximum number of favs that can fit an ultrawide monitor with the default theme
321 const FAVS_LIMIT = 25;
322 const favs = entities.filter(x => x.item.fav || x.item.fav == 'true').slice(0, FAVS_LIMIT);
320323
321 //helpful instruction message if no characters are favorited324 //helpful instruction message if no characters are favorited
322 if (favs.length == 0) {325 if (favs.length == 0) {
public/scripts/char-data.js+1 -0
@@ -113,5 +113,6 @@
113 * @property {string} chat - name of the current chat file chat113 * @property {string} chat - name of the current chat file chat
114 * @property {string} avatar - file name of the avatar image (acts as a unique identifier)114 * @property {string} avatar - file name of the avatar image (acts as a unique identifier)
115 * @property {string} json_data - the full raw JSON data of the character115 * @property {string} json_data - the full raw JSON data of the character
116 * @property {boolean?} shallow - if the data is shallow (lazy-loaded)
116 */117 */
117export default 0;// now this file is a module118export default 0;// now this file is a module
public/scripts/extensions/translate/index.js+110 -10
@@ -11,6 +11,7 @@ import {
11} from '../../../script.js';11} from '../../../script.js';
12import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';12import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
13import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js';13import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js';
14import { updateReasoningUI } from '../../reasoning.js';
14import { findSecret, secret_state, writeSecret } from '../../secrets.js';15import { findSecret, secret_state, writeSecret } from '../../secrets.js';
15import { SlashCommand } from '../../slash-commands/SlashCommand.js';16import { SlashCommand } from '../../slash-commands/SlashCommand.js';
16import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';17import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
@@ -172,21 +173,38 @@ function loadSettings() {
172 showKeysButton();173 showKeysButton();
173}174}
174175
176/**
177 * Check if the swipe is being generated for a message.
178 * @param {string|number} messageId Message ID
179 * @returns {boolean} Whether the swipe is being generated
180 */
181function isGeneratingSwipe(messageId) {
182 return $(`#chat .mes[mesid="${messageId}"] .mes_text`).text() === '...';
183}
184
175async function translateImpersonate(text) {185async function translateImpersonate(text) {
176 const translatedText = await translate(text, extension_settings.translate.target_language);186 const translatedText = await translate(text, extension_settings.translate.target_language);
177 $('#send_textarea').val(translatedText);187 $('#send_textarea').val(translatedText);
178}188}
179189
190/**
191 * Translates the contents of an incoming message.
192 * @param {string | number} messageId Message ID
193 * @returns {Promise<void>}
194 */
180async function translateIncomingMessage(messageId) {195async function translateIncomingMessage(messageId) {
181 const context = getContext();196 const context = getContext();
182 const message = context.chat[messageId];197 const message = context.chat[messageId];
183198
199 if (!message) {
200 return;
201 }
202
184 if (typeof message.extra !== 'object') {203 if (typeof message.extra !== 'object') {
185 message.extra = {};204 message.extra = {};
186 }205 }
187206
188 // New swipe is being generated. Don't translate that207 if (isGeneratingSwipe(messageId)) {
189 if ($(`#chat .mes[mesid="${messageId}"] .mes_text`).text() == '...') {
190 return;208 return;
191 }209 }
192210
@@ -194,7 +212,36 @@ async function translateIncomingMessage(messageId) {
194 const translation = await translate(textToTranslate, extension_settings.translate.target_language);212 const translation = await translate(textToTranslate, extension_settings.translate.target_language);
195 message.extra.display_text = translation;213 message.extra.display_text = translation;
196214
197 updateMessageBlock(messageId, message);215 updateMessageBlock(Number(messageId), message);
216}
217
218/**
219 * Translates the reasoning of an incoming message.
220 * @param {string | number} messageId
221 * @returns {Promise<boolean>} translated or not
222 */
223async function translateIncomingMessageReasoning(messageId) {
224 const context = getContext();
225 const message = context.chat[messageId];
226
227 if (!message) {
228 return false;
229 }
230
231 if (typeof message.extra !== 'object') {
232 message.extra = {};
233 }
234
235 if (!message.extra.reasoning || isGeneratingSwipe(messageId)) {
236 return false;
237 }
238
239 const textToTranslate = substituteParams(message.extra.reasoning, context.name1, message.name);
240 const translation = await translate(textToTranslate, extension_settings.translate.target_language);
241 message.extra.reasoning_display_text = translation;
242
243 updateReasoningUI(Number(messageId));
244 return true;
198}245}
199246
200async function translateProviderOneRing(text, lang) {247async function translateProviderOneRing(text, lang) {
@@ -535,6 +582,7 @@ async function onTranslateChatClick() {
535 toastr.info(`${chat.length} message(s) queued for translation.`, 'Please wait...');582 toastr.info(`${chat.length} message(s) queued for translation.`, 'Please wait...');
536583
537 for (let i = 0; i < chat.length; i++) {584 for (let i = 0; i < chat.length; i++) {
585 await translateIncomingMessageReasoning(i);
538 await translateIncomingMessage(i);586 await translateIncomingMessage(i);
539 }587 }
540588
@@ -561,6 +609,7 @@ async function onTranslationsClearClick() {
561 for (const mes of chat) {609 for (const mes of chat) {
562 if (mes.extra) {610 if (mes.extra) {
563 delete mes.extra.display_text;611 delete mes.extra.display_text;
612 delete mes.extra.reasoning_display_text;
564 }613 }
565 }614 }
566615
@@ -573,12 +622,47 @@ async function translateMessageEdit(messageId) {
573 const chat = context.chat;622 const chat = context.chat;
574 const message = chat[messageId];623 const message = chat[messageId];
575624
576 if (message.is_system || extension_settings.translate.auto_mode == autoModeOptions.NONE) {625 let anyChange = false;
577 return;626 if (message.is_system || (extension_settings.translate.auto_mode == autoModeOptions.NONE && message.extra?.display_text)) {
627 delete message.extra.display_text;
628 updateMessageBlock(messageId, message);
629 anyChange = true;
630 } else if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) {
631 await translateIncomingMessage(messageId);
632 anyChange = true;
578 }633 }
579634
580 if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) {635 if (anyChange) {
581 await translateIncomingMessage(messageId);636 await context.saveChat();
637 }
638}
639
640async function translateMessageReasoningEdit(messageId) {
641 const context = getContext();
642 const chat = context.chat;
643 const message = chat[messageId];
644
645 let anyChange = false;
646 if (message.is_system || (extension_settings.translate.auto_mode == autoModeOptions.NONE && message.extra?.reasoning_display_text)) {
647 delete message.extra.reasoning_display_text;
648 updateReasoningUI(Number(messageId));
649 anyChange = true;
650 } else if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) {
651 anyChange = await translateIncomingMessageReasoning(messageId);
652 }
653
654 if (anyChange) {
655 await context.saveChat();
656 }
657}
658
659async function removeReasoningDisplayText(messageId) {
660 const context = getContext();
661 const message = context.chat[messageId];
662 if (message.extra?.reasoning_display_text) {
663 delete message.extra.reasoning_display_text;
664 updateReasoningUI(Number(messageId));
665 await context.saveChat();
582 }666 }
583}667}
584668
@@ -588,22 +672,36 @@ async function onMessageTranslateClick() {
588 const message = context.chat[messageId];672 const message = context.chat[messageId];
589673
590 // If the message is already translated, revert it back to the original text674 // If the message is already translated, revert it back to the original text
675 let alreadyTranslated = false;
591 if (message?.extra?.display_text) {676 if (message?.extra?.display_text) {
592 delete message.extra.display_text;677 delete message.extra.display_text;
593 updateMessageBlock(messageId, message);678 updateMessageBlock(Number(messageId), message);
679 alreadyTranslated = true;
594 }680 }
681 if (message?.extra?.reasoning_display_text) {
682 delete message.extra.reasoning_display_text;
683 updateReasoningUI(Number(messageId));
684 alreadyTranslated = true;
685 }
686
595 // If the message is not translated, translate it687 // If the message is not translated, translate it
596 else {688 if (!alreadyTranslated) {
689 await translateIncomingMessageReasoning(messageId);
597 await translateIncomingMessage(messageId);690 await translateIncomingMessage(messageId);
598 }691 }
599692
600 await context.saveChat();693 await context.saveChat();
601}694}
602695
603const handleIncomingMessage = createEventHandler(translateIncomingMessage, () => shouldTranslate(incomingTypes));696const handleIncomingMessage = createEventHandler(async (messageId) => {
697 await translateIncomingMessageReasoning(messageId);
698 await translateIncomingMessage(messageId);
699}, () => shouldTranslate(incomingTypes));
604const handleOutgoingMessage = createEventHandler(translateOutgoingMessage, () => shouldTranslate(outgoingTypes));700const handleOutgoingMessage = createEventHandler(translateOutgoingMessage, () => shouldTranslate(outgoingTypes));
605const handleImpersonateReady = createEventHandler(translateImpersonate, () => shouldTranslate(incomingTypes));701const handleImpersonateReady = createEventHandler(translateImpersonate, () => shouldTranslate(incomingTypes));
606const handleMessageEdit = createEventHandler(translateMessageEdit, () => true);702const handleMessageEdit = createEventHandler(translateMessageEdit, () => true);
703const handleMessageReasoningEdit = createEventHandler(translateMessageReasoningEdit, () => true);
704const handleMessageReasoningDelete = createEventHandler(removeReasoningDisplayText, () => true);
607705
608globalThis.translate = translate;706globalThis.translate = translate;
609707
@@ -717,6 +815,8 @@ jQuery(async () => {
717 eventSource.on(event_types.MESSAGE_SWIPED, handleIncomingMessage);815 eventSource.on(event_types.MESSAGE_SWIPED, handleIncomingMessage);
718 eventSource.on(event_types.IMPERSONATE_READY, handleImpersonateReady);816 eventSource.on(event_types.IMPERSONATE_READY, handleImpersonateReady);
719 eventSource.on(event_types.MESSAGE_UPDATED, handleMessageEdit);817 eventSource.on(event_types.MESSAGE_UPDATED, handleMessageEdit);
818 eventSource.on(event_types.MESSAGE_REASONING_EDITED, handleMessageReasoningEdit);
819 eventSource.on(event_types.MESSAGE_REASONING_DELETED, handleMessageReasoningDelete);
720820
721 document.body.classList.add('translate');821 document.body.classList.add('translate');
722822
public/scripts/group-chats.js+33 -4
@@ -72,6 +72,7 @@ import {
72 animation_duration,72 animation_duration,
73 depth_prompt_role_default,73 depth_prompt_role_default,
74 shouldAutoContinue,74 shouldAutoContinue,
75 unshallowCharacter,
75} from '../script.js';76} from '../script.js';
76import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';77import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';
77import { FILTER_TYPES, FilterHelper } from './filters.js';78import { FILTER_TYPES, FilterHelper } from './filters.js';
@@ -216,6 +217,7 @@ export async function getGroupChat(groupId, reload = false) {
216217
217 // Run validation before any loading218 // Run validation before any loading
218 validateGroup(group);219 validateGroup(group);
220 await unshallowGroupMembers(groupId);
219221
220 const chat_id = group.chat_id;222 const chat_id = group.chat_id;
221 const data = await loadGroupChat(chat_id);223 const data = await loadGroupChat(chat_id);
@@ -824,6 +826,8 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
824 }826 }
825827
826 try {828 try {
829 await unshallowGroupMembers(selected_group);
830
827 throwIfAborted();831 throwIfAborted();
828 hideSwipeButtons();832 hideSwipeButtons();
829 is_group_generating = true;833 is_group_generating = true;
@@ -1137,6 +1141,29 @@ export async function editGroup(id, immediately, reload = true) {
1137 saveGroupDebounced(group, reload);1141 saveGroupDebounced(group, reload);
1138}1142}
11391143
1144/**
1145 * Unshallows all definitions of group members.
1146 * @param {string} groupId Id of the group
1147 * @returns {Promise<void>} Promise that resolves when all group members are unshallowed
1148 */
1149export async function unshallowGroupMembers(groupId) {
1150 const group = groups.find(x => x.id == groupId);
1151 if (!group) {
1152 return;
1153 }
1154 const members = group.members;
1155 if (!Array.isArray(members)) {
1156 return;
1157 }
1158 for (const member of members) {
1159 const index = characters.findIndex(x => x.avatar === member);
1160 if (index === -1) {
1161 continue;
1162 }
1163 await unshallowCharacter(String(index));
1164 }
1165}
1166
1140let groupAutoModeAbortController = null;1167let groupAutoModeAbortController = null;
11411168
1142async function groupChatAutoModeWorker() {1169async function groupChatAutoModeWorker() {
@@ -1158,9 +1185,9 @@ async function groupChatAutoModeWorker() {
1158 await generateGroupWrapper(true, 'auto', { signal: groupAutoModeAbortController.signal });1185 await generateGroupWrapper(true, 'auto', { signal: groupAutoModeAbortController.signal });
1159}1186}
11601187
1161async function modifyGroupMember(chat_id, groupMember, isDelete) {1188async function modifyGroupMember(groupId, groupMember, isDelete) {
1162 const id = groupMember.data('id');1189 const id = groupMember.data('id');
1163 const thisGroup = groups.find((x) => x.id == chat_id);1190 const thisGroup = groups.find((x) => x.id == groupId);
1164 const membersArray = thisGroup?.members ?? newGroupMembers;1191 const membersArray = thisGroup?.members ?? newGroupMembers;
11651192
1166 if (isDelete) {1193 if (isDelete) {
@@ -1173,6 +1200,7 @@ async function modifyGroupMember(chat_id, groupMember, isDelete) {
1173 }1200 }
11741201
1175 if (openGroupId) {1202 if (openGroupId) {
1203 await unshallowGroupMembers(openGroupId);
1176 await editGroup(openGroupId, false, false);1204 await editGroup(openGroupId, false, false);
1177 updateGroupAvatar(thisGroup);1205 updateGroupAvatar(thisGroup);
1178 }1206 }
@@ -1638,7 +1666,7 @@ async function onGroupActionClick(event) {
1638 }1666 }
16391667
1640 if (action === 'view') {1668 if (action === 'view') {
1641 openCharacterDefinition(member);1669 await openCharacterDefinition(member);
1642 }1670 }
16431671
1644 if (action === 'speak') {1672 if (action === 'speak') {
@@ -1690,7 +1718,7 @@ export async function openGroupById(groupId) {
1690 return false;1718 return false;
1691}1719}
16921720
1693function openCharacterDefinition(characterSelect) {1721async function openCharacterDefinition(characterSelect) {
1694 if (is_group_generating) {1722 if (is_group_generating) {
1695 toastr.warning(t`Can't peek a character while group reply is being generated`);1723 toastr.warning(t`Can't peek a character while group reply is being generated`);
1696 console.warn('Can\'t peek a character def while group reply is being generated');1724 console.warn('Can\'t peek a character def while group reply is being generated');
@@ -1703,6 +1731,7 @@ function openCharacterDefinition(characterSelect) {
1703 return;1731 return;
1704 }1732 }
17051733
1734 await unshallowCharacter(chid);
1706 setCharacterId(chid);1735 setCharacterId(chid);
1707 select_selected_character(chid);1736 select_selected_character(chid);
1708 // Gentle nudge to recalculate tokens1737 // Gentle nudge to recalculate tokens
public/scripts/openai.js+12 -0
@@ -300,6 +300,7 @@ export const settingsToUpdate = {
300 function_calling: ['#openai_function_calling', 'function_calling', true],300 function_calling: ['#openai_function_calling', 'function_calling', true],
301 show_thoughts: ['#openai_show_thoughts', 'show_thoughts', true],301 show_thoughts: ['#openai_show_thoughts', 'show_thoughts', true],
302 reasoning_effort: ['#openai_reasoning_effort', 'reasoning_effort', false],302 reasoning_effort: ['#openai_reasoning_effort', 'reasoning_effort', false],
303 enable_web_search: ['#openai_enable_web_search', 'enable_web_search', true],
303 seed: ['#seed_openai', 'seed', false],304 seed: ['#seed_openai', 'seed', false],
304 n: ['#n_openai', 'n', false],305 n: ['#n_openai', 'n', false],
305 bypass_status_check: ['#openai_bypass_status_check', 'bypass_status_check', true],306 bypass_status_check: ['#openai_bypass_status_check', 'bypass_status_check', true],
@@ -380,6 +381,7 @@ const default_settings = {
380 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,381 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
381 show_thoughts: true,382 show_thoughts: true,
382 reasoning_effort: 'medium',383 reasoning_effort: 'medium',
384 enable_web_search: false,
383 seed: -1,385 seed: -1,
384 n: 1,386 n: 1,
385};387};
@@ -459,6 +461,7 @@ const oai_settings = {
459 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,461 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
460 show_thoughts: true,462 show_thoughts: true,
461 reasoning_effort: 'medium',463 reasoning_effort: 'medium',
464 enable_web_search: false,
462 seed: -1,465 seed: -1,
463 n: 1,466 n: 1,
464};467};
@@ -2000,6 +2003,7 @@ async function sendOpenAIRequest(type, messages, signal) {
2000 'group_names': getGroupNames(),2003 'group_names': getGroupNames(),
2001 'include_reasoning': Boolean(oai_settings.show_thoughts),2004 'include_reasoning': Boolean(oai_settings.show_thoughts),
2002 'reasoning_effort': String(oai_settings.reasoning_effort),2005 'reasoning_effort': String(oai_settings.reasoning_effort),
2006 'enable_web_search': Boolean(oai_settings.enable_web_search),
2003 };2007 };
20042008
2005 if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {2009 if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
@@ -3222,6 +3226,7 @@ function loadOpenAISettings(data, settings) {
3222 oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;3226 oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;
3223 oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts;3227 oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts;
3224 oai_settings.reasoning_effort = settings.reasoning_effort ?? default_settings.reasoning_effort;3228 oai_settings.reasoning_effort = settings.reasoning_effort ?? default_settings.reasoning_effort;
3229 oai_settings.enable_web_search = settings.enable_web_search ?? default_settings.enable_web_search;
3225 oai_settings.seed = settings.seed ?? default_settings.seed;3230 oai_settings.seed = settings.seed ?? default_settings.seed;
3226 oai_settings.n = settings.n ?? default_settings.n;3231 oai_settings.n = settings.n ?? default_settings.n;
32273232
@@ -3349,6 +3354,7 @@ function loadOpenAISettings(data, settings) {
3349 $('#seed_openai').val(oai_settings.seed);3354 $('#seed_openai').val(oai_settings.seed);
3350 $('#n_openai').val(oai_settings.n);3355 $('#n_openai').val(oai_settings.n);
3351 $('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts);3356 $('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts);
3357 $('#openai_enable_web_search').prop('checked', oai_settings.enable_web_search);
33523358
3353 $('#openai_reasoning_effort').val(oai_settings.reasoning_effort);3359 $('#openai_reasoning_effort').val(oai_settings.reasoning_effort);
3354 $(`#openai_reasoning_effort option[value="${oai_settings.reasoning_effort}"]`).prop('selected', true);3360 $(`#openai_reasoning_effort option[value="${oai_settings.reasoning_effort}"]`).prop('selected', true);
@@ -3613,6 +3619,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
3613 function_calling: settings.function_calling,3619 function_calling: settings.function_calling,
3614 show_thoughts: settings.show_thoughts,3620 show_thoughts: settings.show_thoughts,
3615 reasoning_effort: settings.reasoning_effort,3621 reasoning_effort: settings.reasoning_effort,
3622 enable_web_search: settings.enable_web_search,
3616 seed: settings.seed,3623 seed: settings.seed,
3617 n: settings.n,3624 n: settings.n,
3618 };3625 };
@@ -5572,6 +5579,11 @@ export function initOpenAI() {
5572 saveSettingsDebounced();5579 saveSettingsDebounced();
5573 });5580 });
55745581
5582 $('#openai_enable_web_search').on('input', function () {
5583 oai_settings.enable_web_search = !!$(this).prop('checked');
5584 saveSettingsDebounced();
5585 });
5586
5575 if (!CSS.supports('field-sizing', 'content')) {5587 if (!CSS.supports('field-sizing', 'content')) {
5576 $(document).on('input', '#openai_settings .autoSetHeight', function () {5588 $(document).on('input', '#openai_settings .autoSetHeight', function () {
5577 resetScrollHeight($(this));5589 resetScrollHeight($(this));
public/scripts/reasoning.js+13 -3
@@ -167,6 +167,8 @@ export class ReasoningHandler {
167 this.type = null;167 this.type = null;
168 /** @type {string} The reasoning output */168 /** @type {string} The reasoning output */
169 this.reasoning = '';169 this.reasoning = '';
170 /** @type {string?} The reasoning output display in case of translate or other */
171 this.reasoningDisplayText = null;
170 /** @type {Date} When the reasoning started */172 /** @type {Date} When the reasoning started */
171 this.startTime = null;173 this.startTime = null;
172 /** @type {Date} When the reasoning ended */174 /** @type {Date} When the reasoning ended */
@@ -234,6 +236,7 @@ export class ReasoningHandler {
234236
235 this.type = extra?.reasoning_type;237 this.type = extra?.reasoning_type;
236 this.reasoning = extra?.reasoning ?? '';238 this.reasoning = extra?.reasoning ?? '';
239 this.reasoningDisplayText = extra?.reasoning_display_text ?? null;
237240
238 if (this.state !== ReasoningState.None) {241 if (this.state !== ReasoningState.None) {
239 this.initialTime = new Date(chat[messageId].gen_started);242 this.initialTime = new Date(chat[messageId].gen_started);
@@ -249,6 +252,7 @@ export class ReasoningHandler {
249 this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;252 this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;
250 this.type = null;253 this.type = null;
251 this.reasoning = '';254 this.reasoning = '';
255 this.reasoningDisplayText = null;
252 this.initialTime = new Date();256 this.initialTime = new Date();
253 this.startTime = null;257 this.startTime = null;
254 this.endTime = null;258 this.endTime = null;
@@ -434,7 +438,7 @@ export class ReasoningHandler {
434 setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type);438 setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type);
435439
436 // Update the reasoning message440 // Update the reasoning message
437 const reasoning = trimSpaces(this.reasoning);441 const reasoning = trimSpaces(this.reasoningDisplayText ?? this.reasoning);
438 const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);442 const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
439 this.messageReasoningContentDom.innerHTML = displayReasoning;443 this.messageReasoningContentDom.innerHTML = displayReasoning;
440444
@@ -888,12 +892,17 @@ function setReasoningEventHandlers() {
888 }892 }
889893
890 const textarea = messageBlock.find('.reasoning_edit_textarea');894 const textarea = messageBlock.find('.reasoning_edit_textarea');
891 updateReasoningFromValue(message, String(textarea.val()));895 const newReasoning = String(textarea.val());
896 textarea.remove();
897 if (newReasoning === message.extra.reasoning) {
898 return;
899 }
900 updateReasoningFromValue(message, newReasoning);
892 await saveChatConditional();901 await saveChatConditional();
893 updateMessageBlock(messageId, message);902 updateMessageBlock(messageId, message);
894 textarea.remove();
895903
896 messageBlock.find('.mes_edit_done:visible').trigger('click');904 messageBlock.find('.mes_edit_done:visible').trigger('click');
905 await eventSource.emit(event_types.MESSAGE_REASONING_EDITED, messageId);
897 });906 });
898907
899 $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {908 $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
@@ -955,6 +964,7 @@ function setReasoningEventHandlers() {
955 updateMessageBlock(messageId, message);964 updateMessageBlock(messageId, message);
956 const textarea = messageBlock.find('.reasoning_edit_textarea');965 const textarea = messageBlock.find('.reasoning_edit_textarea');
957 textarea.remove();966 textarea.remove();
967 await eventSource.emit(event_types.MESSAGE_REASONING_DELETED, messageId);
958 });968 });
959969
960 $(document).on('pointerup', '.mes_reasoning_copy', async function () {970 $(document).on('pointerup', '.mes_reasoning_copy', async function () {
public/scripts/sse-stream.js+5 -0
@@ -137,9 +137,14 @@ async function* parseStreamData(json) {
137 else if (Array.isArray(json.candidates)) {137 else if (Array.isArray(json.candidates)) {
138 for (let i = 0; i < json.candidates.length; i++) {138 for (let i = 0; i < json.candidates.length; i++) {
139 const isNotPrimary = json.candidates?.[0]?.index > 0;139 const isNotPrimary = json.candidates?.[0]?.index > 0;
140 const hasToolCalls = json?.candidates?.[0]?.content?.parts?.some(p => p?.functionCall);
140 if (isNotPrimary || json.candidates.length === 0) {141 if (isNotPrimary || json.candidates.length === 0) {
141 return null;142 return null;
142 }143 }
144 if (hasToolCalls) {
145 yield { data: json, chunk: '' };
146 return;
147 }
143 if (typeof json.candidates[0].content === 'object' && Array.isArray(json.candidates[i].content.parts)) {148 if (typeof json.candidates[0].content === 'object' && Array.isArray(json.candidates[i].content.parts)) {
144 for (let j = 0; j < json.candidates[i].content.parts.length; j++) {149 for (let j = 0; j < json.candidates[i].content.parts.length; j++) {
145 if (typeof json.candidates[i].content.parts[j].text === 'string') {150 if (typeof json.candidates[i].content.parts[j].text === 'string') {
public/scripts/st-context.js+6 -1
@@ -47,6 +47,7 @@ import {
47 updateMessageBlock,47 updateMessageBlock,
48 printMessages,48 printMessages,
49 clearChat,49 clearChat,
50 unshallowCharacter,
50} from '../script.js';51} from '../script.js';
51import {52import {
52 extension_settings,53 extension_settings,
@@ -55,7 +56,7 @@ import {
55 renderExtensionTemplateAsync,56 renderExtensionTemplateAsync,
56 writeExtensionField,57 writeExtensionField,
57} from './extensions.js';58} from './extensions.js';
58import { groups, openGroupChat, selected_group } from './group-chats.js';59import { groups, openGroupChat, selected_group, unshallowGroupMembers } from './group-chats.js';
59import { addLocaleData, getCurrentLocale, t, translate } from './i18n.js';60import { addLocaleData, getCurrentLocale, t, translate } from './i18n.js';
60import { hideLoader, showLoader } from './loader.js';61import { hideLoader, showLoader } from './loader.js';
61import { MacrosParser } from './macros.js';62import { MacrosParser } from './macros.js';
@@ -78,6 +79,7 @@ import { timestampToMoment, uuidv4 } from './utils.js';
78import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';79import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
79import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';80import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
80import { ChatCompletionService, TextCompletionService } from './custom-request.js';81import { ChatCompletionService, TextCompletionService } from './custom-request.js';
82import { updateReasoningUI } from './reasoning.js';
8183
82export function getContext() {84export function getContext() {
83 return {85 return {
@@ -210,6 +212,9 @@ export function getContext() {
210 clearChat,212 clearChat,
211 ChatCompletionService,213 ChatCompletionService,
212 TextCompletionService,214 TextCompletionService,
215 updateReasoningUI,
216 unshallowCharacter,
217 unshallowGroupMembers,
213 };218 };
214}219}
215220
public/scripts/tool-calling.js+33 -0
@@ -506,6 +506,26 @@ export class ToolManager {
506 }506 }
507 }507 }
508 }508 }
509 if (Array.isArray(parsed?.candidates)) {
510 for (let choiceIndex = 0; choiceIndex < parsed.candidates.length; choiceIndex++) {
511 const candidate = parsed.candidates[choiceIndex];
512 if (Array.isArray(candidate?.content?.parts)) {
513 for (let toolCallIndex = 0; toolCallIndex < candidate.content.parts.length; toolCallIndex++) {
514 const part = candidate.content.parts[toolCallIndex];
515 if (part.functionCall) {
516 if (!Array.isArray(toolCalls[choiceIndex])) {
517 toolCalls[choiceIndex] = [];
518 }
519 if (toolCalls[choiceIndex][toolCallIndex] === undefined) {
520 toolCalls[choiceIndex][toolCallIndex] = {};
521 }
522 const targetToolCall = toolCalls[choiceIndex][toolCallIndex];
523 ToolManager.#applyToolCallDelta(targetToolCall, part.functionCall);
524 }
525 }
526 }
527 }
528 }
509 }529 }
510530
511 /**531 /**
@@ -564,6 +584,7 @@ export class ToolManager {
564 chat_completion_sources.GROQ,584 chat_completion_sources.GROQ,
565 chat_completion_sources.COHERE,585 chat_completion_sources.COHERE,
566 chat_completion_sources.DEEPSEEK,586 chat_completion_sources.DEEPSEEK,
587 chat_completion_sources.MAKERSUITE,
567 ];588 ];
568 return supportedSources.includes(oai_settings.chat_completion_source);589 return supportedSources.includes(oai_settings.chat_completion_source);
569 }590 }
@@ -585,8 +606,11 @@ export class ToolManager {
585 * @returns {any[]} Tool calls from the response data606 * @returns {any[]} Tool calls from the response data
586 */607 */
587 static #getToolCallsFromData(data) {608 static #getToolCallsFromData(data) {
609 const getRandomId = () => Math.random().toString(36).substring(2);
588 const isClaudeToolCall = c => Array.isArray(c) ? c.filter(x => x).every(isClaudeToolCall) : c?.input && c?.name && c?.id;610 const isClaudeToolCall = c => Array.isArray(c) ? c.filter(x => x).every(isClaudeToolCall) : c?.input && c?.name && c?.id;
611 const isGoogleToolCall = c => Array.isArray(c) ? c.filter(x => x).every(isGoogleToolCall) : c?.name && c?.args;
589 const convertClaudeToolCall = c => ({ id: c.id, function: { name: c.name, arguments: c.input } });612 const convertClaudeToolCall = c => ({ id: c.id, function: { name: c.name, arguments: c.input } });
613 const convertGoogleToolCall = (c) => ({ id: getRandomId(), function: { name: c.name, arguments: c.args } });
590614
591 // Parsed tool calls from streaming data615 // Parsed tool calls from streaming data
592 if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) {616 if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) {
@@ -594,6 +618,10 @@ export class ToolManager {
594 return data[0].filter(x => x).map(convertClaudeToolCall);618 return data[0].filter(x => x).map(convertClaudeToolCall);
595 }619 }
596620
621 if (isGoogleToolCall(data[0])) {
622 return data[0].filter(x => x).map(convertGoogleToolCall);
623 }
624
597 if (typeof data[0]?.[0]?.tool_calls === 'object') {625 if (typeof data[0]?.[0]?.tool_calls === 'object') {
598 return Array.isArray(data[0]?.[0]?.tool_calls) ? data[0][0].tool_calls : [data[0][0].tool_calls];626 return Array.isArray(data[0]?.[0]?.tool_calls) ? data[0][0].tool_calls : [data[0][0].tool_calls];
599 }627 }
@@ -601,6 +629,11 @@ export class ToolManager {
601 return data[0];629 return data[0];
602 }630 }
603631
632 // Google AI Studio tool calls
633 if (Array.isArray(data?.responseContent?.parts)) {
634 return data.responseContent.parts.filter(p => p.functionCall).map(p => convertGoogleToolCall(p.functionCall));
635 }
636
604 // Parsed tool calls from non-streaming data637 // Parsed tool calls from non-streaming data
605 if (Array.isArray(data?.choices)) {638 if (Array.isArray(data?.choices)) {
606 // Find a choice with 0-index639 // Find a choice with 0-index
server.js+2 -0
@@ -20,6 +20,7 @@ import bodyParser from 'body-parser';
20import open from 'open';20import open from 'open';
2121
22// local library imports22// local library imports
23import { serverEvents, EVENT_NAMES } from './src/server-events.js';
23import { CommandLineParser } from './src/command-line.js';24import { CommandLineParser } from './src/command-line.js';
24import { loadPlugins } from './src/plugin-loader.js';25import { loadPlugins } from './src/plugin-loader.js';
25import {26import {
@@ -348,6 +349,7 @@ async function postSetupTasks(result) {
348 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');349 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
349350
350 setupLogLevel();351 setupLogLevel();
352 serverEvents.emit(EVENT_NAMES.SERVER_STARTED, { url: autorunUrl });
351}353}
352354
353/**355/**
src/electron/Start.bat+6 -0
@@ -0,0 +1,6 @@
1@echo off
2pushd %~dp0
3call npm install --no-audit --no-fund --loglevel=error --no-progress --omit=dev
4npm run start server.js %*
5pause
6popd
src/electron/index.js+62 -0
@@ -0,0 +1,62 @@
1import { app, BrowserWindow } from 'electron';
2import path from 'path';
3import { fileURLToPath } from 'url';
4import yargs from 'yargs';
5import { serverEvents, EVENT_NAMES } from '../server-events.js';
6
7const cliArguments = yargs(process.argv)
8 .usage('Usage: <your-start-script> [options]')
9 .option('width', {
10 type: 'number',
11 default: 800,
12 describe: 'The width of the window',
13 })
14 .option('height', {
15 type: 'number',
16 default: 600,
17 describe: 'The height of the window',
18 })
19 .parseSync();
20
21/** @type {string} The URL to load in the window. */
22let appUrl;
23
24function createSillyTavernWindow() {
25 if (!appUrl) {
26 console.error('The server has not started yet.');
27 return;
28 }
29 new BrowserWindow({
30 height: cliArguments.height,
31 width: cliArguments.width,
32 }).loadURL(appUrl);
33}
34
35function startServer() {
36 return new Promise((_resolve, _reject) => {
37 serverEvents.addListener(EVENT_NAMES.SERVER_STARTED, ({ url }) => {
38 appUrl = url.toString();
39 createSillyTavernWindow();
40 });
41 const sillyTavernRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
42 process.chdir(sillyTavernRoot);
43
44 import('../../server.js');
45 });
46}
47
48app.whenReady().then(() => {
49 app.on('activate', () => {
50 if (BrowserWindow.getAllWindows().length === 0) {
51 createSillyTavernWindow();
52 }
53 });
54
55 startServer();
56});
57
58app.on('window-all-closed', () => {
59 if (process.platform !== 'darwin') {
60 app.quit();
61 }
62});
src/electron/package-lock.json+802 -0
@@ -0,0 +1,802 @@
1{
2 "name": "sillytavern-electron",
3 "version": "1.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "sillytavern-electron",
9 "version": "1.0.0",
10 "license": "AGPL-3.0",
11 "dependencies": {
12 "electron": "^35.0.0"
13 }
14 },
15 "node_modules/@electron/get": {
16 "version": "2.0.3",
17 "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
18 "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
19 "license": "MIT",
20 "dependencies": {
21 "debug": "^4.1.1",
22 "env-paths": "^2.2.0",
23 "fs-extra": "^8.1.0",
24 "got": "^11.8.5",
25 "progress": "^2.0.3",
26 "semver": "^6.2.0",
27 "sumchecker": "^3.0.1"
28 },
29 "engines": {
30 "node": ">=12"
31 },
32 "optionalDependencies": {
33 "global-agent": "^3.0.0"
34 }
35 },
36 "node_modules/@sindresorhus/is": {
37 "version": "4.6.0",
38 "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
39 "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
40 "license": "MIT",
41 "engines": {
42 "node": ">=10"
43 },
44 "funding": {
45 "url": "https://github.com/sindresorhus/is?sponsor=1"
46 }
47 },
48 "node_modules/@szmarczak/http-timer": {
49 "version": "4.0.6",
50 "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
51 "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
52 "license": "MIT",
53 "dependencies": {
54 "defer-to-connect": "^2.0.0"
55 },
56 "engines": {
57 "node": ">=10"
58 }
59 },
60 "node_modules/@types/cacheable-request": {
61 "version": "6.0.3",
62 "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
63 "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
64 "license": "MIT",
65 "dependencies": {
66 "@types/http-cache-semantics": "*",
67 "@types/keyv": "^3.1.4",
68 "@types/node": "*",
69 "@types/responselike": "^1.0.0"
70 }
71 },
72 "node_modules/@types/http-cache-semantics": {
73 "version": "4.0.4",
74 "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
75 "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
76 "license": "MIT"
77 },
78 "node_modules/@types/keyv": {
79 "version": "3.1.4",
80 "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
81 "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
82 "license": "MIT",
83 "dependencies": {
84 "@types/node": "*"
85 }
86 },
87 "node_modules/@types/node": {
88 "version": "22.13.9",
89 "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
90 "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
91 "license": "MIT",
92 "dependencies": {
93 "undici-types": "~6.20.0"
94 }
95 },
96 "node_modules/@types/responselike": {
97 "version": "1.0.3",
98 "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
99 "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
100 "license": "MIT",
101 "dependencies": {
102 "@types/node": "*"
103 }
104 },
105 "node_modules/@types/yauzl": {
106 "version": "2.10.3",
107 "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
108 "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
109 "license": "MIT",
110 "optional": true,
111 "dependencies": {
112 "@types/node": "*"
113 }
114 },
115 "node_modules/boolean": {
116 "version": "3.2.0",
117 "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
118 "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
119 "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
120 "license": "MIT",
121 "optional": true
122 },
123 "node_modules/buffer-crc32": {
124 "version": "0.2.13",
125 "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
126 "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
127 "license": "MIT",
128 "engines": {
129 "node": "*"
130 }
131 },
132 "node_modules/cacheable-lookup": {
133 "version": "5.0.4",
134 "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
135 "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
136 "license": "MIT",
137 "engines": {
138 "node": ">=10.6.0"
139 }
140 },
141 "node_modules/cacheable-request": {
142 "version": "7.0.4",
143 "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
144 "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
145 "license": "MIT",
146 "dependencies": {
147 "clone-response": "^1.0.2",
148 "get-stream": "^5.1.0",
149 "http-cache-semantics": "^4.0.0",
150 "keyv": "^4.0.0",
151 "lowercase-keys": "^2.0.0",
152 "normalize-url": "^6.0.1",
153 "responselike": "^2.0.0"
154 },
155 "engines": {
156 "node": ">=8"
157 }
158 },
159 "node_modules/clone-response": {
160 "version": "1.0.3",
161 "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
162 "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
163 "license": "MIT",
164 "dependencies": {
165 "mimic-response": "^1.0.0"
166 },
167 "funding": {
168 "url": "https://github.com/sponsors/sindresorhus"
169 }
170 },
171 "node_modules/debug": {
172 "version": "4.4.0",
173 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
174 "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
175 "license": "MIT",
176 "dependencies": {
177 "ms": "^2.1.3"
178 },
179 "engines": {
180 "node": ">=6.0"
181 },
182 "peerDependenciesMeta": {
183 "supports-color": {
184 "optional": true
185 }
186 }
187 },
188 "node_modules/decompress-response": {
189 "version": "6.0.0",
190 "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
191 "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
192 "license": "MIT",
193 "dependencies": {
194 "mimic-response": "^3.1.0"
195 },
196 "engines": {
197 "node": ">=10"
198 },
199 "funding": {
200 "url": "https://github.com/sponsors/sindresorhus"
201 }
202 },
203 "node_modules/decompress-response/node_modules/mimic-response": {
204 "version": "3.1.0",
205 "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
206 "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
207 "license": "MIT",
208 "engines": {
209 "node": ">=10"
210 },
211 "funding": {
212 "url": "https://github.com/sponsors/sindresorhus"
213 }
214 },
215 "node_modules/defer-to-connect": {
216 "version": "2.0.1",
217 "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
218 "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
219 "license": "MIT",
220 "engines": {
221 "node": ">=10"
222 }
223 },
224 "node_modules/define-data-property": {
225 "version": "1.1.4",
226 "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
227 "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
228 "license": "MIT",
229 "optional": true,
230 "dependencies": {
231 "es-define-property": "^1.0.0",
232 "es-errors": "^1.3.0",
233 "gopd": "^1.0.1"
234 },
235 "engines": {
236 "node": ">= 0.4"
237 },
238 "funding": {
239 "url": "https://github.com/sponsors/ljharb"
240 }
241 },
242 "node_modules/define-properties": {
243 "version": "1.2.1",
244 "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
245 "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
246 "license": "MIT",
247 "optional": true,
248 "dependencies": {
249 "define-data-property": "^1.0.1",
250 "has-property-descriptors": "^1.0.0",
251 "object-keys": "^1.1.1"
252 },
253 "engines": {
254 "node": ">= 0.4"
255 },
256 "funding": {
257 "url": "https://github.com/sponsors/ljharb"
258 }
259 },
260 "node_modules/detect-node": {
261 "version": "2.1.0",
262 "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
263 "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
264 "license": "MIT",
265 "optional": true
266 },
267 "node_modules/electron": {
268 "version": "35.0.0",
269 "resolved": "https://registry.npmjs.org/electron/-/electron-35.0.0.tgz",
270 "integrity": "sha512-mwNQNktYLPnUWZVR8iNkfWCBjmM5e2/CmB1rhACwE9ASDbVU7CYPgp/jLUB3bj/LyQsfSuubD82OUite6SN8Uw==",
271 "hasInstallScript": true,
272 "license": "MIT",
273 "dependencies": {
274 "@electron/get": "^2.0.0",
275 "@types/node": "^22.7.7",
276 "extract-zip": "^2.0.1"
277 },
278 "bin": {
279 "electron": "cli.js"
280 },
281 "engines": {
282 "node": ">= 12.20.55"
283 }
284 },
285 "node_modules/end-of-stream": {
286 "version": "1.4.4",
287 "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
288 "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
289 "license": "MIT",
290 "dependencies": {
291 "once": "^1.4.0"
292 }
293 },
294 "node_modules/env-paths": {
295 "version": "2.2.1",
296 "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
297 "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
298 "license": "MIT",
299 "engines": {
300 "node": ">=6"
301 }
302 },
303 "node_modules/es-define-property": {
304 "version": "1.0.1",
305 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
306 "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
307 "license": "MIT",
308 "optional": true,
309 "engines": {
310 "node": ">= 0.4"
311 }
312 },
313 "node_modules/es-errors": {
314 "version": "1.3.0",
315 "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
316 "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
317 "license": "MIT",
318 "optional": true,
319 "engines": {
320 "node": ">= 0.4"
321 }
322 },
323 "node_modules/es6-error": {
324 "version": "4.1.1",
325 "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
326 "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
327 "license": "MIT",
328 "optional": true
329 },
330 "node_modules/escape-string-regexp": {
331 "version": "4.0.0",
332 "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
333 "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
334 "license": "MIT",
335 "optional": true,
336 "engines": {
337 "node": ">=10"
338 },
339 "funding": {
340 "url": "https://github.com/sponsors/sindresorhus"
341 }
342 },
343 "node_modules/extract-zip": {
344 "version": "2.0.1",
345 "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
346 "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
347 "license": "BSD-2-Clause",
348 "dependencies": {
349 "debug": "^4.1.1",
350 "get-stream": "^5.1.0",
351 "yauzl": "^2.10.0"
352 },
353 "bin": {
354 "extract-zip": "cli.js"
355 },
356 "engines": {
357 "node": ">= 10.17.0"
358 },
359 "optionalDependencies": {
360 "@types/yauzl": "^2.9.1"
361 }
362 },
363 "node_modules/fd-slicer": {
364 "version": "1.1.0",
365 "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
366 "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
367 "license": "MIT",
368 "dependencies": {
369 "pend": "~1.2.0"
370 }
371 },
372 "node_modules/fs-extra": {
373 "version": "8.1.0",
374 "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
375 "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
376 "license": "MIT",
377 "dependencies": {
378 "graceful-fs": "^4.2.0",
379 "jsonfile": "^4.0.0",
380 "universalify": "^0.1.0"
381 },
382 "engines": {
383 "node": ">=6 <7 || >=8"
384 }
385 },
386 "node_modules/get-stream": {
387 "version": "5.2.0",
388 "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
389 "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
390 "license": "MIT",
391 "dependencies": {
392 "pump": "^3.0.0"
393 },
394 "engines": {
395 "node": ">=8"
396 },
397 "funding": {
398 "url": "https://github.com/sponsors/sindresorhus"
399 }
400 },
401 "node_modules/global-agent": {
402 "version": "3.0.0",
403 "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
404 "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
405 "license": "BSD-3-Clause",
406 "optional": true,
407 "dependencies": {
408 "boolean": "^3.0.1",
409 "es6-error": "^4.1.1",
410 "matcher": "^3.0.0",
411 "roarr": "^2.15.3",
412 "semver": "^7.3.2",
413 "serialize-error": "^7.0.1"
414 },
415 "engines": {
416 "node": ">=10.0"
417 }
418 },
419 "node_modules/global-agent/node_modules/semver": {
420 "version": "7.7.1",
421 "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
422 "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
423 "license": "ISC",
424 "optional": true,
425 "bin": {
426 "semver": "bin/semver.js"
427 },
428 "engines": {
429 "node": ">=10"
430 }
431 },
432 "node_modules/globalthis": {
433 "version": "1.0.4",
434 "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
435 "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
436 "license": "MIT",
437 "optional": true,
438 "dependencies": {
439 "define-properties": "^1.2.1",
440 "gopd": "^1.0.1"
441 },
442 "engines": {
443 "node": ">= 0.4"
444 },
445 "funding": {
446 "url": "https://github.com/sponsors/ljharb"
447 }
448 },
449 "node_modules/gopd": {
450 "version": "1.2.0",
451 "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
452 "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
453 "license": "MIT",
454 "optional": true,
455 "engines": {
456 "node": ">= 0.4"
457 },
458 "funding": {
459 "url": "https://github.com/sponsors/ljharb"
460 }
461 },
462 "node_modules/got": {
463 "version": "11.8.6",
464 "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
465 "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
466 "license": "MIT",
467 "dependencies": {
468 "@sindresorhus/is": "^4.0.0",
469 "@szmarczak/http-timer": "^4.0.5",
470 "@types/cacheable-request": "^6.0.1",
471 "@types/responselike": "^1.0.0",
472 "cacheable-lookup": "^5.0.3",
473 "cacheable-request": "^7.0.2",
474 "decompress-response": "^6.0.0",
475 "http2-wrapper": "^1.0.0-beta.5.2",
476 "lowercase-keys": "^2.0.0",
477 "p-cancelable": "^2.0.0",
478 "responselike": "^2.0.0"
479 },
480 "engines": {
481 "node": ">=10.19.0"
482 },
483 "funding": {
484 "url": "https://github.com/sindresorhus/got?sponsor=1"
485 }
486 },
487 "node_modules/graceful-fs": {
488 "version": "4.2.11",
489 "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
490 "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
491 "license": "ISC"
492 },
493 "node_modules/has-property-descriptors": {
494 "version": "1.0.2",
495 "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
496 "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
497 "license": "MIT",
498 "optional": true,
499 "dependencies": {
500 "es-define-property": "^1.0.0"
501 },
502 "funding": {
503 "url": "https://github.com/sponsors/ljharb"
504 }
505 },
506 "node_modules/http-cache-semantics": {
507 "version": "4.1.1",
508 "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
509 "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
510 "license": "BSD-2-Clause"
511 },
512 "node_modules/http2-wrapper": {
513 "version": "1.0.3",
514 "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
515 "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
516 "license": "MIT",
517 "dependencies": {
518 "quick-lru": "^5.1.1",
519 "resolve-alpn": "^1.0.0"
520 },
521 "engines": {
522 "node": ">=10.19.0"
523 }
524 },
525 "node_modules/json-buffer": {
526 "version": "3.0.1",
527 "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
528 "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
529 "license": "MIT"
530 },
531 "node_modules/json-stringify-safe": {
532 "version": "5.0.1",
533 "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
534 "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
535 "license": "ISC",
536 "optional": true
537 },
538 "node_modules/jsonfile": {
539 "version": "4.0.0",
540 "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
541 "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
542 "license": "MIT",
543 "optionalDependencies": {
544 "graceful-fs": "^4.1.6"
545 }
546 },
547 "node_modules/keyv": {
548 "version": "4.5.4",
549 "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
550 "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
551 "license": "MIT",
552 "dependencies": {
553 "json-buffer": "3.0.1"
554 }
555 },
556 "node_modules/lowercase-keys": {
557 "version": "2.0.0",
558 "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
559 "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
560 "license": "MIT",
561 "engines": {
562 "node": ">=8"
563 }
564 },
565 "node_modules/matcher": {
566 "version": "3.0.0",
567 "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
568 "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
569 "license": "MIT",
570 "optional": true,
571 "dependencies": {
572 "escape-string-regexp": "^4.0.0"
573 },
574 "engines": {
575 "node": ">=10"
576 }
577 },
578 "node_modules/mimic-response": {
579 "version": "1.0.1",
580 "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
581 "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
582 "license": "MIT",
583 "engines": {
584 "node": ">=4"
585 }
586 },
587 "node_modules/ms": {
588 "version": "2.1.3",
589 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
590 "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
591 "license": "MIT"
592 },
593 "node_modules/normalize-url": {
594 "version": "6.1.0",
595 "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
596 "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
597 "license": "MIT",
598 "engines": {
599 "node": ">=10"
600 },
601 "funding": {
602 "url": "https://github.com/sponsors/sindresorhus"
603 }
604 },
605 "node_modules/object-keys": {
606 "version": "1.1.1",
607 "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
608 "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
609 "license": "MIT",
610 "optional": true,
611 "engines": {
612 "node": ">= 0.4"
613 }
614 },
615 "node_modules/once": {
616 "version": "1.4.0",
617 "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
618 "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
619 "license": "ISC",
620 "dependencies": {
621 "wrappy": "1"
622 }
623 },
624 "node_modules/p-cancelable": {
625 "version": "2.1.1",
626 "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
627 "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
628 "license": "MIT",
629 "engines": {
630 "node": ">=8"
631 }
632 },
633 "node_modules/pend": {
634 "version": "1.2.0",
635 "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
636 "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
637 "license": "MIT"
638 },
639 "node_modules/progress": {
640 "version": "2.0.3",
641 "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
642 "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
643 "license": "MIT",
644 "engines": {
645 "node": ">=0.4.0"
646 }
647 },
648 "node_modules/pump": {
649 "version": "3.0.2",
650 "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
651 "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
652 "license": "MIT",
653 "dependencies": {
654 "end-of-stream": "^1.1.0",
655 "once": "^1.3.1"
656 }
657 },
658 "node_modules/quick-lru": {
659 "version": "5.1.1",
660 "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
661 "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
662 "license": "MIT",
663 "engines": {
664 "node": ">=10"
665 },
666 "funding": {
667 "url": "https://github.com/sponsors/sindresorhus"
668 }
669 },
670 "node_modules/resolve-alpn": {
671 "version": "1.2.1",
672 "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
673 "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
674 "license": "MIT"
675 },
676 "node_modules/responselike": {
677 "version": "2.0.1",
678 "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
679 "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
680 "license": "MIT",
681 "dependencies": {
682 "lowercase-keys": "^2.0.0"
683 },
684 "funding": {
685 "url": "https://github.com/sponsors/sindresorhus"
686 }
687 },
688 "node_modules/roarr": {
689 "version": "2.15.4",
690 "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
691 "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
692 "license": "BSD-3-Clause",
693 "optional": true,
694 "dependencies": {
695 "boolean": "^3.0.1",
696 "detect-node": "^2.0.4",
697 "globalthis": "^1.0.1",
698 "json-stringify-safe": "^5.0.1",
699 "semver-compare": "^1.0.0",
700 "sprintf-js": "^1.1.2"
701 },
702 "engines": {
703 "node": ">=8.0"
704 }
705 },
706 "node_modules/semver": {
707 "version": "6.3.1",
708 "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
709 "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
710 "license": "ISC",
711 "bin": {
712 "semver": "bin/semver.js"
713 }
714 },
715 "node_modules/semver-compare": {
716 "version": "1.0.0",
717 "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
718 "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
719 "license": "MIT",
720 "optional": true
721 },
722 "node_modules/serialize-error": {
723 "version": "7.0.1",
724 "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
725 "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
726 "license": "MIT",
727 "optional": true,
728 "dependencies": {
729 "type-fest": "^0.13.1"
730 },
731 "engines": {
732 "node": ">=10"
733 },
734 "funding": {
735 "url": "https://github.com/sponsors/sindresorhus"
736 }
737 },
738 "node_modules/sprintf-js": {
739 "version": "1.1.3",
740 "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
741 "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
742 "license": "BSD-3-Clause",
743 "optional": true
744 },
745 "node_modules/sumchecker": {
746 "version": "3.0.1",
747 "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
748 "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
749 "license": "Apache-2.0",
750 "dependencies": {
751 "debug": "^4.1.0"
752 },
753 "engines": {
754 "node": ">= 8.0"
755 }
756 },
757 "node_modules/type-fest": {
758 "version": "0.13.1",
759 "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
760 "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
761 "license": "(MIT OR CC0-1.0)",
762 "optional": true,
763 "engines": {
764 "node": ">=10"
765 },
766 "funding": {
767 "url": "https://github.com/sponsors/sindresorhus"
768 }
769 },
770 "node_modules/undici-types": {
771 "version": "6.20.0",
772 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
773 "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
774 "license": "MIT"
775 },
776 "node_modules/universalify": {
777 "version": "0.1.2",
778 "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
779 "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
780 "license": "MIT",
781 "engines": {
782 "node": ">= 4.0.0"
783 }
784 },
785 "node_modules/wrappy": {
786 "version": "1.0.2",
787 "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
788 "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
789 "license": "ISC"
790 },
791 "node_modules/yauzl": {
792 "version": "2.10.0",
793 "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
794 "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
795 "license": "MIT",
796 "dependencies": {
797 "buffer-crc32": "~0.2.3",
798 "fd-slicer": "~1.1.0"
799 }
800 }
801 }
802}
src/electron/package.json+16 -0
@@ -0,0 +1,16 @@
1{
2 "name": "sillytavern-electron",
3 "version": "1.0.0",
4 "description": "Electron server for SillyTavern",
5 "license": "AGPL-3.0",
6 "author": "",
7 "type": "module",
8 "main": "index.js",
9 "scripts": {
10 "test": "echo \"Error: no test specified\" && exit 1",
11 "start": "electron ."
12 },
13 "dependencies": {
14 "electron": "^35.0.0"
15 }
16}
src/electron/start.sh+11 -0
@@ -0,0 +1,11 @@
1#!/usr/bin/env bash
2
3# Make sure pwd is the directory of the script
4cd "$(dirname "$0")"
5
6echo "Assuming nodejs and npm is already installed. If you haven't installed them already, do so now"
7echo "Installing Electron Wrapper's Node Modules..."
8npm i --no-audit --no-fund --loglevel=error --no-progress --omit=dev
9
10echo "Starting Electron Wrapper..."
11npm run start -- "$@"
src/endpoints/backends/chat-completions.js+45 -1
@@ -99,6 +99,21 @@ function getOpenRouterTransforms(request) {
99}99}
100100
101/**101/**
102 * Gets OpenRouter plugins based on the request.
103 * @param {import('express').Request} request
104 * @returns {any[]} OpenRouter plugins
105 */
106function getOpenRouterPlugins(request) {
107 const plugins = [];
108
109 if (request.body.enable_web_search) {
110 plugins.push({ 'id': 'web' });
111 }
112
113 return plugins;
114}
115
116/**
102 * Sends a request to Claude API.117 * Sends a request to Claude API.
103 * @param {express.Request} request Express request118 * @param {express.Request} request Express request
104 * @param {express.Response} response Express response119 * @param {express.Response} response Express response
@@ -323,6 +338,7 @@ async function sendMakerSuiteRequest(request, response) {
323338
324 const model = String(request.body.model);339 const model = String(request.body.model);
325 const stream = Boolean(request.body.stream);340 const stream = Boolean(request.body.stream);
341 const enableWebSearch = Boolean(request.body.enable_web_search);
326 const isThinking = model.includes('thinking');342 const isThinking = model.includes('thinking');
327343
328 const generationConfig = {344 const generationConfig = {
@@ -350,6 +366,7 @@ async function sendMakerSuiteRequest(request, response) {
350 model.startsWith('gemini-exp')366 model.startsWith('gemini-exp')
351 ) && request.body.use_makersuite_sysprompt;367 ) && request.body.use_makersuite_sysprompt;
352368
369 const tools = [];
353 const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, getPromptNames(request));370 const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, getPromptNames(request));
354 let safetySettings = GEMINI_SAFETY;371 let safetySettings = GEMINI_SAFETY;
355372
@@ -363,6 +380,26 @@ async function sendMakerSuiteRequest(request, response) {
363 }380 }
364 // Most of the other models allow for setting the threshold of filters, except for HARM_CATEGORY_CIVIC_INTEGRITY, to OFF.381 // Most of the other models allow for setting the threshold of filters, except for HARM_CATEGORY_CIVIC_INTEGRITY, to OFF.
365382
383 if (enableWebSearch) {
384 const searchTool = model.includes('1.5') || model.includes('1.0')
385 ? ({ google_search_retrieval: {} })
386 : ({ google_search: {} });
387 tools.push(searchTool);
388 }
389
390 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
391 const functionDeclarations = [];
392 for (const tool of request.body.tools) {
393 if (tool.type === 'function') {
394 if (tool.function.parameters?.$schema) {
395 delete tool.function.parameters.$schema;
396 }
397 functionDeclarations.push(tool.function);
398 }
399 }
400 tools.push({ function_declarations: functionDeclarations });
401 }
402
366 let body = {403 let body = {
367 contents: prompt.contents,404 contents: prompt.contents,
368 safetySettings: safetySettings,405 safetySettings: safetySettings,
@@ -374,6 +411,10 @@ async function sendMakerSuiteRequest(request, response) {
374 body.systemInstruction = prompt.system_instruction;411 body.systemInstruction = prompt.system_instruction;
375 }412 }
376413
414 if (tools.length) {
415 body.tools = tools;
416 }
417
377 return body;418 return body;
378 }419 }
379420
@@ -429,10 +470,11 @@ async function sendMakerSuiteRequest(request, response) {
429 }470 }
430471
431 const responseContent = candidates[0].content ?? candidates[0].output;472 const responseContent = candidates[0].content ?? candidates[0].output;
473 const functionCall = (candidates?.[0]?.content?.parts ?? []).some(part => part.functionCall);
432 console.warn('Google AI Studio response:', responseContent);474 console.warn('Google AI Studio response:', responseContent);
433475
434 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');476 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
435 if (!responseText) {477 if (!responseText && !functionCall) {
436 let message = 'Google AI Studio Candidate text empty';478 let message = 'Google AI Studio Candidate text empty';
437 console.warn(message, generateResponseJson);479 console.warn(message, generateResponseJson);
438 return response.send({ error: { message } });480 return response.send({ error: { message } });
@@ -1017,6 +1059,7 @@ router.post('/generate', jsonParser, function (request, response) {
1017 headers = { ...OPENROUTER_HEADERS };1059 headers = { ...OPENROUTER_HEADERS };
1018 bodyParams = {1060 bodyParams = {
1019 'transforms': getOpenRouterTransforms(request),1061 'transforms': getOpenRouterTransforms(request),
1062 'plugins': getOpenRouterPlugins(request),
1020 'include_reasoning': Boolean(request.body.include_reasoning),1063 'include_reasoning': Boolean(request.body.include_reasoning),
1021 };1064 };
10221065
@@ -1185,6 +1228,7 @@ router.post('/generate', jsonParser, function (request, response) {
1185 */1228 */
1186 async function makeRequest(config, response, request, retries = 5, timeout = 5000) {1229 async function makeRequest(config, response, request, retries = 5, timeout = 5000) {
1187 try {1230 try {
1231 controller.signal.throwIfAborted();
1188 const fetchResponse = await fetch(endpointUrl, config);1232 const fetchResponse = await fetch(endpointUrl, config);
11891233
1190 if (request.body.stream) {1234 if (request.body.stream) {
src/endpoints/characters.js+45 -13
@@ -23,12 +23,13 @@ import { invalidateThumbnail } from './thumbnails.js';
23import { importRisuSprites } from './sprites.js';23import { importRisuSprites } from './sprites.js';
24const defaultAvatarPath = './public/img/ai4.png';24const defaultAvatarPath = './public/img/ai4.png';
2525
26// KV-store for parsed character data
27const cacheCapacity = Number(getConfigValue('cardsCacheCapacity', 100, 'number')); // MB
28// With 100 MB limit it would take roughly 3000 characters to reach this limit26// With 100 MB limit it would take roughly 3000 characters to reach this limit
29const characterDataCache = new MemoryLimitedMap(1024 * 1024 * cacheCapacity);27const memoryCacheCapacity = getConfigValue('performance.memoryCacheCapacity', '100mb');
28const memoryCache = new MemoryLimitedMap(memoryCacheCapacity);
30// Some Android devices require tighter memory management29// Some Android devices require tighter memory management
31const isAndroid = process.platform === 'android';30const isAndroid = process.platform === 'android';
31// Use shallow character data for the character list
32const useShallowCharacters = !!getConfigValue('performance.lazyLoadCharacters', false, 'boolean');
3233
33/**34/**
34 * Reads the character card from the specified image file.35 * Reads the character card from the specified image file.
@@ -39,12 +40,12 @@ const isAndroid = process.platform === 'android';
39async function readCharacterData(inputFile, inputFormat = 'png') {40async function readCharacterData(inputFile, inputFormat = 'png') {
40 const stat = fs.statSync(inputFile);41 const stat = fs.statSync(inputFile);
41 const cacheKey = `${inputFile}-${stat.mtimeMs}`;42 const cacheKey = `${inputFile}-${stat.mtimeMs}`;
42 if (characterDataCache.has(cacheKey)) {43 if (memoryCache.has(cacheKey)) {
43 return characterDataCache.get(cacheKey);44 return memoryCache.get(cacheKey);
44 }45 }
4546
46 const result = parse(inputFile, inputFormat);47 const result = parse(inputFile, inputFormat);
47 !isAndroid && characterDataCache.set(cacheKey, result);48 !isAndroid && memoryCache.set(cacheKey, result);
48 return result;49 return result;
49}50}
5051
@@ -60,12 +61,12 @@ async function readCharacterData(inputFile, inputFormat = 'png') {
60async function writeCharacterData(inputFile, data, outputFile, request, crop = undefined) {61async function writeCharacterData(inputFile, data, outputFile, request, crop = undefined) {
61 try {62 try {
62 // Reset the cache63 // Reset the cache
63 for (const key of characterDataCache.keys()) {64 for (const key of memoryCache.keys()) {
64 if (Buffer.isBuffer(inputFile)) {65 if (Buffer.isBuffer(inputFile)) {
65 break;66 break;
66 }67 }
67 if (key.startsWith(inputFile)) {68 if (key.startsWith(inputFile)) {
68 characterDataCache.delete(key);69 memoryCache.delete(key);
69 break;70 break;
70 }71 }
71 }72 }
@@ -201,13 +202,44 @@ const calculateDataSize = (data) => {
201};202};
202203
203/**204/**
205 * Only get fields that are used to display the character list.
206 * @param {object} character Character object
207 * @returns {{shallow: true, [key: string]: any}} Shallow character
208 */
209const toShallow = (character) => {
210 return {
211 shallow: true,
212 name: character.name,
213 avatar: character.avatar,
214 chat: character.chat,
215 fav: character.fav,
216 date_added: character.date_added,
217 create_date: character.create_date,
218 date_last_chat: character.date_last_chat,
219 chat_size: character.chat_size,
220 data_size: character.data_size,
221 data: {
222 name: _.get(character, 'data.name', ''),
223 character_version: _.get(character, 'data.character_version', ''),
224 creator: _.get(character, 'data.creator', ''),
225 creator_notes: _.get(character, 'data.creator_notes', ''),
226 extensions: {
227 fav: _.get(character, 'data.extensions.fav', false),
228 },
229 },
230 };
231};
232
233/**
204 * processCharacter - Process a given character, read its data and calculate its statistics.234 * processCharacter - Process a given character, read its data and calculate its statistics.
205 *235 *
206 * @param {string} item The name of the character.236 * @param {string} item The name of the character.
207 * @param {import('../users.js').UserDirectoryList} directories User directories237 * @param {import('../users.js').UserDirectoryList} directories User directories
238 * @param {object} options Options for the character processing
239 * @param {boolean} options.shallow If true, only return the core character's metadata
208 * @return {Promise<object>} A Promise that resolves when the character processing is done.240 * @return {Promise<object>} A Promise that resolves when the character processing is done.
209 */241 */
210const processCharacter = async (item, directories) => {242const processCharacter = async (item, directories, { shallow }) => {
211 try {243 try {
212 const imgFile = path.join(directories.characters, item);244 const imgFile = path.join(directories.characters, item);
213 const imgData = await readCharacterData(imgFile);245 const imgData = await readCharacterData(imgFile);
@@ -226,7 +258,7 @@ const processCharacter = async (item, directories) => {
226 character['chat_size'] = chatSize;258 character['chat_size'] = chatSize;
227 character['date_last_chat'] = dateLastChat;259 character['date_last_chat'] = dateLastChat;
228 character['data_size'] = calculateDataSize(jsonObject?.data);260 character['data_size'] = calculateDataSize(jsonObject?.data);
229 return character;261 return shallow ? toShallow(character) : character;
230 }262 }
231 catch (err) {263 catch (err) {
232 console.error(`Could not process character: ${item}`);264 console.error(`Could not process character: ${item}`);
@@ -993,7 +1025,7 @@ router.post('/all', jsonParser, async function (request, response) {
993 try {1025 try {
994 const files = fs.readdirSync(request.user.directories.characters);1026 const files = fs.readdirSync(request.user.directories.characters);
995 const pngFiles = files.filter(file => file.endsWith('.png'));1027 const pngFiles = files.filter(file => file.endsWith('.png'));
996 const processingPromises = pngFiles.map(file => processCharacter(file, request.user.directories));1028 const processingPromises = pngFiles.map(file => processCharacter(file, request.user.directories, { shallow: useShallowCharacters }));
997 const data = (await Promise.all(processingPromises)).filter(c => c.name);1029 const data = (await Promise.all(processingPromises)).filter(c => c.name);
998 return response.send(data);1030 return response.send(data);
999 } catch (err) {1031 } catch (err) {
@@ -1012,7 +1044,7 @@ router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (req
1012 return response.sendStatus(404);1044 return response.sendStatus(404);
1013 }1045 }
10141046
1015 const data = await processCharacter(item, request.user.directories);1047 const data = await processCharacter(item, request.user.directories, { shallow: false });
10161048
1017 return response.send(data);1049 return response.send(data);
1018 } catch (err) {1050 } catch (err) {
@@ -1022,11 +1054,11 @@ router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (req
1022});1054});
10231055
1024router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {1056router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
1057 try {
1025 if (!request.body) return response.sendStatus(400);1058 if (!request.body) return response.sendStatus(400);
10261059
1027 const characterDirectory = (request.body.avatar_url).replace('.png', '');1060 const characterDirectory = (request.body.avatar_url).replace('.png', '');
10281061
1029 try {
1030 const chatsDirectory = path.join(request.user.directories.chats, characterDirectory);1062 const chatsDirectory = path.join(request.user.directories.chats, characterDirectory);
10311063
1032 if (!fs.existsSync(chatsDirectory)) {1064 if (!fs.existsSync(chatsDirectory)) {
src/prompt-converters.js+38 -4
@@ -1,5 +1,5 @@
1import crypto from 'node:crypto';1import crypto from 'node:crypto';
2import { getConfigValue } from './util.js';2import { getConfigValue, tryParse } from './util.js';
33
4const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.');4const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.');
55
@@ -411,11 +411,12 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
411 }411 }
412412
413 const system_instruction = { parts: { text: sys_prompt.trim() } };413 const system_instruction = { parts: { text: sys_prompt.trim() } };
414 const toolNameMap = {};
414415
415 const contents = [];416 const contents = [];
416 messages.forEach((message, index) => {417 messages.forEach((message, index) => {
417 // fix the roles418 // fix the roles
418 if (message.role === 'system') {419 if (message.role === 'system' || message.role === 'tool') {
419 message.role = 'user';420 message.role = 'user';
420 } else if (message.role === 'assistant') {421 } else if (message.role === 'assistant') {
421 message.role = 'model';422 message.role = 'model';
@@ -423,7 +424,21 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
423424
424 // Convert the content to an array of parts425 // Convert the content to an array of parts
425 if (!Array.isArray(message.content)) {426 if (!Array.isArray(message.content)) {
426 message.content = [{ type: 'text', text: String(message.content ?? '') }];427 const content = (() => {
428 const hasToolCalls = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
429 const hasToolCallId = typeof message.tool_call_id === 'string' && message.tool_call_id.length > 0;
430
431 if (hasToolCalls) {
432 return { type: 'tool_calls', tool_calls: message.tool_calls };
433 }
434
435 if (hasToolCallId) {
436 return { type: 'tool_call_id', tool_call_id: message.tool_call_id, content: String(message.content ?? '') };
437 }
438
439 return { type: 'text', text: String(message.content ?? '') };
440 })();
441 message.content = [content];
427 }442 }
428443
429 // similar story as claude444 // similar story as claude
@@ -455,6 +470,25 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
455 message.content.forEach((part) => {470 message.content.forEach((part) => {
456 if (part.type === 'text') {471 if (part.type === 'text') {
457 parts.push({ text: part.text });472 parts.push({ text: part.text });
473 } else if (part.type === 'tool_call_id') {
474 const name = toolNameMap[part.tool_call_id] ?? 'unknown';
475 parts.push({
476 functionResponse: {
477 name: name,
478 response: { name: name, content: part.content },
479 },
480 });
481 } else if (part.type === 'tool_calls') {
482 part.tool_calls.forEach((toolCall) => {
483 parts.push({
484 functionCall: {
485 name: toolCall.function.name,
486 args: tryParse(toolCall.function.arguments) ?? toolCall.function.arguments,
487 },
488 });
489
490 toolNameMap[toolCall.id] = toolCall.function.name;
491 });
458 } else if (part.type === 'image_url' && isMultimodal) {492 } else if (part.type === 'image_url' && isMultimodal) {
459 const mimeType = part.image_url.url.split(';')[0].split(':')[1];493 const mimeType = part.image_url.url.split(';')[0].split(':')[1];
460 const base64Data = part.image_url.url.split(',')[1];494 const base64Data = part.image_url.url.split(',')[1];
@@ -473,7 +507,7 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
473 if (part.text) {507 if (part.text) {
474 contents[contents.length - 1].parts[0].text += '\n\n' + part.text;508 contents[contents.length - 1].parts[0].text += '\n\n' + part.text;
475 }509 }
476 if (part.inlineData) {510 if (part.inlineData || part.functionCall || part.functionResponse) {
477 contents[contents.length - 1].parts.push(part);511 contents[contents.length - 1].parts.push(part);
478 }512 }
479 });513 });
src/server-events.js+21 -0
@@ -0,0 +1,21 @@
1import EventEmitter from 'node:events';
2import process from 'node:process';
3
4/**
5 * @typedef {import('../index').ServerEventMap} ServerEventMap
6 * @type {EventEmitter<ServerEventMap>} The default event source.
7 */
8export const serverEvents = new EventEmitter();
9process.serverEvents = serverEvents;
10export default serverEvents;
11
12/**
13 * @enum {string}
14 * @readonly
15 */
16export const EVENT_NAMES = Object.freeze({
17 /**
18 * Emitted when the server has started.
19 */
20 SERVER_STARTED: 'server-started',
21});
src/util.js+8 -7
@@ -16,6 +16,7 @@ import mime from 'mime-types';
16import { default as simpleGit } from 'simple-git';16import { default as simpleGit } from 'simple-git';
17import chalk from 'chalk';17import chalk from 'chalk';
18import { LOG_LEVELS } from './constants.js';18import { LOG_LEVELS } from './constants.js';
19import bytes from 'bytes';
1920
20/**21/**
21 * Parsed config object.22 * Parsed config object.
@@ -856,14 +857,10 @@ export function setupLogLevel() {
856export class MemoryLimitedMap {857export class MemoryLimitedMap {
857 /**858 /**
858 * Creates an instance of MemoryLimitedMap.859 * Creates an instance of MemoryLimitedMap.
859 * @param {number} maxMemoryInBytes - The maximum allowed memory in bytes for string values.860 * @param {string} cacheCapacity - Maximum memory usage in human-readable format (e.g., '1 GB').
860 */861 */
861 constructor(maxMemoryInBytes) {862 constructor(cacheCapacity) {
862 if (typeof maxMemoryInBytes !== 'number' || maxMemoryInBytes <= 0 || isNaN(maxMemoryInBytes)) {863 this.maxMemory = bytes.parse(cacheCapacity) ?? 0;
863 console.warn('Invalid maxMemoryInBytes, using a fallback value of 1 GB.');
864 maxMemoryInBytes = 1024 * 1024 * 1024; // 1 GB
865 }
866 this.maxMemory = maxMemoryInBytes;
867 this.currentMemory = 0;864 this.currentMemory = 0;
868 this.map = new Map();865 this.map = new Map();
869 this.queue = [];866 this.queue = [];
@@ -886,6 +883,10 @@ export class MemoryLimitedMap {
886 * @param {string} value883 * @param {string} value
887 */884 */
888 set(key, value) {885 set(key, value) {
886 if (this.maxMemory <= 0) {
887 return;
888 }
889
889 if (typeof key !== 'string' || typeof value !== 'string') {890 if (typeof key !== 'string' || typeof value !== 'string') {
890 return;891 return;
891 }892 }