Merge branch 'staging' into reasoning-template

This commit is contained in:
Cohee
2025-03-16 22:51:28 +02:00
11 changed files with 695 additions and 106 deletions

View File

@ -39,9 +39,13 @@ jobs:
label-by-branches: label-by-branches:
name: 🏷️ Label PR by Branches name: 🏷️ Label PR by Branches
needs: [label-by-size]
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Run, even if the previous jobs were skipped/failed
# Only label once when PR is created or branches are changed, to allow manual label removal # Only label once when PR is created or branches are changed, to allow manual label removal
if: github.event.action == 'opened' || (github.event.action == 'synchronize' && (github.event.changes.base || github.event.changes.head)) if: |
always()
&& github.event.action == 'opened' || (github.event.action == 'synchronize' && (github.event.changes.base || github.event.changes.head))
steps: steps:
- name: Checkout Repository - name: Checkout Repository
@ -59,7 +63,10 @@ jobs:
label-by-files: label-by-files:
name: 🏷️ Label PR by Files name: 🏷️ Label PR by Files
needs: [label-by-branches]
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Run, even if the previous jobs were skipped/failed
if: always()
steps: steps:
- name: Checkout Repository - name: Checkout Repository
@ -77,9 +84,12 @@ jobs:
remove-stale-label: remove-stale-label:
name: 🗑️ Remove Stale Label on Comment name: 🗑️ Remove Stale Label on Comment
needs: [label-by-files]
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Only runs when this is not done by the github actions bot # Only runs when this is not done by the github actions bot
if: github.event_name == 'pull_request_review_comment' && github.actor != 'github-actions[bot]' if: |
always()
&& github.event_name == 'pull_request_review_comment' && github.actor != 'github-actions[bot]'
steps: steps:
- name: Remove Stale Label - name: Remove Stale Label
@ -94,7 +104,7 @@ jobs:
check-merge-blocking-labels: check-merge-blocking-labels:
name: 🚫 Check Merge Blocking Labels name: 🚫 Check Merge Blocking Labels
needs: [label-by-branches, label-by-files] needs: [label-by-size, label-by-branches, label-by-files, remove-stale-label]
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Run, even if the previous jobs were skipped/failed # Run, even if the previous jobs were skipped/failed
if: always() if: always()
@ -144,7 +154,7 @@ jobs:
write-auto-comments: write-auto-comments:
name: 💬 Post PR Comments Based on Labels name: 💬 Post PR Comments Based on Labels
needs: [label-by-size, label-by-branches, label-by-files] needs: [label-by-size, label-by-branches, label-by-files, remove-stale-label]
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Run, even if the previous jobs were skipped/failed # Run, even if the previous jobs were skipped/failed
if: always() if: always()

4
public/global.d.ts vendored
View File

@ -1,7 +1,11 @@
import libs from './lib'; import libs from './lib';
import getContext from './scripts/st-context'; import getContext from './scripts/st-context';
import { power_user } from './scripts/power-user';
declare global { declare global {
// Custom types
declare type InstructSettings = typeof power_user.instruct;
// Global namespace modules // Global namespace modules
interface Window { interface Window {
ai: any; ai: any;

View File

@ -514,6 +514,9 @@ export const event_types = {
ONLINE_STATUS_CHANGED: 'online_status_changed', ONLINE_STATUS_CHANGED: 'online_status_changed',
IMAGE_SWIPED: 'image_swiped', IMAGE_SWIPED: 'image_swiped',
CONNECTION_PROFILE_LOADED: 'connection_profile_loaded', CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',
CONNECTION_PROFILE_CREATED: 'connection_profile_created',
CONNECTION_PROFILE_DELETED: 'connection_profile_deleted',
CONNECTION_PROFILE_UPDATED: 'connection_profile_updated',
TOOL_CALLS_PERFORMED: 'tool_calls_performed', TOOL_CALLS_PERFORMED: 'tool_calls_performed',
TOOL_CALLS_RENDERED: 'tool_calls_rendered', TOOL_CALLS_RENDERED: 'tool_calls_rendered',
}; };
@ -1370,8 +1373,11 @@ export function resultCheckStatus() {
* If the character ID doesn't exist, if the chat is being saved, or if a group is being generated, this function does nothing. * If the character ID doesn't exist, if the chat is being saved, or if a group is being generated, this function does nothing.
* If the character is different from the currently selected one, it will clear the chat and reset any selected character or group. * If the character is different from the currently selected one, it will clear the chat and reset any selected character or group.
* @param {number} id The ID of the character to switch to. * @param {number} id The ID of the character to switch to.
* @param {object} [options] Options for the switch.
* @param {boolean} [options.switchMenu=true] Whether to switch the right menu to the character edit menu if the character is already selected.
* @returns {Promise<void>} A promise that resolves when the character is switched.
*/ */
export async function selectCharacterById(id) { export async function selectCharacterById(id, { switchMenu = true } = {}) {
if (characters[id] === undefined) { if (characters[id] === undefined) {
return; return;
} }
@ -1400,9 +1406,9 @@ export async function selectCharacterById(id) {
} }
} else { } else {
//if clicked on character that was already selected //if clicked on character that was already selected
selected_button = 'character_edit'; switchMenu && (selected_button = 'character_edit');
await unshallowCharacter(this_chid); await unshallowCharacter(this_chid);
select_selected_character(this_chid); select_selected_character(this_chid, { switchMenu });
} }
} }
@ -1787,6 +1793,7 @@ export async function getCharacters() {
body: JSON.stringify({}), body: JSON.stringify({}),
}); });
if (response.ok === true) { if (response.ok === true) {
const previousAvatar = this_chid !== undefined ? characters[this_chid]?.avatar : null;
characters.splice(0, characters.length); characters.splice(0, characters.length);
const getData = await response.json(); const getData = await response.json();
for (let i = 0; i < getData.length; i++) { for (let i = 0; i < getData.length; i++) {
@ -1800,8 +1807,16 @@ export async function getCharacters() {
characters[i]['chat'] = String(characters[i]['chat']); characters[i]['chat'] = String(characters[i]['chat']);
} }
if (this_chid !== undefined) {
$('#avatar_url_pole').val(characters[this_chid].avatar); if (previousAvatar) {
const newCharacterId = characters.findIndex(x => x.avatar === previousAvatar);
if (newCharacterId >= 0) {
setCharacterId(newCharacterId);
await selectCharacterById(newCharacterId, { switchMenu: false });
} else {
await Popup.show.text(t`ERROR: The active character is no longer available.`, t`The page will be refreshed to prevent data loss. Press "OK" to continue.`);
return location.reload();
}
} }
await getGroups(); await getGroups();
@ -6527,6 +6542,8 @@ export async function renameCharacter(name = null, { silent = false, renameChats
await eventSource.emit(event_types.CHARACTER_RENAMED, oldAvatar, newAvatar); await eventSource.emit(event_types.CHARACTER_RENAMED, oldAvatar, newAvatar);
// Unload current character
setCharacterId(undefined);
// Reload characters list // Reload characters list
await getCharacters(); await getCharacters();
@ -6535,7 +6552,6 @@ export async function renameCharacter(name = null, { silent = false, renameChats
if (newChId !== -1) { if (newChId !== -1) {
// Select the character after the renaming // Select the character after the renaming
setCharacterId(undefined);
await selectCharacterById(newChId); await selectCharacterById(newChId);
// Async delay to update UI // Async delay to update UI
@ -7882,14 +7898,19 @@ export function select_rm_info(type, charId, previousCharId = null) {
} }
} }
export function select_selected_character(chid) { /**
* Selects the right menu for displaying the character editor.
* @param {number|string} chid Character array index
* @param {object} [param1] Options for the switch
* @param {boolean} [param1.switchMenu=true] Whether to switch the menu
*/
export function select_selected_character(chid, { switchMenu = true } = {}) {
//character select //character select
//console.log('select_selected_character() -- starting with input of -- ' + chid + ' (name:' + characters[chid].name + ')'); //console.log('select_selected_character() -- starting with input of -- ' + chid + ' (name:' + characters[chid].name + ')');
select_rm_create(); select_rm_create({ switchMenu });
setMenuType('character_edit'); switchMenu && setMenuType('character_edit');
$('#delete_button').css('display', 'flex'); $('#delete_button').css('display', 'flex');
$('#export_button').css('display', 'flex'); $('#export_button').css('display', 'flex');
var display_name = characters[chid].name;
//create text poles //create text poles
$('#rm_button_back').css('display', 'none'); $('#rm_button_back').css('display', 'none');
@ -7904,7 +7925,7 @@ export function select_selected_character(chid) {
// Don't update the navbar name if we're peeking the group member defs // Don't update the navbar name if we're peeking the group member defs
if (!selected_group) { if (!selected_group) {
$('#rm_button_selected_ch').children('h2').text(display_name); $('#rm_button_selected_ch').children('h2').text(characters[chid].name);
} }
$('#add_avatar_button').val(''); $('#add_avatar_button').val('');
@ -7935,22 +7956,20 @@ export function select_selected_character(chid) {
$('#chat_import_avatar_url').val(characters[chid].avatar); $('#chat_import_avatar_url').val(characters[chid].avatar);
$('#chat_import_character_name').val(characters[chid].name); $('#chat_import_character_name').val(characters[chid].name);
$('#character_json_data').val(characters[chid].json_data); $('#character_json_data').val(characters[chid].json_data);
let this_avatar = default_avatar;
if (characters[chid].avatar != 'none') {
this_avatar = getThumbnailUrl('avatar', characters[chid].avatar);
}
updateFavButtonState(characters[chid].fav || characters[chid].fav == 'true'); updateFavButtonState(characters[chid].fav || characters[chid].fav == 'true');
$('#avatar_load_preview').attr('src', this_avatar); const avatarUrl = characters[chid].avatar != 'none' ? getThumbnailUrl('avatar', characters[chid].avatar) : default_avatar;
$('#name_div').removeClass('displayBlock'); $('#avatar_load_preview').attr('src', avatarUrl);
$('#name_div').addClass('displayNone');
$('#renameCharButton').css('display', '');
$('.open_alternate_greetings').data('chid', chid); $('.open_alternate_greetings').data('chid', chid);
$('#set_character_world').data('chid', chid); $('#set_character_world').data('chid', chid);
setWorldInfoButtonClass(chid); setWorldInfoButtonClass(chid);
checkEmbeddedWorld(chid); checkEmbeddedWorld(chid);
$('#name_div').removeClass('displayBlock');
$('#name_div').addClass('displayNone');
$('#renameCharButton').css('display', '');
$('#form_create').attr('actiontype', 'editcharacter'); $('#form_create').attr('actiontype', 'editcharacter');
$('.form_create_bottom_buttons_block .chat_lorebook_button').show(); $('.form_create_bottom_buttons_block .chat_lorebook_button').show();
@ -7962,8 +7981,13 @@ export function select_selected_character(chid) {
saveSettingsDebounced(); saveSettingsDebounced();
} }
function select_rm_create() { /**
setMenuType('create'); * Selects the right menu for creating a new character.
* @param {object} [options] Options for the switch
* @param {boolean} [options.switchMenu=true] Whether to switch the menu
*/
function select_rm_create({ switchMenu = true } = {}) {
switchMenu && setMenuType('create');
//console.log('select_rm_Create() -- selected button: '+selected_button); //console.log('select_rm_Create() -- selected button: '+selected_button);
if (selected_button == 'create') { if (selected_button == 'create') {
@ -7973,7 +7997,7 @@ function select_rm_create() {
} }
} }
selectRightMenuWithAnimation('rm_ch_create_block'); switchMenu && selectRightMenuWithAnimation('rm_ch_create_block');
$('#set_chat_scenario').hide(); $('#set_chat_scenario').hide();
$('#delete_button_div').css('display', 'none'); $('#delete_button_div').css('display', 'none');
@ -9196,6 +9220,17 @@ function swipe_right(_event, { source, repeated } = {}) {
} }
} }
/**
* @typedef {object} ConnectAPIMap
* @property {string} selected - API name (e.g. "textgenerationwebui", "openai")
* @property {string?} [button] - CSS selector for the API button
* @property {string?} [type] - API type, mostly used by text completion. (e.g. "openrouter")
* @property {string?} [source] - API source, mostly used by chat completion. (e.g. "openai")
*/
/**
* @type {Record<string, ConnectAPIMap>}
*/
export const CONNECT_API_MAP = { export const CONNECT_API_MAP = {
// Default APIs not contined inside text gen / chat gen // Default APIs not contined inside text gen / chat gen
'kobold': { 'kobold': {

View File

@ -1,20 +1,20 @@
import { getPresetManager } from './preset-manager.js'; import { getPresetManager } from './preset-manager.js';
import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js'; import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js';
import { getTextGenServer } from './textgen-settings.js'; import { getTextGenServer } from './textgen-settings.js';
import { extractReasoningFromData } from './reasoning.js';
import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js';
// #region Type Definitions // #region Type Definitions
/** /**
* @typedef {Object} TextCompletionRequestBase * @typedef {Object} TextCompletionRequestBase
* @property {string} prompt - The text prompt for completion
* @property {number} max_tokens - Maximum number of tokens to generate * @property {number} max_tokens - Maximum number of tokens to generate
* @property {string} [model] - Optional model name * @property {string} [model] - Optional model name
* @property {string} api_type - Type of API to use * @property {string} api_type - Type of API to use
* @property {string} [api_server] - Optional API server URL * @property {string} [api_server] - Optional API server URL
* @property {number} [temperature] - Optional temperature parameter * @property {number} [temperature] - Optional temperature parameter
* @property {number} [min_p] - Optional min_p parameter
*/ */
/** @typedef {Record<string, any> & TextCompletionRequestBase} TextCompletionRequest */
/** /**
* @typedef {Object} TextCompletionPayloadBase * @typedef {Object} TextCompletionPayloadBase
* @property {string} prompt - The text prompt for completion * @property {string} prompt - The text prompt for completion
@ -44,6 +44,13 @@ import { getTextGenServer } from './textgen-settings.js';
*/ */
/** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */ /** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */
/**
* @typedef {Object} ExtractedData
* @property {string} content - Extracted content.
* @property {string} reasoning - Extracted reasoning.
*/
// #endregion // #endregion
/** /**
@ -53,11 +60,11 @@ export class TextCompletionService {
static TYPE = 'textgenerationwebui'; static TYPE = 'textgenerationwebui';
/** /**
* @param {TextCompletionRequest} custom * @param {Record<string, any> & TextCompletionRequestBase & {prompt: string}} custom
* @returns {TextCompletionPayload} * @returns {TextCompletionPayload}
*/ */
static createRequestData({ prompt, max_tokens, model, api_type, api_server, temperature, ...props }) { static createRequestData({ prompt, max_tokens, model, api_type, api_server, temperature, min_p, ...props }) {
return { const payload = {
...props, ...props,
prompt, prompt,
max_tokens, max_tokens,
@ -66,15 +73,25 @@ export class TextCompletionService {
api_type, api_type,
api_server: api_server ?? getTextGenServer(api_type), api_server: api_server ?? getTextGenServer(api_type),
temperature, temperature,
min_p,
stream: false, stream: false,
}; };
// Remove undefined values to avoid API errors
Object.keys(payload).forEach(key => {
if (payload[key] === undefined) {
delete payload[key];
}
});
return payload;
} }
/** /**
* Sends a text completion request to the specified server * Sends a text completion request to the specified server
* @param {TextCompletionPayload} data Request data * @param {TextCompletionPayload} data Request data
* @param {boolean?} extractData Extract message from the response. Default true * @param {boolean?} extractData Extract message from the response. Default true
* @returns {Promise<string | any>} Extracted data or the raw response * @returns {Promise<ExtractedData | any>} Extracted data or the raw response
* @throws {Error} * @throws {Error}
*/ */
static async sendRequest(data, extractData = true) { static async sendRequest(data, extractData = true) {
@ -91,31 +108,150 @@ export class TextCompletionService {
throw json; throw json;
} }
return extractData ? extractMessageFromData(json, this.TYPE) : json; if (!extractData) {
return json;
}
return {
content: extractMessageFromData(json, this.TYPE),
reasoning: extractReasoningFromData(json, {
mainApi: this.TYPE,
textGenType: data.api_type,
ignoreShowThoughts: true,
}),
};
} }
/** /**
* @param {string} presetName * Process and send a text completion request with optional preset & instruct
* @param {TextCompletionRequest} custom * @param {Record<string, any> & TextCompletionRequestBase & {prompt: (ChatCompletionMessage & {ignoreInstruct?: boolean})[] |string}} custom
* @param {boolean?} extractData Extract message from the response. Default true * @param {Object} options - Configuration options
* @returns {Promise<string | any>} Extracted data or the raw response * @param {string?} [options.presetName] - Name of the preset to use for generation settings
* @param {string?} [options.instructName] - Name of instruct preset for message formatting
* @param {boolean} extractData - Whether to extract structured data from response
* @returns {Promise<ExtractedData | any>} Extracted data or the raw response
* @throws {Error} * @throws {Error}
*/ */
static async sendRequestWithPreset(presetName, custom, extractData = true) { static async processRequest(
const presetManager = getPresetManager(this.TYPE); custom,
if (!presetManager) { options = {},
throw new Error('Preset manager not found'); extractData = true,
) {
const { presetName, instructName } = options;
let requestData = { ...custom };
const prompt = custom.prompt;
// Apply generation preset if specified
if (presetName) {
const presetManager = getPresetManager(this.TYPE);
if (presetManager) {
const preset = presetManager.getCompletionPresetByName(presetName);
if (preset) {
// Convert preset to payload and merge with custom parameters
const presetPayload = this.presetToGeneratePayload(preset, {});
requestData = { ...presetPayload, ...requestData };
} else {
console.warn(`Preset "${presetName}" not found, continuing with default settings`);
}
} else {
console.warn('Preset manager not found, continuing with default settings');
}
} }
const preset = presetManager.getCompletionPresetByName(presetName); // Handle instruct formatting if requested
if (!preset) { if (Array.isArray(prompt) && instructName) {
throw new Error('Preset not found'); const instructPresetManager = getPresetManager('instruct');
let instructPreset = instructPresetManager?.getCompletionPresetByName(instructName);
if (instructPreset) {
// Clone the preset to avoid modifying the original
instructPreset = structuredClone(instructPreset);
instructPreset.macro = false;
instructPreset.names_behavior = names_behavior_types.NONE;
// Format messages using instruct formatting
const formattedMessages = [];
for (const message of prompt) {
let messageContent = message.content;
if (!message.ignoreInstruct) {
messageContent = formatInstructModeChat(
message.role,
message.content,
message.role === 'user',
false,
undefined,
undefined,
undefined,
undefined,
instructPreset,
);
// Add prompt formatting for the last message
if (message === prompt[prompt.length - 1]) {
messageContent += formatInstructModePrompt(
undefined,
false,
undefined,
undefined,
undefined,
false,
false,
instructPreset,
);
}
}
formattedMessages.push(messageContent);
}
requestData.prompt = formattedMessages.join('');
if (instructPreset.output_suffix) {
requestData.stop = [instructPreset.output_suffix];
requestData.stopping_strings = [instructPreset.output_suffix];
}
} else {
console.warn(`Instruct preset "${instructName}" not found, using basic formatting`);
requestData.prompt = prompt.map(x => x.content).join('\n\n');
}
} else if (typeof prompt === 'string') {
requestData.prompt = prompt;
} else {
requestData.prompt = prompt.map(x => x.content).join('\n\n');
} }
const data = this.createRequestData({ ...preset, ...custom }); // @ts-ignore
const data = this.createRequestData(requestData);
return await this.sendRequest(data, extractData); return await this.sendRequest(data, extractData);
} }
/**
* Converts a preset to a valid text completion payload.
* Only supports temperature.
* @param {Object} preset - The preset configuration
* @param {Object} customPreset - Additional parameters to override preset values
* @returns {Object} - Formatted payload for text completion API
*/
static presetToGeneratePayload(preset, customPreset = {}) {
if (!preset || typeof preset !== 'object') {
throw new Error('Invalid preset: must be an object');
}
// Merge preset with custom parameters
const settings = { ...preset, ...customPreset };
// Initialize base payload with common parameters
let payload = {
'temperature': settings.temp ? Number(settings.temp) : undefined,
'min_p': settings.min_p ? Number(settings.min_p) : undefined,
};
// Remove undefined values to avoid API errors
Object.keys(payload).forEach(key => {
if (payload[key] === undefined) {
delete payload[key];
}
});
return payload;
}
} }
/** /**
@ -129,7 +265,7 @@ export class ChatCompletionService {
* @returns {ChatCompletionPayload} * @returns {ChatCompletionPayload}
*/ */
static createRequestData({ messages, model, chat_completion_source, max_tokens, temperature, ...props }) { static createRequestData({ messages, model, chat_completion_source, max_tokens, temperature, ...props }) {
return { const payload = {
...props, ...props,
messages, messages,
model, model,
@ -138,13 +274,22 @@ export class ChatCompletionService {
temperature, temperature,
stream: false, stream: false,
}; };
// Remove undefined values to avoid API errors
Object.keys(payload).forEach(key => {
if (payload[key] === undefined) {
delete payload[key];
}
});
return payload;
} }
/** /**
* Sends a chat completion request * Sends a chat completion request
* @param {ChatCompletionPayload} data Request data * @param {ChatCompletionPayload} data Request data
* @param {boolean?} extractData Extract message from the response. Default true * @param {boolean?} extractData Extract message from the response. Default true
* @returns {Promise<string | any>} Extracted data or the raw response * @returns {Promise<ExtractedData | any>} Extracted data or the raw response
* @throws {Error} * @throws {Error}
*/ */
static async sendRequest(data, extractData = true) { static async sendRequest(data, extractData = true) {
@ -161,29 +306,82 @@ export class ChatCompletionService {
throw json; throw json;
} }
return extractData ? extractMessageFromData(json, this.TYPE) : json; if (!extractData) {
return json;
}
return {
content: extractMessageFromData(json, this.TYPE),
reasoning: extractReasoningFromData(json, {
mainApi: this.TYPE,
textGenType: data.chat_completion_source,
ignoreShowThoughts: true,
}),
};
} }
/** /**
* @param {string} presetName * Process and send a chat completion request with optional preset
* @param {ChatCompletionPayload} custom * @param {ChatCompletionPayload} custom
* @param {boolean} extractData Extract message from the response. Default true * @param {Object} options - Configuration options
* @returns {Promise<string | any>} Extracted data or the raw response * @param {string?} [options.presetName] - Name of the preset to use for generation settings
* @param {boolean} extractData - Whether to extract structured data from response
* @returns {Promise<ExtractedData | any>} Extracted data or the raw response
* @throws {Error} * @throws {Error}
*/ */
static async sendRequestWithPreset(presetName, custom, extractData = true) { static async processRequest(custom, options, extractData = true) {
const presetManager = getPresetManager(this.TYPE); const { presetName } = options;
if (!presetManager) { let requestData = { ...custom };
throw new Error('Preset manager not found');
// Apply generation preset if specified
if (presetName) {
const presetManager = getPresetManager(this.TYPE);
if (presetManager) {
const preset = presetManager.getCompletionPresetByName(presetName);
if (preset) {
// Convert preset to payload and merge with custom parameters
const presetPayload = this.presetToGeneratePayload(preset, {});
requestData = { ...presetPayload, ...requestData };
} else {
console.warn(`Preset "${presetName}" not found, continuing with default settings`);
}
} else {
console.warn('Preset manager not found, continuing with default settings');
}
} }
const preset = presetManager.getCompletionPresetByName(presetName); const data = this.createRequestData(requestData);
if (!preset) {
throw new Error('Preset not found');
}
const data = this.createRequestData({ ...preset, ...custom });
return await this.sendRequest(data, extractData); return await this.sendRequest(data, extractData);
} }
/**
* Converts a preset to a valid chat completion payload
* Only supports temperature.
* @param {Object} preset - The preset configuration
* @param {Object} customParams - Additional parameters to override preset values
* @returns {Object} - Formatted payload for chat completion API
*/
static presetToGeneratePayload(preset, customParams = {}) {
if (!preset || typeof preset !== 'object') {
throw new Error('Invalid preset: must be an object');
}
// Merge preset with custom parameters
const settings = { ...preset, ...customParams };
// Initialize base payload with common parameters
const payload = {
temperature: settings.temperature ? Number(settings.temperature) : undefined,
};
// Remove undefined values to avoid API errors
Object.keys(payload).forEach(key => {
if (payload[key] === undefined) {
delete payload[key];
}
});
return payload;
}
} }

View File

@ -1,4 +1,4 @@
import { Fuse } from '../../../lib.js'; import { DOMPurify, Fuse } from '../../../lib.js';
import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js'; import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js';
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js'; import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
@ -271,11 +271,16 @@ async function createConnectionProfile(forceName = null) {
}); });
const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n); const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n);
const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken); const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken);
const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 }); let name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 });
// If it's cancelled, it will be false
if (!name) { if (!name) {
return null; return null;
} }
name = DOMPurify.sanitize(String(name));
if (!name) {
toastr.error('Name cannot be empty.');
return null;
}
if (isNameTaken(name) || name === NONE) { if (isNameTaken(name) || name === NONE) {
toastr.error('A profile with the same name already exists.'); toastr.error('A profile with the same name already exists.');
@ -307,7 +312,8 @@ async function deleteConnectionProfile() {
return; return;
} }
const name = extension_settings.connectionManager.profiles[index].name; const profile = extension_settings.connectionManager.profiles[index];
const name = profile.name;
const confirm = await Popup.show.confirm(t`Are you sure you want to delete the selected profile?`, name); const confirm = await Popup.show.confirm(t`Are you sure you want to delete the selected profile?`, name);
if (!confirm) { if (!confirm) {
@ -317,6 +323,8 @@ async function deleteConnectionProfile() {
extension_settings.connectionManager.profiles.splice(index, 1); extension_settings.connectionManager.profiles.splice(index, 1);
extension_settings.connectionManager.selectedProfile = null; extension_settings.connectionManager.selectedProfile = null;
saveSettingsDebounced(); saveSettingsDebounced();
await eventSource.emit(event_types.CONNECTION_PROFILE_DELETED, profile);
} }
/** /**
@ -516,6 +524,7 @@ async function renderDetailsContent(detailsContent) {
saveSettingsDebounced(); saveSettingsDebounced();
renderConnectionProfiles(profiles); renderConnectionProfiles(profiles);
await renderDetailsContent(detailsContent); await renderDetailsContent(detailsContent);
await eventSource.emit(event_types.CONNECTION_PROFILE_CREATED, profile);
await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);
}); });
@ -527,9 +536,11 @@ async function renderDetailsContent(detailsContent) {
console.log('No profile selected'); console.log('No profile selected');
return; return;
} }
const oldProfile = structuredClone(profile);
await updateConnectionProfile(profile); await updateConnectionProfile(profile);
await renderDetailsContent(detailsContent); await renderDetailsContent(detailsContent);
saveSettingsDebounced(); saveSettingsDebounced();
await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);
toastr.success('Connection profile updated', '', { timeOut: 1500 }); toastr.success('Connection profile updated', '', { timeOut: 1500 });
}); });
@ -563,7 +574,7 @@ async function renderDetailsContent(detailsContent) {
return acc; return acc;
}, {}); }, {});
const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings })); const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings }));
const newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, { let newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, {
rows: 2, rows: 2,
customButtons: [{ customButtons: [{
text: t`Save and Update`, text: t`Save and Update`,
@ -575,9 +586,15 @@ async function renderDetailsContent(detailsContent) {
}], }],
}); });
// If it's cancelled, it will be false
if (!newName) { if (!newName) {
return; return;
} }
newName = DOMPurify.sanitize(String(newName));
if (!newName) {
toastr.error('Name cannot be empty.');
return;
}
if (profile.name !== newName && extension_settings.connectionManager.profiles.some(p => p.name === newName)) { if (profile.name !== newName && extension_settings.connectionManager.profiles.some(p => p.name === newName)) {
toastr.error('A profile with the same name already exists.'); toastr.error('A profile with the same name already exists.');
@ -588,6 +605,7 @@ async function renderDetailsContent(detailsContent) {
return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0]; return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0];
}).get(); }).get();
const oldProfile = structuredClone(profile);
if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) { if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) {
profile.exclude = newExcludeList; profile.exclude = newExcludeList;
for (const command of newExcludeList) { for (const command of newExcludeList) {
@ -602,10 +620,11 @@ async function renderDetailsContent(detailsContent) {
if (profile.name !== newName) { if (profile.name !== newName) {
toastr.success('Connection profile renamed.'); toastr.success('Connection profile renamed.');
profile.name = String(newName); profile.name = newName;
} }
saveSettingsDebounced(); saveSettingsDebounced();
await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
renderConnectionProfiles(profiles); renderConnectionProfiles(profiles);
await renderDetailsContent(detailsContent); await renderDetailsContent(detailsContent);
}); });
@ -708,6 +727,7 @@ async function renderDetailsContent(detailsContent) {
saveSettingsDebounced(); saveSettingsDebounced();
renderConnectionProfiles(profiles); renderConnectionProfiles(profiles);
await renderDetailsContent(detailsContent); await renderDetailsContent(detailsContent);
await eventSource.emit(event_types.CONNECTION_PROFILE_CREATED, profile);
return profile.name; return profile.name;
}, },
})); }));
@ -722,9 +742,11 @@ async function renderDetailsContent(detailsContent) {
toastr.warning('No profile selected.'); toastr.warning('No profile selected.');
return ''; return '';
} }
const oldProfile = structuredClone(profile);
await updateConnectionProfile(profile); await updateConnectionProfile(profile);
await renderDetailsContent(detailsContent); await renderDetailsContent(detailsContent);
saveSettingsDebounced(); saveSettingsDebounced();
await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
return profile.name; return profile.name;
}, },
})); }));

View File

@ -1,5 +1,6 @@
import { getRequestHeaders } from '../../script.js'; import { CONNECT_API_MAP, getRequestHeaders } from '../../script.js';
import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js'; import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';
import { t } from '../i18n.js';
import { oai_settings } from '../openai.js'; import { oai_settings } from '../openai.js';
import { SECRET_KEYS, secret_state } from '../secrets.js'; import { SECRET_KEYS, secret_state } from '../secrets.js';
import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js'; import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
@ -273,3 +274,309 @@ export async function getWebLlmContextSize() {
const model = await engine.getCurrentModelInfo(); const model = await engine.getCurrentModelInfo();
return model?.context_size; return model?.context_size;
} }
/**
* It uses the profiles to send a generate request to the API. Doesn't support streaming.
*/
export class ConnectionManagerRequestService {
static defaultSendRequestParams = {
extractData: true,
includePreset: true,
includeInstruct: true,
};
static getAllowedTypes() {
return {
openai: t`Chat Completion`,
textgenerationwebui: t`Text Completion`,
};
}
/**
* @param {string} profileId
* @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt
* @param {number} maxTokens
* @param {{extractData?: boolean, includePreset?: boolean, includeInstruct?: boolean}} custom - default values are true
* @returns {Promise<import('../custom-request.js').ExtractedData | any>} Extracted data or the raw response
*/
static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) {
const { extractData, includePreset, includeInstruct } = { ...this.defaultSendRequestParams, ...custom };
const context = SillyTavern.getContext();
if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
throw new Error('Connection Manager is not available');
}
const profile = context.extensionSettings.connectionManager.profiles.find((p) => p.id === profileId);
const selectedApiMap = this.validateProfile(profile);
try {
switch (selectedApiMap.selected) {
case 'openai': {
if (!selectedApiMap.source) {
throw new Error(`API type ${selectedApiMap.selected} does not support chat completions`);
}
const messages = Array.isArray(prompt) ? prompt : [{ role: 'user', content: prompt }];
return await context.ChatCompletionService.processRequest({
messages,
max_tokens: maxTokens,
model: profile.model,
chat_completion_source: selectedApiMap.source,
}, {
presetName: includePreset ? profile.preset : undefined,
}, extractData);
}
case 'textgenerationwebui': {
if (!selectedApiMap.type) {
throw new Error(`API type ${selectedApiMap.selected} does not support text completions`);
}
return await context.TextCompletionService.processRequest({
prompt,
max_tokens: maxTokens,
model: profile.model,
api_type: selectedApiMap.type,
api_server: profile['api-url'],
}, {
instructName: includeInstruct ? profile.instruct : undefined,
presetName: includePreset ? profile.preset : undefined,
}, extractData);
}
default: {
throw new Error(`Unknown API type ${selectedApiMap.selected}`);
}
}
} catch (error) {
throw new Error('API request failed', { cause: error });
}
}
/**
* Respects allowed types.
* @returns {import('./connection-manager/index.js').ConnectionProfile[]}
*/
static getSupportedProfiles() {
const context = SillyTavern.getContext();
if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
throw new Error('Connection Manager is not available');
}
const profiles = context.extensionSettings.connectionManager.profiles;
return profiles.filter((p) => this.isProfileSupported(p));
}
/**
* @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]
* @returns {boolean}
*/
static isProfileSupported(profile) {
if (!profile) {
return false;
}
const apiMap = CONNECT_API_MAP[profile.api];
if (!Object.hasOwn(this.getAllowedTypes(), apiMap.selected)) {
return false;
}
// Some providers not need model, like koboldcpp. But I don't want to check by provider.
switch (apiMap.selected) {
case 'openai':
return !!apiMap.source;
case 'textgenerationwebui':
return !!apiMap.type;
}
return false;
}
/**
* @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]
* @return {import('../../script.js').ConnectAPIMap}
* @throws {Error}
*/
static validateProfile(profile) {
if (!profile) {
throw new Error('Could not find profile.');
}
if (!profile.api) {
throw new Error('Select a connection profile that has an API');
}
const context = SillyTavern.getContext();
const selectedApiMap = context.CONNECT_API_MAP[profile.api];
if (!selectedApiMap) {
throw new Error(`Unknown API type ${profile.api}`);
}
if (!Object.hasOwn(this.getAllowedTypes(), selectedApiMap.selected)) {
throw new Error(`API type ${selectedApiMap.selected} is not supported. Supported types: ${Object.values(this.getAllowedTypes()).join(', ')}`);
}
return selectedApiMap;
}
/**
* Create profiles dropdown and updates select element accordingly. Use onChange, onCreate, unUpdate, onDelete callbacks for custom behaviour. e.g updating extension settings.
* @param {string} selector
* @param {string} initialSelectedProfileId
* @param {(profile?: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} onChange - 3 cases. 1- When user selects new profile. 2- When user deletes selected profile. 3- When user updates selected profile.
* @param {(profile: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} onCreate
* @param {(oldProfile: import('./connection-manager/index.js').ConnectionProfile, newProfile: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} unUpdate
* @param {(profile: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} onDelete
*/
static handleDropdown(
selector,
initialSelectedProfileId,
onChange = () => { },
onCreate = () => { },
unUpdate = () => { },
onDelete = () => { },
) {
const context = SillyTavern.getContext();
if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
throw new Error('Connection Manager is not available');
}
/**
* @type {JQuery<HTMLSelectElement>}
*/
const dropdown = $(selector);
if (!dropdown || !dropdown.length) {
throw new Error(`Could not find dropdown with selector ${selector}`);
}
dropdown.empty();
// Create default option using document.createElement
const defaultOption = document.createElement('option');
defaultOption.value = '';
defaultOption.textContent = 'Select a Connection Profile';
defaultOption.dataset.i18n = 'Select a Connection Profile';
dropdown.append(defaultOption);
const profiles = context.extensionSettings.connectionManager.profiles;
// Create optgroups using document.createElement
const groups = {};
for (const [apiType, groupLabel] of Object.entries(this.getAllowedTypes())) {
const optgroup = document.createElement('optgroup');
optgroup.label = groupLabel;
groups[apiType] = optgroup;
}
const sortedProfilesByGroup = {};
for (const apiType of Object.keys(this.getAllowedTypes())) {
sortedProfilesByGroup[apiType] = [];
}
for (const profile of profiles) {
if (this.isProfileSupported(profile)) {
const apiMap = CONNECT_API_MAP[profile.api];
if (sortedProfilesByGroup[apiMap.selected]) {
sortedProfilesByGroup[apiMap.selected].push(profile);
}
}
}
// Sort each group alphabetically and add to dropdown
for (const [apiType, groupProfiles] of Object.entries(sortedProfilesByGroup)) {
if (groupProfiles.length === 0) continue;
groupProfiles.sort((a, b) => a.name.localeCompare(b.name));
const group = groups[apiType];
for (const profile of groupProfiles) {
const option = document.createElement('option');
option.value = profile.id;
option.textContent = profile.name;
group.appendChild(option);
}
}
for (const group of Object.values(groups)) {
if (group.children.length > 0) {
dropdown.append(group);
}
}
const selectedProfile = profiles.find((p) => p.id === initialSelectedProfileId);
if (selectedProfile) {
dropdown.val(selectedProfile.id);
}
context.eventSource.on(context.eventTypes.CONNECTION_PROFILE_CREATED, async (profile) => {
const isSupported = this.isProfileSupported(profile);
if (!isSupported) {
return;
}
const group = groups[CONNECT_API_MAP[profile.api].selected];
const option = document.createElement('option');
option.value = profile.id;
option.textContent = profile.name;
group.appendChild(option);
await onCreate(profile);
});
context.eventSource.on(context.eventTypes.CONNECTION_PROFILE_UPDATED, async (oldProfile, newProfile) => {
const currentSelected = dropdown.val();
const isSelectedProfile = currentSelected === oldProfile.id;
await unUpdate(oldProfile, newProfile);
if (!this.isProfileSupported(newProfile)) {
if (isSelectedProfile) {
dropdown.val('');
dropdown.trigger('change');
}
return;
}
const group = groups[CONNECT_API_MAP[newProfile.api].selected];
const oldOption = group.querySelector(`option[value="${oldProfile.id}"]`);
if (oldOption) {
oldOption.remove();
}
const option = document.createElement('option');
option.value = newProfile.id;
option.textContent = newProfile.name;
group.appendChild(option);
if (isSelectedProfile) {
// Ackchyually, we don't need to reselect but what if id changes? It is not possible for now I couldn't stop myself.
dropdown.val(newProfile.id);
dropdown.trigger('change');
}
});
context.eventSource.on(context.eventTypes.CONNECTION_PROFILE_DELETED, async (profile) => {
const currentSelected = dropdown.val();
const isSelectedProfile = currentSelected === profile.id;
if (!this.isProfileSupported(profile)) {
return;
}
const group = groups[CONNECT_API_MAP[profile.api].selected];
const optionToRemove = group.querySelector(`option[value="${profile.id}"]`);
if (optionToRemove) {
optionToRemove.remove();
}
if (isSelectedProfile) {
dropdown.val('');
dropdown.trigger('change');
}
await onDelete(profile);
});
dropdown.on('change', async () => {
const profileId = dropdown.val();
const profile = context.extensionSettings.connectionManager.profiles.find((p) => p.id === profileId);
await onChange(profile);
});
}
}

View File

@ -320,59 +320,61 @@ export const force_output_sequence = {
* @param {string} name1 User name. * @param {string} name1 User name.
* @param {string} name2 Character name. * @param {string} name2 Character name.
* @param {boolean|number} forceOutputSequence Force to use first/last output sequence (if configured). * @param {boolean|number} forceOutputSequence Force to use first/last output sequence (if configured).
* @param {InstructSettings} customInstruct Custom instruct mode settings.
* @returns {string} Formatted instruct mode chat message. * @returns {string} Formatted instruct mode chat message.
*/ */
export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2, forceOutputSequence) { export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2, forceOutputSequence, customInstruct = null) {
let includeNames = isNarrator ? false : power_user.instruct.names_behavior === names_behavior_types.ALWAYS; const instruct = structuredClone(customInstruct ?? power_user.instruct);
let includeNames = isNarrator ? false : instruct.names_behavior === names_behavior_types.ALWAYS;
if (!isNarrator && power_user.instruct.names_behavior === names_behavior_types.FORCE && ((selected_group && name !== name1) || (forceAvatar && name !== name1))) { if (!isNarrator && instruct.names_behavior === names_behavior_types.FORCE && ((selected_group && name !== name1) || (forceAvatar && name !== name1))) {
includeNames = true; includeNames = true;
} }
function getPrefix() { function getPrefix() {
if (isNarrator) { if (isNarrator) {
return power_user.instruct.system_same_as_user ? power_user.instruct.input_sequence : power_user.instruct.system_sequence; return instruct.system_same_as_user ? instruct.input_sequence : instruct.system_sequence;
} }
if (isUser) { if (isUser) {
if (forceOutputSequence === force_output_sequence.FIRST) { if (forceOutputSequence === force_output_sequence.FIRST) {
return power_user.instruct.first_input_sequence || power_user.instruct.input_sequence; return instruct.first_input_sequence || instruct.input_sequence;
} }
if (forceOutputSequence === force_output_sequence.LAST) { if (forceOutputSequence === force_output_sequence.LAST) {
return power_user.instruct.last_input_sequence || power_user.instruct.input_sequence; return instruct.last_input_sequence || instruct.input_sequence;
} }
return power_user.instruct.input_sequence; return instruct.input_sequence;
} }
if (forceOutputSequence === force_output_sequence.FIRST) { if (forceOutputSequence === force_output_sequence.FIRST) {
return power_user.instruct.first_output_sequence || power_user.instruct.output_sequence; return instruct.first_output_sequence || instruct.output_sequence;
} }
if (forceOutputSequence === force_output_sequence.LAST) { if (forceOutputSequence === force_output_sequence.LAST) {
return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence; return instruct.last_output_sequence || instruct.output_sequence;
} }
return power_user.instruct.output_sequence; return instruct.output_sequence;
} }
function getSuffix() { function getSuffix() {
if (isNarrator) { if (isNarrator) {
return power_user.instruct.system_same_as_user ? power_user.instruct.input_suffix : power_user.instruct.system_suffix; return instruct.system_same_as_user ? instruct.input_suffix : instruct.system_suffix;
} }
if (isUser) { if (isUser) {
return power_user.instruct.input_suffix; return instruct.input_suffix;
} }
return power_user.instruct.output_suffix; return instruct.output_suffix;
} }
let prefix = getPrefix() || ''; let prefix = getPrefix() || '';
let suffix = getSuffix() || ''; let suffix = getSuffix() || '';
if (power_user.instruct.macro) { if (instruct.macro) {
prefix = substituteParams(prefix, name1, name2); prefix = substituteParams(prefix, name1, name2);
prefix = prefix.replace(/{{name}}/gi, name || 'System'); prefix = prefix.replace(/{{name}}/gi, name || 'System');
@ -380,11 +382,11 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
suffix = suffix.replace(/{{name}}/gi, name || 'System'); suffix = suffix.replace(/{{name}}/gi, name || 'System');
} }
if (!suffix && power_user.instruct.wrap) { if (!suffix && instruct.wrap) {
suffix = '\n'; suffix = '\n';
} }
const separator = power_user.instruct.wrap ? '\n' : ''; const separator = instruct.wrap ? '\n' : '';
// Don't include the name if it's empty // Don't include the name if it's empty
const textArray = includeNames && name ? [prefix, `${name}: ${mes}` + suffix] : [prefix, mes + suffix]; const textArray = includeNames && name ? [prefix, `${name}: ${mes}` + suffix] : [prefix, mes + suffix];
@ -504,30 +506,32 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
* @param {string} name2 Character name. * @param {string} name2 Character name.
* @param {boolean} isQuiet Is quiet mode generation. * @param {boolean} isQuiet Is quiet mode generation.
* @param {boolean} isQuietToLoud Is quiet to loud generation. * @param {boolean} isQuietToLoud Is quiet to loud generation.
* @param {InstructSettings} customInstruct Custom instruct settings.
* @returns {string} Formatted instruct mode last prompt line. * @returns {string} Formatted instruct mode last prompt line.
*/ */
export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, isQuietToLoud) { export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, isQuietToLoud, customInstruct = null) {
const includeNames = name && (power_user.instruct.names_behavior === names_behavior_types.ALWAYS || (!!selected_group && power_user.instruct.names_behavior === names_behavior_types.FORCE)) && !(isQuiet && !isQuietToLoud); const instruct = structuredClone(customInstruct ?? power_user.instruct);
const includeNames = name && (instruct.names_behavior === names_behavior_types.ALWAYS || (!!selected_group && instruct.names_behavior === names_behavior_types.FORCE)) && !(isQuiet && !isQuietToLoud);
function getSequence() { function getSequence() {
// User impersonation prompt // User impersonation prompt
if (isImpersonate) { if (isImpersonate) {
return power_user.instruct.input_sequence; return instruct.input_sequence;
} }
// Neutral / system / quiet prompt // Neutral / system / quiet prompt
// Use a special quiet instruct sequence if defined, or assistant's output sequence otherwise // Use a special quiet instruct sequence if defined, or assistant's output sequence otherwise
if (isQuiet && !isQuietToLoud) { if (isQuiet && !isQuietToLoud) {
return power_user.instruct.last_system_sequence || power_user.instruct.output_sequence; return instruct.last_system_sequence || instruct.output_sequence;
} }
// Quiet in-character prompt // Quiet in-character prompt
if (isQuiet && isQuietToLoud) { if (isQuiet && isQuietToLoud) {
return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence; return instruct.last_output_sequence || instruct.output_sequence;
} }
// Default AI response // Default AI response
return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence; return instruct.last_output_sequence || instruct.output_sequence;
} }
let sequence = getSequence() || ''; let sequence = getSequence() || '';
@ -536,21 +540,21 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
// A hack for Mistral's formatting that has a normal output sequence ending with a space // A hack for Mistral's formatting that has a normal output sequence ending with a space
if ( if (
includeNames && includeNames &&
power_user.instruct.last_output_sequence && instruct.last_output_sequence &&
power_user.instruct.output_sequence && instruct.output_sequence &&
sequence === power_user.instruct.last_output_sequence && sequence === instruct.last_output_sequence &&
/\s$/.test(power_user.instruct.output_sequence) && /\s$/.test(instruct.output_sequence) &&
!/\s$/.test(power_user.instruct.last_output_sequence) !/\s$/.test(instruct.last_output_sequence)
) { ) {
nameFiller = power_user.instruct.output_sequence.slice(-1); nameFiller = instruct.output_sequence.slice(-1);
} }
if (power_user.instruct.macro) { if (instruct.macro) {
sequence = substituteParams(sequence, name1, name2); sequence = substituteParams(sequence, name1, name2);
sequence = sequence.replace(/{{name}}/gi, name || 'System'); sequence = sequence.replace(/{{name}}/gi, name || 'System');
} }
const separator = power_user.instruct.wrap ? '\n' : ''; const separator = instruct.wrap ? '\n' : '';
let text = includeNames ? (separator + sequence + separator + nameFiller + `${name}:`) : (separator + sequence); let text = includeNames ? (separator + sequence + separator + nameFiller + `${name}:`) : (separator + sequence);
// Quiet prompt already has a newline at the end // Quiet prompt already has a newline at the end
@ -562,7 +566,7 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
text += (includeNames ? promptBias : (separator + promptBias.trimStart())); text += (includeNames ? promptBias : (separator + promptBias.trimStart()));
} }
return (power_user.instruct.wrap ? text.trimEnd() : text) + (includeNames ? '' : separator); return (instruct.wrap ? text.trimEnd() : text) + (includeNames ? '' : separator);
} }
/** /**

View File

@ -219,7 +219,9 @@ let power_user = {
system_sequence: '', system_sequence: '',
system_suffix: '', system_suffix: '',
last_system_sequence: '', last_system_sequence: '',
first_input_sequence: '',
first_output_sequence: '', first_output_sequence: '',
last_input_sequence: '',
last_output_sequence: '', last_output_sequence: '',
system_sequence_prefix: '', system_sequence_prefix: '',
system_sequence_suffix: '', system_sequence_suffix: '',

View File

@ -87,19 +87,24 @@ function toggleReasoningAutoExpand() {
* @param {object} data Response data * @param {object} data Response data
* @returns {string} Extracted reasoning * @returns {string} Extracted reasoning
*/ */
export function extractReasoningFromData(data) { export function extractReasoningFromData(data, {
switch (main_api) { mainApi = null,
ignoreShowThoughts = false,
textGenType = null,
chatCompletionSource = null
} = {}) {
switch (mainApi ?? main_api) {
case 'textgenerationwebui': case 'textgenerationwebui':
switch (textgenerationwebui_settings.type) { switch (textGenType ?? textgenerationwebui_settings.type) {
case textgen_types.OPENROUTER: case textgen_types.OPENROUTER:
return data?.choices?.[0]?.reasoning ?? ''; return data?.choices?.[0]?.reasoning ?? '';
} }
break; break;
case 'openai': case 'openai':
if (!oai_settings.show_thoughts) break; if (!ignoreShowThoughts && !oai_settings.show_thoughts) break;
switch (oai_settings.chat_completion_source) { switch (chatCompletionSource ?? oai_settings.chat_completion_source) {
case chat_completion_sources.DEEPSEEK: case chat_completion_sources.DEEPSEEK:
return data?.choices?.[0]?.message?.reasoning_content ?? ''; return data?.choices?.[0]?.message?.reasoning_content ?? '';
case chat_completion_sources.OPENROUTER: case chat_completion_sources.OPENROUTER:

View File

@ -80,6 +80,7 @@ import { timestampToMoment, uuidv4 } from './utils.js';
import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js'; import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js'; import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
import { ChatCompletionService, TextCompletionService } from './custom-request.js'; import { ChatCompletionService, TextCompletionService } from './custom-request.js';
import { ConnectionManagerRequestService } from './extensions/shared.js';
import { updateReasoningUI, parseReasoningFromString } from './reasoning.js'; import { updateReasoningUI, parseReasoningFromString } from './reasoning.js';
export function getContext() { export function getContext() {
@ -215,6 +216,7 @@ export function getContext() {
clearChat, clearChat,
ChatCompletionService, ChatCompletionService,
TextCompletionService, TextCompletionService,
ConnectionManagerRequestService,
updateReasoningUI, updateReasoningUI,
parseReasoningFromString, parseReasoningFromString,
unshallowCharacter, unshallowCharacter,

View File

@ -86,7 +86,7 @@ const OOBA_DEFAULT_ORDER = [
'encoder_repetition_penalty', 'encoder_repetition_penalty',
'no_repeat_ngram', 'no_repeat_ngram',
]; ];
const APHRODITE_DEFAULT_ORDER = [ export const APHRODITE_DEFAULT_ORDER = [
'dry', 'dry',
'penalties', 'penalties',
'no_repeat_ngram', 'no_repeat_ngram',