Merge branch 'staging' into geminiStructured

This commit is contained in:
Cohee
2025-03-07 00:22:18 +02:00
27 changed files with 1388 additions and 57 deletions

View File

@@ -1,8 +1,6 @@
# -- DATA CONFIGURATION --
# Root directory for user data storage
dataRoot: ./data
# The maximum amount of memory that parsed character cards can use in MB
cardsCacheCapacity: 100
# -- SERVER CONFIGURATION --
# Listen for incoming connections
listen: false
@@ -135,6 +133,14 @@ thumbnails:
# Maximum thumbnail dimensions per type [width, height]
dimensions: { 'bg': [160, 90], 'avatar': [96, 144] }
# PERFORMANCE-RELATED CONFIGURATION
performance:
# Enables lazy loading of character cards. Improves performances with large card libraries.
# May have compatibility issues with some extensions.
lazyLoadCharacters: false
# The maximum amount of memory that parsed character cards can use. Set to 0 to disable memory caching.
memoryCacheCapacity: '100mb'
# Allow secret keys exposure via API
allowKeysExposure: false
# Skip new default content checks

34
index.d.ts vendored
View File

@@ -1,8 +1,36 @@
import { UserDirectoryList, User } from "./src/users";
import { CommandLineArguments } from "./src/command-line";
import { CsrfSyncedToken } from "csrf-sync";
import { EventEmitter } from 'node:events';
import { CsrfSyncedToken } from 'csrf-sync';
import { UserDirectoryList, User } from './src/users.js';
import { CommandLineArguments } from './src/command-line.js';
import { EVENT_NAMES } from './src/server-events.js';
/**
* Event payload for SERVER_STARTED event.
*/
export interface ServerStartedEvent {
/**
* The URL the server is listening on.
*/
url: URL;
}
/**
* Map of all server events to their payload types.
*/
export interface ServerEventMap {
[EVENT_NAMES.SERVER_STARTED]: [ServerStartedEvent];
}
declare global {
declare namespace NodeJS {
export interface Process {
/**
* A global instance of the server events emitter.
*/
serverEvents: EventEmitter<ServerEventMap>;
}
}
declare namespace CookieSessionInterfaces {
export interface CookieSessionObject {
/**

12
package-lock.json generated
View File

@@ -21,6 +21,7 @@
"bing-translate-api": "^4.0.2",
"body-parser": "^1.20.2",
"bowser": "^2.11.0",
"bytes": "^3.1.2",
"chalk": "^5.4.1",
"command-exists": "^1.2.9",
"compression": "^1.8.0",
@@ -83,6 +84,7 @@
},
"devDependencies": {
"@types/archiver": "^6.0.3",
"@types/bytes": "^3.1.5",
"@types/command-exists": "^1.2.3",
"@types/compression": "^1.7.5",
"@types/cookie-parser": "^1.4.8",
@@ -1105,6 +1107,13 @@
"@types/node": "*"
}
},
"node_modules/@types/bytes": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@types/bytes/-/bytes-3.1.5.tgz",
"integrity": "sha512-VgZkrJckypj85YxEsEavcMmmSOIzkUHqWmM4CCyia5dc54YwsXzJ5uT4fYxBQNEXx+oF1krlhgCbvfubXqZYsQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/cacheable-request": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
@@ -3399,6 +3408,9 @@
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}

View File

@@ -11,6 +11,7 @@
"bing-translate-api": "^4.0.2",
"body-parser": "^1.20.2",
"bowser": "^2.11.0",
"bytes": "^3.1.2",
"chalk": "^5.4.1",
"command-exists": "^1.2.9",
"compression": "^1.8.0",
@@ -92,7 +93,8 @@
"version": "1.12.12",
"scripts": {
"start": "node server.js",
"debug": "node server.js --inspect",
"debug": "node --inspect server.js",
"electron": "electron ./src/electron",
"start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
"start:bun": "bun server.js",
"start:no-csrf": "node server.js --disableCsrf",
@@ -112,6 +114,7 @@
"main": "server.js",
"devDependencies": {
"@types/archiver": "^6.0.3",
"@types/bytes": "^3.1.5",
"@types/command-exists": "^1.2.3",
"@types/compression": "^1.7.5",
"@types/cookie-parser": "^1.4.8",

View File

@@ -96,6 +96,11 @@ const keyMigrationMap = [
newKey: 'logging.minLogLevel',
migrate: (value) => value,
},
{
oldKey: 'cardsCacheCapacity',
newKey: 'performance.memoryCacheCapacity',
migrate: (value) => `${value}mb`,
},
// uncomment one release after 1.12.13
/*
{

View File

@@ -1951,7 +1951,18 @@
</span>
</div>
</div>
<div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek">
<div class="range-block" data-source="makersuite,openrouter">
<label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">
<input id="openai_enable_web_search" type="checkbox" />
<span data-i18n="Enable web search">Enable web search</span>
</label>
<div class="flexBasis100p toggle-description justifyLeft">
<span>
Use search capabilities provided by the backend.
</span>
</div>
</div>
<div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek,makersuite">
<label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
<input id="openai_function_calling" type="checkbox" />
<span data-i18n="Enable function calling">Enable function calling</span>

View File

@@ -453,6 +453,8 @@ export const event_types = {
MESSAGE_DELETED: 'message_deleted',
MESSAGE_UPDATED: 'message_updated',
MESSAGE_FILE_EMBEDDED: 'message_file_embedded',
MESSAGE_REASONING_EDITED: 'message_reasoning_edited',
MESSAGE_REASONING_DELETED: 'message_reasoning_deleted',
MORE_MESSAGES_LOADED: 'more_messages_loaded',
IMPERSONATE_READY: 'impersonate_ready',
CHAT_CHANGED: 'chat_id_changed',
@@ -1780,9 +1782,7 @@ export async function getCharacters() {
const response = await fetch('/api/characters/all', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
'': '',
}),
body: JSON.stringify({}),
});
if (response.ok === true) {
characters.splice(0, characters.length);
@@ -3678,6 +3678,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
setGenerationProgress(0);
generation_started = new Date();
// Prevent generation from shallow characters
await unshallowCharacter(this_chid);
// Occurs every time, even if the generation is aborted due to slash commands execution
await eventSource.emit(event_types.GENERATION_STARTED, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage }, dryRun);
@@ -6724,9 +6727,43 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t
}
}
/**
* Loads all the data of a shallow character.
* @param {string|undefined} characterId Array index
* @returns {Promise<void>} Promise that resolves when the character is unshallowed
*/
export async function unshallowCharacter(characterId) {
if (characterId === undefined) {
console.warn('Undefined character cannot be unshallowed');
return;
}
/** @type {import('./scripts/char-data.js').v1CharData} */
const character = characters[characterId];
if (!character) {
console.warn('Character not found:', characterId);
return;
}
// Character is not shallow
if (!character.shallow) {
return;
}
const avatar = character.avatar;
if (!avatar) {
console.warn('Character has no avatar field:', characterId);
return;
}
await getOneCharacter(avatar);
}
export async function getChat() {
//console.log('/api/chats/get -- entered for -- ' + characters[this_chid].name);
try {
await unshallowCharacter(this_chid);
const response = await $.ajax({
type: 'POST',
url: '/api/chats/get',

View File

@@ -316,7 +316,10 @@ export async function favsToHotswap() {
const entities = getEntitiesList({ doFilter: false });
const container = $('#right-nav-panel .hotswap');
const favs = entities.filter(x => x.item.fav || x.item.fav == 'true');
// Hard limit is required because even if all hotswaps don't fit the screen, their images would still be loaded
// 25 is roughly calculated as the maximum number of favs that can fit an ultrawide monitor with the default theme
const FAVS_LIMIT = 25;
const favs = entities.filter(x => x.item.fav || x.item.fav == 'true').slice(0, FAVS_LIMIT);
//helpful instruction message if no characters are favorited
if (favs.length == 0) {

View File

@@ -113,5 +113,6 @@
* @property {string} chat - name of the current chat file chat
* @property {string} avatar - file name of the avatar image (acts as a unique identifier)
* @property {string} json_data - the full raw JSON data of the character
* @property {boolean?} shallow - if the data is shallow (lazy-loaded)
*/
export default 0;// now this file is a module

View File

@@ -11,6 +11,7 @@ import {
} from '../../../script.js';
import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js';
import { updateReasoningUI } from '../../reasoning.js';
import { findSecret, secret_state, writeSecret } from '../../secrets.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
@@ -172,21 +173,38 @@ function loadSettings() {
showKeysButton();
}
/**
* Check if the swipe is being generated for a message.
* @param {string|number} messageId Message ID
* @returns {boolean} Whether the swipe is being generated
*/
function isGeneratingSwipe(messageId) {
return $(`#chat .mes[mesid="${messageId}"] .mes_text`).text() === '...';
}
async function translateImpersonate(text) {
const translatedText = await translate(text, extension_settings.translate.target_language);
$('#send_textarea').val(translatedText);
}
/**
* Translates the contents of an incoming message.
* @param {string | number} messageId Message ID
* @returns {Promise<void>}
*/
async function translateIncomingMessage(messageId) {
const context = getContext();
const message = context.chat[messageId];
if (!message) {
return;
}
if (typeof message.extra !== 'object') {
message.extra = {};
}
// New swipe is being generated. Don't translate that
if ($(`#chat .mes[mesid="${messageId}"] .mes_text`).text() == '...') {
if (isGeneratingSwipe(messageId)) {
return;
}
@@ -194,7 +212,36 @@ async function translateIncomingMessage(messageId) {
const translation = await translate(textToTranslate, extension_settings.translate.target_language);
message.extra.display_text = translation;
updateMessageBlock(messageId, message);
updateMessageBlock(Number(messageId), message);
}
/**
* Translates the reasoning of an incoming message.
* @param {string | number} messageId
* @returns {Promise<boolean>} translated or not
*/
async function translateIncomingMessageReasoning(messageId) {
const context = getContext();
const message = context.chat[messageId];
if (!message) {
return false;
}
if (typeof message.extra !== 'object') {
message.extra = {};
}
if (!message.extra.reasoning || isGeneratingSwipe(messageId)) {
return false;
}
const textToTranslate = substituteParams(message.extra.reasoning, context.name1, message.name);
const translation = await translate(textToTranslate, extension_settings.translate.target_language);
message.extra.reasoning_display_text = translation;
updateReasoningUI(Number(messageId));
return true;
}
async function translateProviderOneRing(text, lang) {
@@ -535,6 +582,7 @@ async function onTranslateChatClick() {
toastr.info(`${chat.length} message(s) queued for translation.`, 'Please wait...');
for (let i = 0; i < chat.length; i++) {
await translateIncomingMessageReasoning(i);
await translateIncomingMessage(i);
}
@@ -561,6 +609,7 @@ async function onTranslationsClearClick() {
for (const mes of chat) {
if (mes.extra) {
delete mes.extra.display_text;
delete mes.extra.reasoning_display_text;
}
}
@@ -573,12 +622,47 @@ async function translateMessageEdit(messageId) {
const chat = context.chat;
const message = chat[messageId];
if (message.is_system || extension_settings.translate.auto_mode == autoModeOptions.NONE) {
return;
let anyChange = false;
if (message.is_system || (extension_settings.translate.auto_mode == autoModeOptions.NONE && message.extra?.display_text)) {
delete message.extra.display_text;
updateMessageBlock(messageId, message);
anyChange = true;
} else if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) {
await translateIncomingMessage(messageId);
anyChange = true;
}
if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) {
await translateIncomingMessage(messageId);
if (anyChange) {
await context.saveChat();
}
}
async function translateMessageReasoningEdit(messageId) {
const context = getContext();
const chat = context.chat;
const message = chat[messageId];
let anyChange = false;
if (message.is_system || (extension_settings.translate.auto_mode == autoModeOptions.NONE && message.extra?.reasoning_display_text)) {
delete message.extra.reasoning_display_text;
updateReasoningUI(Number(messageId));
anyChange = true;
} else if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) {
anyChange = await translateIncomingMessageReasoning(messageId);
}
if (anyChange) {
await context.saveChat();
}
}
async function removeReasoningDisplayText(messageId) {
const context = getContext();
const message = context.chat[messageId];
if (message.extra?.reasoning_display_text) {
delete message.extra.reasoning_display_text;
updateReasoningUI(Number(messageId));
await context.saveChat();
}
}
@@ -588,22 +672,36 @@ async function onMessageTranslateClick() {
const message = context.chat[messageId];
// If the message is already translated, revert it back to the original text
let alreadyTranslated = false;
if (message?.extra?.display_text) {
delete message.extra.display_text;
updateMessageBlock(messageId, message);
updateMessageBlock(Number(messageId), message);
alreadyTranslated = true;
}
if (message?.extra?.reasoning_display_text) {
delete message.extra.reasoning_display_text;
updateReasoningUI(Number(messageId));
alreadyTranslated = true;
}
// If the message is not translated, translate it
else {
if (!alreadyTranslated) {
await translateIncomingMessageReasoning(messageId);
await translateIncomingMessage(messageId);
}
await context.saveChat();
}
const handleIncomingMessage = createEventHandler(translateIncomingMessage, () => shouldTranslate(incomingTypes));
const handleIncomingMessage = createEventHandler(async (messageId) => {
await translateIncomingMessageReasoning(messageId);
await translateIncomingMessage(messageId);
}, () => shouldTranslate(incomingTypes));
const handleOutgoingMessage = createEventHandler(translateOutgoingMessage, () => shouldTranslate(outgoingTypes));
const handleImpersonateReady = createEventHandler(translateImpersonate, () => shouldTranslate(incomingTypes));
const handleMessageEdit = createEventHandler(translateMessageEdit, () => true);
const handleMessageReasoningEdit = createEventHandler(translateMessageReasoningEdit, () => true);
const handleMessageReasoningDelete = createEventHandler(removeReasoningDisplayText, () => true);
globalThis.translate = translate;
@@ -717,6 +815,8 @@ jQuery(async () => {
eventSource.on(event_types.MESSAGE_SWIPED, handleIncomingMessage);
eventSource.on(event_types.IMPERSONATE_READY, handleImpersonateReady);
eventSource.on(event_types.MESSAGE_UPDATED, handleMessageEdit);
eventSource.on(event_types.MESSAGE_REASONING_EDITED, handleMessageReasoningEdit);
eventSource.on(event_types.MESSAGE_REASONING_DELETED, handleMessageReasoningDelete);
document.body.classList.add('translate');

View File

@@ -72,6 +72,7 @@ import {
animation_duration,
depth_prompt_role_default,
shouldAutoContinue,
unshallowCharacter,
} from '../script.js';
import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';
import { FILTER_TYPES, FilterHelper } from './filters.js';
@@ -216,6 +217,7 @@ export async function getGroupChat(groupId, reload = false) {
// Run validation before any loading
validateGroup(group);
await unshallowGroupMembers(groupId);
const chat_id = group.chat_id;
const data = await loadGroupChat(chat_id);
@@ -824,6 +826,8 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
}
try {
await unshallowGroupMembers(selected_group);
throwIfAborted();
hideSwipeButtons();
is_group_generating = true;
@@ -1137,6 +1141,29 @@ export async function editGroup(id, immediately, reload = true) {
saveGroupDebounced(group, reload);
}
/**
* Unshallows all definitions of group members.
* @param {string} groupId Id of the group
* @returns {Promise<void>} Promise that resolves when all group members are unshallowed
*/
export async function unshallowGroupMembers(groupId) {
const group = groups.find(x => x.id == groupId);
if (!group) {
return;
}
const members = group.members;
if (!Array.isArray(members)) {
return;
}
for (const member of members) {
const index = characters.findIndex(x => x.avatar === member);
if (index === -1) {
continue;
}
await unshallowCharacter(String(index));
}
}
let groupAutoModeAbortController = null;
async function groupChatAutoModeWorker() {
@@ -1158,9 +1185,9 @@ async function groupChatAutoModeWorker() {
await generateGroupWrapper(true, 'auto', { signal: groupAutoModeAbortController.signal });
}
async function modifyGroupMember(chat_id, groupMember, isDelete) {
async function modifyGroupMember(groupId, groupMember, isDelete) {
const id = groupMember.data('id');
const thisGroup = groups.find((x) => x.id == chat_id);
const thisGroup = groups.find((x) => x.id == groupId);
const membersArray = thisGroup?.members ?? newGroupMembers;
if (isDelete) {
@@ -1173,6 +1200,7 @@ async function modifyGroupMember(chat_id, groupMember, isDelete) {
}
if (openGroupId) {
await unshallowGroupMembers(openGroupId);
await editGroup(openGroupId, false, false);
updateGroupAvatar(thisGroup);
}
@@ -1638,7 +1666,7 @@ async function onGroupActionClick(event) {
}
if (action === 'view') {
openCharacterDefinition(member);
await openCharacterDefinition(member);
}
if (action === 'speak') {
@@ -1690,7 +1718,7 @@ export async function openGroupById(groupId) {
return false;
}
function openCharacterDefinition(characterSelect) {
async function openCharacterDefinition(characterSelect) {
if (is_group_generating) {
toastr.warning(t`Can't peek a character while group reply is being generated`);
console.warn('Can\'t peek a character def while group reply is being generated');
@@ -1703,6 +1731,7 @@ function openCharacterDefinition(characterSelect) {
return;
}
await unshallowCharacter(chid);
setCharacterId(chid);
select_selected_character(chid);
// Gentle nudge to recalculate tokens

View File

@@ -300,6 +300,7 @@ export const settingsToUpdate = {
function_calling: ['#openai_function_calling', 'function_calling', true],
show_thoughts: ['#openai_show_thoughts', 'show_thoughts', true],
reasoning_effort: ['#openai_reasoning_effort', 'reasoning_effort', false],
enable_web_search: ['#openai_enable_web_search', 'enable_web_search', true],
seed: ['#seed_openai', 'seed', false],
n: ['#n_openai', 'n', false],
bypass_status_check: ['#openai_bypass_status_check', 'bypass_status_check', true],
@@ -380,6 +381,7 @@ const default_settings = {
custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
show_thoughts: true,
reasoning_effort: 'medium',
enable_web_search: false,
seed: -1,
n: 1,
};
@@ -459,6 +461,7 @@ const oai_settings = {
custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
show_thoughts: true,
reasoning_effort: 'medium',
enable_web_search: false,
seed: -1,
n: 1,
};
@@ -2000,6 +2003,7 @@ async function sendOpenAIRequest(type, messages, signal) {
'group_names': getGroupNames(),
'include_reasoning': Boolean(oai_settings.show_thoughts),
'reasoning_effort': String(oai_settings.reasoning_effort),
'enable_web_search': Boolean(oai_settings.enable_web_search),
};
if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
@@ -3222,6 +3226,7 @@ function loadOpenAISettings(data, settings) {
oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;
oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts;
oai_settings.reasoning_effort = settings.reasoning_effort ?? default_settings.reasoning_effort;
oai_settings.enable_web_search = settings.enable_web_search ?? default_settings.enable_web_search;
oai_settings.seed = settings.seed ?? default_settings.seed;
oai_settings.n = settings.n ?? default_settings.n;
@@ -3349,6 +3354,7 @@ function loadOpenAISettings(data, settings) {
$('#seed_openai').val(oai_settings.seed);
$('#n_openai').val(oai_settings.n);
$('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts);
$('#openai_enable_web_search').prop('checked', oai_settings.enable_web_search);
$('#openai_reasoning_effort').val(oai_settings.reasoning_effort);
$(`#openai_reasoning_effort option[value="${oai_settings.reasoning_effort}"]`).prop('selected', true);
@@ -3613,6 +3619,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
function_calling: settings.function_calling,
show_thoughts: settings.show_thoughts,
reasoning_effort: settings.reasoning_effort,
enable_web_search: settings.enable_web_search,
seed: settings.seed,
n: settings.n,
};
@@ -5572,6 +5579,11 @@ export function initOpenAI() {
saveSettingsDebounced();
});
$('#openai_enable_web_search').on('input', function () {
oai_settings.enable_web_search = !!$(this).prop('checked');
saveSettingsDebounced();
});
if (!CSS.supports('field-sizing', 'content')) {
$(document).on('input', '#openai_settings .autoSetHeight', function () {
resetScrollHeight($(this));

View File

@@ -167,6 +167,8 @@ export class ReasoningHandler {
this.type = null;
/** @type {string} The reasoning output */
this.reasoning = '';
/** @type {string?} The reasoning output display in case of translate or other */
this.reasoningDisplayText = null;
/** @type {Date} When the reasoning started */
this.startTime = null;
/** @type {Date} When the reasoning ended */
@@ -234,6 +236,7 @@ export class ReasoningHandler {
this.type = extra?.reasoning_type;
this.reasoning = extra?.reasoning ?? '';
this.reasoningDisplayText = extra?.reasoning_display_text ?? null;
if (this.state !== ReasoningState.None) {
this.initialTime = new Date(chat[messageId].gen_started);
@@ -249,6 +252,7 @@ export class ReasoningHandler {
this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;
this.type = null;
this.reasoning = '';
this.reasoningDisplayText = null;
this.initialTime = new Date();
this.startTime = null;
this.endTime = null;
@@ -434,7 +438,7 @@ export class ReasoningHandler {
setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type);
// Update the reasoning message
const reasoning = trimSpaces(this.reasoning);
const reasoning = trimSpaces(this.reasoningDisplayText ?? this.reasoning);
const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
this.messageReasoningContentDom.innerHTML = displayReasoning;
@@ -888,12 +892,17 @@ function setReasoningEventHandlers() {
}
const textarea = messageBlock.find('.reasoning_edit_textarea');
updateReasoningFromValue(message, String(textarea.val()));
const newReasoning = String(textarea.val());
textarea.remove();
if (newReasoning === message.extra.reasoning) {
return;
}
updateReasoningFromValue(message, newReasoning);
await saveChatConditional();
updateMessageBlock(messageId, message);
textarea.remove();
messageBlock.find('.mes_edit_done:visible').trigger('click');
await eventSource.emit(event_types.MESSAGE_REASONING_EDITED, messageId);
});
$(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
@@ -955,6 +964,7 @@ function setReasoningEventHandlers() {
updateMessageBlock(messageId, message);
const textarea = messageBlock.find('.reasoning_edit_textarea');
textarea.remove();
await eventSource.emit(event_types.MESSAGE_REASONING_DELETED, messageId);
});
$(document).on('pointerup', '.mes_reasoning_copy', async function () {

View File

@@ -137,9 +137,14 @@ async function* parseStreamData(json) {
else if (Array.isArray(json.candidates)) {
for (let i = 0; i < json.candidates.length; i++) {
const isNotPrimary = json.candidates?.[0]?.index > 0;
const hasToolCalls = json?.candidates?.[0]?.content?.parts?.some(p => p?.functionCall);
if (isNotPrimary || json.candidates.length === 0) {
return null;
}
if (hasToolCalls) {
yield { data: json, chunk: '' };
return;
}
if (typeof json.candidates[0].content === 'object' && Array.isArray(json.candidates[i].content.parts)) {
for (let j = 0; j < json.candidates[i].content.parts.length; j++) {
if (typeof json.candidates[i].content.parts[j].text === 'string') {

View File

@@ -47,6 +47,7 @@ import {
updateMessageBlock,
printMessages,
clearChat,
unshallowCharacter,
} from '../script.js';
import {
extension_settings,
@@ -55,7 +56,7 @@ import {
renderExtensionTemplateAsync,
writeExtensionField,
} from './extensions.js';
import { groups, openGroupChat, selected_group } from './group-chats.js';
import { groups, openGroupChat, selected_group, unshallowGroupMembers } from './group-chats.js';
import { addLocaleData, getCurrentLocale, t, translate } from './i18n.js';
import { hideLoader, showLoader } from './loader.js';
import { MacrosParser } from './macros.js';
@@ -78,6 +79,7 @@ import { timestampToMoment, uuidv4 } from './utils.js';
import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
import { ChatCompletionService, TextCompletionService } from './custom-request.js';
import { updateReasoningUI } from './reasoning.js';
export function getContext() {
return {
@@ -210,6 +212,9 @@ export function getContext() {
clearChat,
ChatCompletionService,
TextCompletionService,
updateReasoningUI,
unshallowCharacter,
unshallowGroupMembers,
};
}

View File

@@ -506,6 +506,26 @@ export class ToolManager {
}
}
}
if (Array.isArray(parsed?.candidates)) {
for (let choiceIndex = 0; choiceIndex < parsed.candidates.length; choiceIndex++) {
const candidate = parsed.candidates[choiceIndex];
if (Array.isArray(candidate?.content?.parts)) {
for (let toolCallIndex = 0; toolCallIndex < candidate.content.parts.length; toolCallIndex++) {
const part = candidate.content.parts[toolCallIndex];
if (part.functionCall) {
if (!Array.isArray(toolCalls[choiceIndex])) {
toolCalls[choiceIndex] = [];
}
if (toolCalls[choiceIndex][toolCallIndex] === undefined) {
toolCalls[choiceIndex][toolCallIndex] = {};
}
const targetToolCall = toolCalls[choiceIndex][toolCallIndex];
ToolManager.#applyToolCallDelta(targetToolCall, part.functionCall);
}
}
}
}
}
}
/**
@@ -564,6 +584,7 @@ export class ToolManager {
chat_completion_sources.GROQ,
chat_completion_sources.COHERE,
chat_completion_sources.DEEPSEEK,
chat_completion_sources.MAKERSUITE,
];
return supportedSources.includes(oai_settings.chat_completion_source);
}
@@ -585,8 +606,11 @@ export class ToolManager {
* @returns {any[]} Tool calls from the response data
*/
static #getToolCallsFromData(data) {
const getRandomId = () => Math.random().toString(36).substring(2);
const isClaudeToolCall = c => Array.isArray(c) ? c.filter(x => x).every(isClaudeToolCall) : c?.input && c?.name && c?.id;
const isGoogleToolCall = c => Array.isArray(c) ? c.filter(x => x).every(isGoogleToolCall) : c?.name && c?.args;
const convertClaudeToolCall = c => ({ id: c.id, function: { name: c.name, arguments: c.input } });
const convertGoogleToolCall = (c) => ({ id: getRandomId(), function: { name: c.name, arguments: c.args } });
// Parsed tool calls from streaming data
if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) {
@@ -594,6 +618,10 @@ export class ToolManager {
return data[0].filter(x => x).map(convertClaudeToolCall);
}
if (isGoogleToolCall(data[0])) {
return data[0].filter(x => x).map(convertGoogleToolCall);
}
if (typeof data[0]?.[0]?.tool_calls === 'object') {
return Array.isArray(data[0]?.[0]?.tool_calls) ? data[0][0].tool_calls : [data[0][0].tool_calls];
}
@@ -601,6 +629,11 @@ export class ToolManager {
return data[0];
}
// Google AI Studio tool calls
if (Array.isArray(data?.responseContent?.parts)) {
return data.responseContent.parts.filter(p => p.functionCall).map(p => convertGoogleToolCall(p.functionCall));
}
// Parsed tool calls from non-streaming data
if (Array.isArray(data?.choices)) {
// Find a choice with 0-index

View File

@@ -20,6 +20,7 @@ import bodyParser from 'body-parser';
import open from 'open';
// local library imports
import { serverEvents, EVENT_NAMES } from './src/server-events.js';
import { CommandLineParser } from './src/command-line.js';
import { loadPlugins } from './src/plugin-loader.js';
import {
@@ -348,6 +349,7 @@ async function postSetupTasks(result) {
console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
setupLogLevel();
serverEvents.emit(EVENT_NAMES.SERVER_STARTED, { url: autorunUrl });
}
/**

6
src/electron/Start.bat Normal file
View File

@@ -0,0 +1,6 @@
@echo off
pushd %~dp0
call npm install --no-audit --no-fund --loglevel=error --no-progress --omit=dev
npm run start server.js %*
pause
popd

62
src/electron/index.js Normal file
View File

@@ -0,0 +1,62 @@
import { app, BrowserWindow } from 'electron';
import path from 'path';
import { fileURLToPath } from 'url';
import yargs from 'yargs';
import { serverEvents, EVENT_NAMES } from '../server-events.js';
const cliArguments = yargs(process.argv)
.usage('Usage: <your-start-script> [options]')
.option('width', {
type: 'number',
default: 800,
describe: 'The width of the window',
})
.option('height', {
type: 'number',
default: 600,
describe: 'The height of the window',
})
.parseSync();
/** @type {string} The URL to load in the window. */
let appUrl;
function createSillyTavernWindow() {
if (!appUrl) {
console.error('The server has not started yet.');
return;
}
new BrowserWindow({
height: cliArguments.height,
width: cliArguments.width,
}).loadURL(appUrl);
}
function startServer() {
return new Promise((_resolve, _reject) => {
serverEvents.addListener(EVENT_NAMES.SERVER_STARTED, ({ url }) => {
appUrl = url.toString();
createSillyTavernWindow();
});
const sillyTavernRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
process.chdir(sillyTavernRoot);
import('../../server.js');
});
}
app.whenReady().then(() => {
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createSillyTavernWindow();
}
});
startServer();
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});

802
src/electron/package-lock.json generated Normal file
View File

@@ -0,0 +1,802 @@
{
"name": "sillytavern-electron",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sillytavern-electron",
"version": "1.0.0",
"license": "AGPL-3.0",
"dependencies": {
"electron": "^35.0.0"
}
},
"node_modules/@electron/get": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
"integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"env-paths": "^2.2.0",
"fs-extra": "^8.1.0",
"got": "^11.8.5",
"progress": "^2.0.3",
"semver": "^6.2.0",
"sumchecker": "^3.0.1"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"global-agent": "^3.0.0"
}
},
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"license": "MIT",
"dependencies": {
"defer-to-connect": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@types/cacheable-request": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
"license": "MIT",
"dependencies": {
"@types/http-cache-semantics": "*",
"@types/keyv": "^3.1.4",
"@types/node": "*",
"@types/responselike": "^1.0.0"
}
},
"node_modules/@types/http-cache-semantics": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"license": "MIT"
},
"node_modules/@types/keyv": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "22.13.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/@types/responselike": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
"integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"license": "MIT",
"optional": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/boolean": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"license": "MIT",
"optional": true
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
"license": "MIT",
"engines": {
"node": ">=10.6.0"
}
},
"node_modules/cacheable-request": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
"license": "MIT",
"dependencies": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.0",
"keyv": "^4.0.0",
"lowercase-keys": "^2.0.0",
"normalize-url": "^6.0.1",
"responselike": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/clone-response": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
"license": "MIT",
"dependencies": {
"mimic-response": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/decompress-response/node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"license": "MIT",
"optional": true,
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-properties": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"license": "MIT",
"optional": true,
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/detect-node": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
"license": "MIT",
"optional": true
},
"node_modules/electron": {
"version": "35.0.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-35.0.0.tgz",
"integrity": "sha512-mwNQNktYLPnUWZVR8iNkfWCBjmM5e2/CmB1rhACwE9ASDbVU7CYPgp/jLUB3bj/LyQsfSuubD82OUite6SN8Uw==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^22.7.7",
"extract-zip": "^2.0.1"
},
"bin": {
"electron": "cli.js"
},
"engines": {
"node": ">= 12.20.55"
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
"license": "MIT",
"optional": true
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"license": "BSD-2-Clause",
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
"yauzl": "^2.10.0"
},
"bin": {
"extract-zip": "cli.js"
},
"engines": {
"node": ">= 10.17.0"
},
"optionalDependencies": {
"@types/yauzl": "^2.9.1"
}
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"license": "MIT",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/global-agent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"boolean": "^3.0.1",
"es6-error": "^4.1.1",
"matcher": "^3.0.0",
"roarr": "^2.15.3",
"semver": "^7.3.2",
"serialize-error": "^7.0.1"
},
"engines": {
"node": ">=10.0"
}
},
"node_modules/global-agent/node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/globalthis": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"define-properties": "^1.2.1",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/got": {
"version": "11.8.6",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
"license": "MIT",
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
"p-cancelable": "^2.0.0",
"responselike": "^2.0.0"
},
"engines": {
"node": ">=10.19.0"
},
"funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"license": "MIT",
"optional": true,
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"license": "BSD-2-Clause"
},
"node_modules/http2-wrapper": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
"license": "MIT",
"dependencies": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.0.0"
},
"engines": {
"node": ">=10.19.0"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"license": "MIT"
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"license": "ISC",
"optional": true
},
"node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"license": "MIT",
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
}
},
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/matcher": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
"license": "MIT",
"optional": true,
"dependencies": {
"escape-string-regexp": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/p-cancelable": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"license": "MIT"
},
"node_modules/responselike": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
"license": "MIT",
"dependencies": {
"lowercase-keys": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/roarr": {
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"boolean": "^3.0.1",
"detect-node": "^2.0.4",
"globalthis": "^1.0.1",
"json-stringify-safe": "^5.0.1",
"semver-compare": "^1.0.0",
"sprintf-js": "^1.1.2"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"license": "MIT",
"optional": true
},
"node_modules/serialize-error": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
"license": "MIT",
"optional": true,
"dependencies": {
"type-fest": "^0.13.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause",
"optional": true
},
"node_modules/sumchecker": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.0"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/type-fest": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
"license": "(MIT OR CC0-1.0)",
"optional": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"license": "MIT"
},
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"license": "MIT",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
}
}
}

16
src/electron/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "sillytavern-electron",
"version": "1.0.0",
"description": "Electron server for SillyTavern",
"license": "AGPL-3.0",
"author": "",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "electron ."
},
"dependencies": {
"electron": "^35.0.0"
}
}

11
src/electron/start.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Make sure pwd is the directory of the script
cd "$(dirname "$0")"
echo "Assuming nodejs and npm is already installed. If you haven't installed them already, do so now"
echo "Installing Electron Wrapper's Node Modules..."
npm i --no-audit --no-fund --loglevel=error --no-progress --omit=dev
echo "Starting Electron Wrapper..."
npm run start -- "$@"

View File

@@ -98,6 +98,21 @@ function getOpenRouterTransforms(request) {
}
}
/**
* Gets OpenRouter plugins based on the request.
* @param {import('express').Request} request
* @returns {any[]} OpenRouter plugins
*/
function getOpenRouterPlugins(request) {
const plugins = [];
if (request.body.enable_web_search) {
plugins.push({ 'id': 'web' });
}
return plugins;
}
/**
* Sends a request to Claude API.
* @param {express.Request} request Express request
@@ -323,6 +338,7 @@ async function sendMakerSuiteRequest(request, response) {
const model = String(request.body.model);
const stream = Boolean(request.body.stream);
const enableWebSearch = Boolean(request.body.enable_web_search);
const isThinking = model.includes('thinking');
const generationConfig = {
@@ -350,6 +366,7 @@ async function sendMakerSuiteRequest(request, response) {
model.startsWith('gemini-exp')
) && request.body.use_makersuite_sysprompt;
const tools = [];
const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, getPromptNames(request));
let safetySettings = GEMINI_SAFETY;
@@ -363,6 +380,26 @@ async function sendMakerSuiteRequest(request, response) {
}
// Most of the other models allow for setting the threshold of filters, except for HARM_CATEGORY_CIVIC_INTEGRITY, to OFF.
if (enableWebSearch) {
const searchTool = model.includes('1.5') || model.includes('1.0')
? ({ google_search_retrieval: {} })
: ({ google_search: {} });
tools.push(searchTool);
}
if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
const functionDeclarations = [];
for (const tool of request.body.tools) {
if (tool.type === 'function') {
if (tool.function.parameters?.$schema) {
delete tool.function.parameters.$schema;
}
functionDeclarations.push(tool.function);
}
}
tools.push({ function_declarations: functionDeclarations });
}
let body = {
contents: prompt.contents,
safetySettings: safetySettings,
@@ -374,6 +411,10 @@ async function sendMakerSuiteRequest(request, response) {
body.systemInstruction = prompt.system_instruction;
}
if (tools.length) {
body.tools = tools;
}
return body;
}
@@ -429,10 +470,11 @@ async function sendMakerSuiteRequest(request, response) {
}
const responseContent = candidates[0].content ?? candidates[0].output;
const functionCall = (candidates?.[0]?.content?.parts ?? []).some(part => part.functionCall);
console.warn('Google AI Studio response:', responseContent);
const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
if (!responseText) {
if (!responseText && !functionCall) {
let message = 'Google AI Studio Candidate text empty';
console.warn(message, generateResponseJson);
return response.send({ error: { message } });
@@ -1017,6 +1059,7 @@ router.post('/generate', jsonParser, function (request, response) {
headers = { ...OPENROUTER_HEADERS };
bodyParams = {
'transforms': getOpenRouterTransforms(request),
'plugins': getOpenRouterPlugins(request),
'include_reasoning': Boolean(request.body.include_reasoning),
};
@@ -1185,6 +1228,7 @@ router.post('/generate', jsonParser, function (request, response) {
*/
async function makeRequest(config, response, request, retries = 5, timeout = 5000) {
try {
controller.signal.throwIfAborted();
const fetchResponse = await fetch(endpointUrl, config);
if (request.body.stream) {

View File

@@ -23,12 +23,13 @@ import { invalidateThumbnail } from './thumbnails.js';
import { importRisuSprites } from './sprites.js';
const defaultAvatarPath = './public/img/ai4.png';
// KV-store for parsed character data
const cacheCapacity = Number(getConfigValue('cardsCacheCapacity', 100, 'number')); // MB
// With 100 MB limit it would take roughly 3000 characters to reach this limit
const characterDataCache = new MemoryLimitedMap(1024 * 1024 * cacheCapacity);
const memoryCacheCapacity = getConfigValue('performance.memoryCacheCapacity', '100mb');
const memoryCache = new MemoryLimitedMap(memoryCacheCapacity);
// Some Android devices require tighter memory management
const isAndroid = process.platform === 'android';
// Use shallow character data for the character list
const useShallowCharacters = !!getConfigValue('performance.lazyLoadCharacters', false, 'boolean');
/**
* Reads the character card from the specified image file.
@@ -39,12 +40,12 @@ const isAndroid = process.platform === 'android';
async function readCharacterData(inputFile, inputFormat = 'png') {
const stat = fs.statSync(inputFile);
const cacheKey = `${inputFile}-${stat.mtimeMs}`;
if (characterDataCache.has(cacheKey)) {
return characterDataCache.get(cacheKey);
if (memoryCache.has(cacheKey)) {
return memoryCache.get(cacheKey);
}
const result = parse(inputFile, inputFormat);
!isAndroid && characterDataCache.set(cacheKey, result);
!isAndroid && memoryCache.set(cacheKey, result);
return result;
}
@@ -60,12 +61,12 @@ async function readCharacterData(inputFile, inputFormat = 'png') {
async function writeCharacterData(inputFile, data, outputFile, request, crop = undefined) {
try {
// Reset the cache
for (const key of characterDataCache.keys()) {
for (const key of memoryCache.keys()) {
if (Buffer.isBuffer(inputFile)) {
break;
}
if (key.startsWith(inputFile)) {
characterDataCache.delete(key);
memoryCache.delete(key);
break;
}
}
@@ -200,14 +201,45 @@ const calculateDataSize = (data) => {
return typeof data === 'object' ? Object.values(data).reduce((acc, val) => acc + String(val).length, 0) : 0;
};
/**
* Only get fields that are used to display the character list.
* @param {object} character Character object
* @returns {{shallow: true, [key: string]: any}} Shallow character
*/
const toShallow = (character) => {
return {
shallow: true,
name: character.name,
avatar: character.avatar,
chat: character.chat,
fav: character.fav,
date_added: character.date_added,
create_date: character.create_date,
date_last_chat: character.date_last_chat,
chat_size: character.chat_size,
data_size: character.data_size,
data: {
name: _.get(character, 'data.name', ''),
character_version: _.get(character, 'data.character_version', ''),
creator: _.get(character, 'data.creator', ''),
creator_notes: _.get(character, 'data.creator_notes', ''),
extensions: {
fav: _.get(character, 'data.extensions.fav', false),
},
},
};
};
/**
* processCharacter - Process a given character, read its data and calculate its statistics.
*
* @param {string} item The name of the character.
* @param {import('../users.js').UserDirectoryList} directories User directories
* @param {object} options Options for the character processing
* @param {boolean} options.shallow If true, only return the core character's metadata
* @return {Promise<object>} A Promise that resolves when the character processing is done.
*/
const processCharacter = async (item, directories) => {
const processCharacter = async (item, directories, { shallow }) => {
try {
const imgFile = path.join(directories.characters, item);
const imgData = await readCharacterData(imgFile);
@@ -226,7 +258,7 @@ const processCharacter = async (item, directories) => {
character['chat_size'] = chatSize;
character['date_last_chat'] = dateLastChat;
character['data_size'] = calculateDataSize(jsonObject?.data);
return character;
return shallow ? toShallow(character) : character;
}
catch (err) {
console.error(`Could not process character: ${item}`);
@@ -993,7 +1025,7 @@ router.post('/all', jsonParser, async function (request, response) {
try {
const files = fs.readdirSync(request.user.directories.characters);
const pngFiles = files.filter(file => file.endsWith('.png'));
const processingPromises = pngFiles.map(file => processCharacter(file, request.user.directories));
const processingPromises = pngFiles.map(file => processCharacter(file, request.user.directories, { shallow: useShallowCharacters }));
const data = (await Promise.all(processingPromises)).filter(c => c.name);
return response.send(data);
} catch (err) {
@@ -1012,7 +1044,7 @@ router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (req
return response.sendStatus(404);
}
const data = await processCharacter(item, request.user.directories);
const data = await processCharacter(item, request.user.directories, { shallow: false });
return response.send(data);
} catch (err) {
@@ -1022,11 +1054,11 @@ router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (req
});
router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
if (!request.body) return response.sendStatus(400);
const characterDirectory = (request.body.avatar_url).replace('.png', '');
try {
if (!request.body) return response.sendStatus(400);
const characterDirectory = (request.body.avatar_url).replace('.png', '');
const chatsDirectory = path.join(request.user.directories.chats, characterDirectory);
if (!fs.existsSync(chatsDirectory)) {

View File

@@ -1,5 +1,5 @@
import crypto from 'node:crypto';
import { getConfigValue } from './util.js';
import { getConfigValue, tryParse } from './util.js';
const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.');
@@ -411,11 +411,12 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
}
const system_instruction = { parts: { text: sys_prompt.trim() } };
const toolNameMap = {};
const contents = [];
messages.forEach((message, index) => {
// fix the roles
if (message.role === 'system') {
if (message.role === 'system' || message.role === 'tool') {
message.role = 'user';
} else if (message.role === 'assistant') {
message.role = 'model';
@@ -423,7 +424,21 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
// Convert the content to an array of parts
if (!Array.isArray(message.content)) {
message.content = [{ type: 'text', text: String(message.content ?? '') }];
const content = (() => {
const hasToolCalls = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
const hasToolCallId = typeof message.tool_call_id === 'string' && message.tool_call_id.length > 0;
if (hasToolCalls) {
return { type: 'tool_calls', tool_calls: message.tool_calls };
}
if (hasToolCallId) {
return { type: 'tool_call_id', tool_call_id: message.tool_call_id, content: String(message.content ?? '') };
}
return { type: 'text', text: String(message.content ?? '') };
})();
message.content = [content];
}
// similar story as claude
@@ -455,6 +470,25 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
message.content.forEach((part) => {
if (part.type === 'text') {
parts.push({ text: part.text });
} else if (part.type === 'tool_call_id') {
const name = toolNameMap[part.tool_call_id] ?? 'unknown';
parts.push({
functionResponse: {
name: name,
response: { name: name, content: part.content },
},
});
} else if (part.type === 'tool_calls') {
part.tool_calls.forEach((toolCall) => {
parts.push({
functionCall: {
name: toolCall.function.name,
args: tryParse(toolCall.function.arguments) ?? toolCall.function.arguments,
},
});
toolNameMap[toolCall.id] = toolCall.function.name;
});
} else if (part.type === 'image_url' && isMultimodal) {
const mimeType = part.image_url.url.split(';')[0].split(':')[1];
const base64Data = part.image_url.url.split(',')[1];
@@ -473,7 +507,7 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
if (part.text) {
contents[contents.length - 1].parts[0].text += '\n\n' + part.text;
}
if (part.inlineData) {
if (part.inlineData || part.functionCall || part.functionResponse) {
contents[contents.length - 1].parts.push(part);
}
});

21
src/server-events.js Normal file
View File

@@ -0,0 +1,21 @@
import EventEmitter from 'node:events';
import process from 'node:process';
/**
* @typedef {import('../index').ServerEventMap} ServerEventMap
* @type {EventEmitter<ServerEventMap>} The default event source.
*/
export const serverEvents = new EventEmitter();
process.serverEvents = serverEvents;
export default serverEvents;
/**
* @enum {string}
* @readonly
*/
export const EVENT_NAMES = Object.freeze({
/**
* Emitted when the server has started.
*/
SERVER_STARTED: 'server-started',
});

View File

@@ -16,6 +16,7 @@ import mime from 'mime-types';
import { default as simpleGit } from 'simple-git';
import chalk from 'chalk';
import { LOG_LEVELS } from './constants.js';
import bytes from 'bytes';
/**
* Parsed config object.
@@ -856,14 +857,10 @@ export function setupLogLevel() {
export class MemoryLimitedMap {
/**
* Creates an instance of MemoryLimitedMap.
* @param {number} maxMemoryInBytes - The maximum allowed memory in bytes for string values.
* @param {string} cacheCapacity - Maximum memory usage in human-readable format (e.g., '1 GB').
*/
constructor(maxMemoryInBytes) {
if (typeof maxMemoryInBytes !== 'number' || maxMemoryInBytes <= 0 || isNaN(maxMemoryInBytes)) {
console.warn('Invalid maxMemoryInBytes, using a fallback value of 1 GB.');
maxMemoryInBytes = 1024 * 1024 * 1024; // 1 GB
}
this.maxMemory = maxMemoryInBytes;
constructor(cacheCapacity) {
this.maxMemory = bytes.parse(cacheCapacity) ?? 0;
this.currentMemory = 0;
this.map = new Map();
this.queue = [];
@@ -886,6 +883,10 @@ export class MemoryLimitedMap {
* @param {string} value
*/
set(key, value) {
if (this.maxMemory <= 0) {
return;
}
if (typeof key !== 'string' || typeof value !== 'string') {
return;
}