Merge branch 'staging' into reasoning-template

cf2671c6d77b53a165bf8a5862d85e18625b7394

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

11 files changed, +692 -103Showing whitespace changes
.github/workflows/pr-auto-manager.yml+14 -4
@@ -39,9 +39,13 @@ jobs:
3939
40 label-by-branches:40 label-by-branches:
41 name: 🏷️ Label PR by Branches41 name: 🏷️ Label PR by Branches
42 needs: [label-by-size]
42 runs-on: ubuntu-latest43 runs-on: ubuntu-latest
44 # Run, even if the previous jobs were skipped/failed
43 # Only label once when PR is created or branches are changed, to allow manual label removal45 # Only label once when PR is created or branches are changed, to allow manual label removal
44 if: github.event.action == 'opened' || (github.event.action == 'synchronize' && (github.event.changes.base || github.event.changes.head))46 if: |
47 always()
48 && github.event.action == 'opened' || (github.event.action == 'synchronize' && (github.event.changes.base || github.event.changes.head))
4549
46 steps:50 steps:
47 - name: Checkout Repository51 - name: Checkout Repository
@@ -59,7 +63,10 @@ jobs:
5963
60 label-by-files:64 label-by-files:
61 name: 🏷️ Label PR by Files65 name: 🏷️ Label PR by Files
66 needs: [label-by-branches]
62 runs-on: ubuntu-latest67 runs-on: ubuntu-latest
68 # Run, even if the previous jobs were skipped/failed
69 if: always()
6370
64 steps:71 steps:
65 - name: Checkout Repository72 - name: Checkout Repository
@@ -77,9 +84,12 @@ jobs:
7784
78 remove-stale-label:85 remove-stale-label:
79 name: 🗑️ Remove Stale Label on Comment86 name: 🗑️ Remove Stale Label on Comment
87 needs: [label-by-files]
80 runs-on: ubuntu-latest88 runs-on: ubuntu-latest
81 # Only runs when this is not done by the github actions bot89 # Only runs when this is not done by the github actions bot
82 if: github.event_name == 'pull_request_review_comment' && github.actor != 'github-actions[bot]'90 if: |
91 always()
92 && github.event_name == 'pull_request_review_comment' && github.actor != 'github-actions[bot]'
8393
84 steps:94 steps:
85 - name: Remove Stale Label95 - name: Remove Stale Label
@@ -94,7 +104,7 @@ jobs:
94104
95 check-merge-blocking-labels:105 check-merge-blocking-labels:
96 name: 🚫 Check Merge Blocking Labels106 name: 🚫 Check Merge Blocking Labels
97 needs: [label-by-branches, label-by-files]107 needs: [label-by-size, label-by-branches, label-by-files, remove-stale-label]
98 runs-on: ubuntu-latest108 runs-on: ubuntu-latest
99 # Run, even if the previous jobs were skipped/failed109 # Run, even if the previous jobs were skipped/failed
100 if: always()110 if: always()
@@ -144,7 +154,7 @@ jobs:
144154
145 write-auto-comments:155 write-auto-comments:
146 name: 💬 Post PR Comments Based on Labels156 name: 💬 Post PR Comments Based on Labels
147 needs: [label-by-size, label-by-branches, label-by-files]157 needs: [label-by-size, label-by-branches, label-by-files, remove-stale-label]
148 runs-on: ubuntu-latest158 runs-on: ubuntu-latest
149 # Run, even if the previous jobs were skipped/failed159 # Run, even if the previous jobs were skipped/failed
150 if: always()160 if: always()
public/global.d.ts+4 -0
@@ -1,7 +1,11 @@
1import libs from './lib';1import libs from './lib';
2import getContext from './scripts/st-context';2import getContext from './scripts/st-context';
3import { power_user } from './scripts/power-user';
34
4declare global {5declare global {
6 // Custom types
7 declare type InstructSettings = typeof power_user.instruct;
8
5 // Global namespace modules9 // Global namespace modules
6 interface Window {10 interface Window {
7 ai: any;11 ai: any;
public/script.js+57 -22
@@ -514,6 +514,9 @@ export const event_types = {
514 ONLINE_STATUS_CHANGED: 'online_status_changed',514 ONLINE_STATUS_CHANGED: 'online_status_changed',
515 IMAGE_SWIPED: 'image_swiped',515 IMAGE_SWIPED: 'image_swiped',
516 CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',516 CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',
517 CONNECTION_PROFILE_CREATED: 'connection_profile_created',
518 CONNECTION_PROFILE_DELETED: 'connection_profile_deleted',
519 CONNECTION_PROFILE_UPDATED: 'connection_profile_updated',
517 TOOL_CALLS_PERFORMED: 'tool_calls_performed',520 TOOL_CALLS_PERFORMED: 'tool_calls_performed',
518 TOOL_CALLS_RENDERED: 'tool_calls_rendered',521 TOOL_CALLS_RENDERED: 'tool_calls_rendered',
519};522};
@@ -1370,8 +1373,11 @@ export function resultCheckStatus() {
1370 * If the character ID doesn't exist, if the chat is being saved, or if a group is being generated, this function does nothing.1373 * If the character ID doesn't exist, if the chat is being saved, or if a group is being generated, this function does nothing.
1371 * If the character is different from the currently selected one, it will clear the chat and reset any selected character or group.1374 * If the character is different from the currently selected one, it will clear the chat and reset any selected character or group.
1372 * @param {number} id The ID of the character to switch to.1375 * @param {number} id The ID of the character to switch to.
1376 * @param {object} [options] Options for the switch.
1377 * @param {boolean} [options.switchMenu=true] Whether to switch the right menu to the character edit menu if the character is already selected.
1378 * @returns {Promise<void>} A promise that resolves when the character is switched.
1373 */1379 */
1374export async function selectCharacterById(id) {1380export async function selectCharacterById(id, { switchMenu = true } = {}) {
1375 if (characters[id] === undefined) {1381 if (characters[id] === undefined) {
1376 return;1382 return;
1377 }1383 }
@@ -1400,9 +1406,9 @@ export async function selectCharacterById(id) {
1400 }1406 }
1401 } else {1407 } else {
1402 //if clicked on character that was already selected1408 //if clicked on character that was already selected
1403 selected_button = 'character_edit';1409 switchMenu && (selected_button = 'character_edit');
1404 await unshallowCharacter(this_chid);1410 await unshallowCharacter(this_chid);
1405 select_selected_character(this_chid);1411 select_selected_character(this_chid, { switchMenu });
1406 }1412 }
1407}1413}
14081414
@@ -1787,6 +1793,7 @@ export async function getCharacters() {
1787 body: JSON.stringify({}),1793 body: JSON.stringify({}),
1788 });1794 });
1789 if (response.ok === true) {1795 if (response.ok === true) {
1796 const previousAvatar = this_chid !== undefined ? characters[this_chid]?.avatar : null;
1790 characters.splice(0, characters.length);1797 characters.splice(0, characters.length);
1791 const getData = await response.json();1798 const getData = await response.json();
1792 for (let i = 0; i < getData.length; i++) {1799 for (let i = 0; i < getData.length; i++) {
@@ -1800,8 +1807,16 @@ export async function getCharacters() {
18001807
1801 characters[i]['chat'] = String(characters[i]['chat']);1808 characters[i]['chat'] = String(characters[i]['chat']);
1802 }1809 }
1803 if (this_chid !== undefined) {1810
1804 $('#avatar_url_pole').val(characters[this_chid].avatar);1811 if (previousAvatar) {
1812 const newCharacterId = characters.findIndex(x => x.avatar === previousAvatar);
1813 if (newCharacterId >= 0) {
1814 setCharacterId(newCharacterId);
1815 await selectCharacterById(newCharacterId, { switchMenu: false });
1816 } else {
1817 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.`);
1818 return location.reload();
1819 }
1805 }1820 }
18061821
1807 await getGroups();1822 await getGroups();
@@ -6527,6 +6542,8 @@ export async function renameCharacter(name = null, { silent = false, renameChats
65276542
6528 await eventSource.emit(event_types.CHARACTER_RENAMED, oldAvatar, newAvatar);6543 await eventSource.emit(event_types.CHARACTER_RENAMED, oldAvatar, newAvatar);
65296544
6545 // Unload current character
6546 setCharacterId(undefined);
6530 // Reload characters list6547 // Reload characters list
6531 await getCharacters();6548 await getCharacters();
65326549
@@ -6535,7 +6552,6 @@ export async function renameCharacter(name = null, { silent = false, renameChats
65356552
6536 if (newChId !== -1) {6553 if (newChId !== -1) {
6537 // Select the character after the renaming6554 // Select the character after the renaming
6538 setCharacterId(undefined);
6539 await selectCharacterById(newChId);6555 await selectCharacterById(newChId);
65406556
6541 // Async delay to update UI6557 // Async delay to update UI
@@ -7882,14 +7898,19 @@ export function select_rm_info(type, charId, previousCharId = null) {
7882 }7898 }
7883}7899}
78847900
7885export function select_selected_character(chid) {7901/**
7902 * Selects the right menu for displaying the character editor.
7903 * @param {number|string} chid Character array index
7904 * @param {object} [param1] Options for the switch
7905 * @param {boolean} [param1.switchMenu=true] Whether to switch the menu
7906 */
7907export function select_selected_character(chid, { switchMenu = true } = {}) {
7886 //character select7908 //character select
7887 //console.log('select_selected_character() -- starting with input of -- ' + chid + ' (name:' + characters[chid].name + ')');7909 //console.log('select_selected_character() -- starting with input of -- ' + chid + ' (name:' + characters[chid].name + ')');
7888 select_rm_create();7910 select_rm_create({ switchMenu });
7889 setMenuType('character_edit');7911 switchMenu && setMenuType('character_edit');
7890 $('#delete_button').css('display', 'flex');7912 $('#delete_button').css('display', 'flex');
7891 $('#export_button').css('display', 'flex');7913 $('#export_button').css('display', 'flex');
7892 var display_name = characters[chid].name;
78937914
7894 //create text poles7915 //create text poles
7895 $('#rm_button_back').css('display', 'none');7916 $('#rm_button_back').css('display', 'none');
@@ -7904,7 +7925,7 @@ export function select_selected_character(chid) {
79047925
7905 // Don't update the navbar name if we're peeking the group member defs7926 // Don't update the navbar name if we're peeking the group member defs
7906 if (!selected_group) {7927 if (!selected_group) {
7907 $('#rm_button_selected_ch').children('h2').text(display_name);7928 $('#rm_button_selected_ch').children('h2').text(characters[chid].name);
7908 }7929 }
79097930
7910 $('#add_avatar_button').val('');7931 $('#add_avatar_button').val('');
@@ -7935,22 +7956,20 @@ export function select_selected_character(chid) {
7935 $('#chat_import_avatar_url').val(characters[chid].avatar);7956 $('#chat_import_avatar_url').val(characters[chid].avatar);
7936 $('#chat_import_character_name').val(characters[chid].name);7957 $('#chat_import_character_name').val(characters[chid].name);
7937 $('#character_json_data').val(characters[chid].json_data);7958 $('#character_json_data').val(characters[chid].json_data);
7938 let this_avatar = default_avatar;
7939 if (characters[chid].avatar != 'none') {
7940 this_avatar = getThumbnailUrl('avatar', characters[chid].avatar);
7941 }
79427959
7943 updateFavButtonState(characters[chid].fav || characters[chid].fav == 'true');7960 updateFavButtonState(characters[chid].fav || characters[chid].fav == 'true');
79447961
7945 $('#avatar_load_preview').attr('src', this_avatar);7962 const avatarUrl = characters[chid].avatar != 'none' ? getThumbnailUrl('avatar', characters[chid].avatar) : default_avatar;
7946 $('#name_div').removeClass('displayBlock');7963 $('#avatar_load_preview').attr('src', avatarUrl);
7947 $('#name_div').addClass('displayNone');
7948 $('#renameCharButton').css('display', '');
7949 $('.open_alternate_greetings').data('chid', chid);7964 $('.open_alternate_greetings').data('chid', chid);
7950 $('#set_character_world').data('chid', chid);7965 $('#set_character_world').data('chid', chid);
7951 setWorldInfoButtonClass(chid);7966 setWorldInfoButtonClass(chid);
7952 checkEmbeddedWorld(chid);7967 checkEmbeddedWorld(chid);
79537968
7969 $('#name_div').removeClass('displayBlock');
7970 $('#name_div').addClass('displayNone');
7971 $('#renameCharButton').css('display', '');
7972
7954 $('#form_create').attr('actiontype', 'editcharacter');7973 $('#form_create').attr('actiontype', 'editcharacter');
7955 $('.form_create_bottom_buttons_block .chat_lorebook_button').show();7974 $('.form_create_bottom_buttons_block .chat_lorebook_button').show();
79567975
@@ -7962,8 +7981,13 @@ export function select_selected_character(chid) {
7962 saveSettingsDebounced();7981 saveSettingsDebounced();
7963}7982}
79647983
7965function select_rm_create() {7984/**
7966 setMenuType('create');7985 * Selects the right menu for creating a new character.
7986 * @param {object} [options] Options for the switch
7987 * @param {boolean} [options.switchMenu=true] Whether to switch the menu
7988 */
7989function select_rm_create({ switchMenu = true } = {}) {
7990 switchMenu && setMenuType('create');
79677991
7968 //console.log('select_rm_Create() -- selected button: '+selected_button);7992 //console.log('select_rm_Create() -- selected button: '+selected_button);
7969 if (selected_button == 'create') {7993 if (selected_button == 'create') {
@@ -7973,7 +7997,7 @@ function select_rm_create() {
7973 }7997 }
7974 }7998 }
79757999
7976 selectRightMenuWithAnimation('rm_ch_create_block');8000 switchMenu && selectRightMenuWithAnimation('rm_ch_create_block');
79778001
7978 $('#set_chat_scenario').hide();8002 $('#set_chat_scenario').hide();
7979 $('#delete_button_div').css('display', 'none');8003 $('#delete_button_div').css('display', 'none');
@@ -9196,6 +9220,17 @@ function swipe_right(_event, { source, repeated } = {}) {
9196 }9220 }
9197}9221}
91989222
9223/**
9224 * @typedef {object} ConnectAPIMap
9225 * @property {string} selected - API name (e.g. "textgenerationwebui", "openai")
9226 * @property {string?} [button] - CSS selector for the API button
9227 * @property {string?} [type] - API type, mostly used by text completion. (e.g. "openrouter")
9228 * @property {string?} [source] - API source, mostly used by chat completion. (e.g. "openai")
9229 */
9230
9231/**
9232 * @type {Record<string, ConnectAPIMap>}
9233 */
9199export const CONNECT_API_MAP = {9234export const CONNECT_API_MAP = {
9200 // Default APIs not contined inside text gen / chat gen9235 // Default APIs not contined inside text gen / chat gen
9201 'kobold': {9236 'kobold': {
public/scripts/custom-request.js+231 -33
@@ -1,20 +1,20 @@
1import { getPresetManager } from './preset-manager.js';1import { getPresetManager } from './preset-manager.js';
2import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js';2import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js';
3import { getTextGenServer } from './textgen-settings.js';3import { getTextGenServer } from './textgen-settings.js';
4import { extractReasoningFromData } from './reasoning.js';
5import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js';
46
5// #region Type Definitions7// #region Type Definitions
6/**8/**
7 * @typedef {Object} TextCompletionRequestBase9 * @typedef {Object} TextCompletionRequestBase
8 * @property {string} prompt - The text prompt for completion
9 * @property {number} max_tokens - Maximum number of tokens to generate10 * @property {number} max_tokens - Maximum number of tokens to generate
10 * @property {string} [model] - Optional model name11 * @property {string} [model] - Optional model name
11 * @property {string} api_type - Type of API to use12 * @property {string} api_type - Type of API to use
12 * @property {string} [api_server] - Optional API server URL13 * @property {string} [api_server] - Optional API server URL
13 * @property {number} [temperature] - Optional temperature parameter14 * @property {number} [temperature] - Optional temperature parameter
15 * @property {number} [min_p] - Optional min_p parameter
14 */16 */
1517
16/** @typedef {Record<string, any> & TextCompletionRequestBase} TextCompletionRequest */
17
18/**18/**
19 * @typedef {Object} TextCompletionPayloadBase19 * @typedef {Object} TextCompletionPayloadBase
20 * @property {string} prompt - The text prompt for completion20 * @property {string} prompt - The text prompt for completion
@@ -44,6 +44,13 @@ import { getTextGenServer } from './textgen-settings.js';
44 */44 */
4545
46/** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */46/** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */
47
48/**
49 * @typedef {Object} ExtractedData
50 * @property {string} content - Extracted content.
51 * @property {string} reasoning - Extracted reasoning.
52 */
53
47// #endregion54// #endregion
4855
49/**56/**
@@ -53,11 +60,11 @@ export class TextCompletionService {
53 static TYPE = 'textgenerationwebui';60 static TYPE = 'textgenerationwebui';
5461
55 /**62 /**
56 * @param {TextCompletionRequest} custom63 * @param {Record<string, any> & TextCompletionRequestBase & {prompt: string}} custom
57 * @returns {TextCompletionPayload}64 * @returns {TextCompletionPayload}
58 */65 */
59 static createRequestData({ prompt, max_tokens, model, api_type, api_server, temperature, ...props }) {66 static createRequestData({ prompt, max_tokens, model, api_type, api_server, temperature, min_p, ...props }) {
60 return {67 const payload = {
61 ...props,68 ...props,
62 prompt,69 prompt,
63 max_tokens,70 max_tokens,
@@ -66,15 +73,25 @@ export class TextCompletionService {
66 api_type,73 api_type,
67 api_server: api_server ?? getTextGenServer(api_type),74 api_server: api_server ?? getTextGenServer(api_type),
68 temperature,75 temperature,
76 min_p,
69 stream: false,77 stream: false,
70 };78 };
79
80 // Remove undefined values to avoid API errors
81 Object.keys(payload).forEach(key => {
82 if (payload[key] === undefined) {
83 delete payload[key];
84 }
85 });
86
87 return payload;
71 }88 }
7289
73 /**90 /**
74 * Sends a text completion request to the specified server91 * Sends a text completion request to the specified server
75 * @param {TextCompletionPayload} data Request data92 * @param {TextCompletionPayload} data Request data
76 * @param {boolean?} extractData Extract message from the response. Default true93 * @param {boolean?} extractData Extract message from the response. Default true
77 * @returns {Promise<string | any>} Extracted data or the raw response94 * @returns {Promise<ExtractedData | any>} Extracted data or the raw response
78 * @throws {Error}95 * @throws {Error}
79 */96 */
80 static async sendRequest(data, extractData = true) {97 static async sendRequest(data, extractData = true) {
@@ -91,31 +108,150 @@ export class TextCompletionService {
91 throw json;108 throw json;
92 }109 }
93110
94 return extractData ? extractMessageFromData(json, this.TYPE) : json;111 if (!extractData) {
112 return json;
113 }
114
115 return {
116 content: extractMessageFromData(json, this.TYPE),
117 reasoning: extractReasoningFromData(json, {
118 mainApi: this.TYPE,
119 textGenType: data.api_type,
120 ignoreShowThoughts: true,
121 }),
122 };
95 }123 }
96124
97 /**125 /**
98 * @param {string} presetName126 * Process and send a text completion request with optional preset & instruct
99 * @param {TextCompletionRequest} custom127 * @param {Record<string, any> & TextCompletionRequestBase & {prompt: (ChatCompletionMessage & {ignoreInstruct?: boolean})[] |string}} custom
100 * @param {boolean?} extractData Extract message from the response. Default true128 * @param {Object} options - Configuration options
101 * @returns {Promise<string | any>} Extracted data or the raw response129 * @param {string?} [options.presetName] - Name of the preset to use for generation settings
130 * @param {string?} [options.instructName] - Name of instruct preset for message formatting
131 * @param {boolean} extractData - Whether to extract structured data from response
132 * @returns {Promise<ExtractedData | any>} Extracted data or the raw response
102 * @throws {Error}133 * @throws {Error}
103 */134 */
104 static async sendRequestWithPreset(presetName, custom, extractData = true) {135 static async processRequest(
136 custom,
137 options = {},
138 extractData = true,
139 ) {
140 const { presetName, instructName } = options;
141 let requestData = { ...custom };
142 const prompt = custom.prompt;
143
144 // Apply generation preset if specified
145 if (presetName) {
105 const presetManager = getPresetManager(this.TYPE);146 const presetManager = getPresetManager(this.TYPE);
106 if (!presetManager) {147 if (presetManager) {
107 throw new Error('Preset manager not found');148 const preset = presetManager.getCompletionPresetByName(presetName);
149 if (preset) {
150 // Convert preset to payload and merge with custom parameters
151 const presetPayload = this.presetToGeneratePayload(preset, {});
152 requestData = { ...presetPayload, ...requestData };
153 } else {
154 console.warn(`Preset "${presetName}" not found, continuing with default settings`);
155 }
156 } else {
157 console.warn('Preset manager not found, continuing with default settings');
158 }
108 }159 }
109160
110 const preset = presetManager.getCompletionPresetByName(presetName);161 // Handle instruct formatting if requested
111 if (!preset) {162 if (Array.isArray(prompt) && instructName) {
112 throw new Error('Preset not found');163 const instructPresetManager = getPresetManager('instruct');
164 let instructPreset = instructPresetManager?.getCompletionPresetByName(instructName);
165 if (instructPreset) {
166 // Clone the preset to avoid modifying the original
167 instructPreset = structuredClone(instructPreset);
168 instructPreset.macro = false;
169 instructPreset.names_behavior = names_behavior_types.NONE;
170
171 // Format messages using instruct formatting
172 const formattedMessages = [];
173 for (const message of prompt) {
174 let messageContent = message.content;
175 if (!message.ignoreInstruct) {
176 messageContent = formatInstructModeChat(
177 message.role,
178 message.content,
179 message.role === 'user',
180 false,
181 undefined,
182 undefined,
183 undefined,
184 undefined,
185 instructPreset,
186 );
187
188 // Add prompt formatting for the last message
189 if (message === prompt[prompt.length - 1]) {
190 messageContent += formatInstructModePrompt(
191 undefined,
192 false,
193 undefined,
194 undefined,
195 undefined,
196 false,
197 false,
198 instructPreset,
199 );
200 }
201 }
202 formattedMessages.push(messageContent);
203 }
204 requestData.prompt = formattedMessages.join('');
205 if (instructPreset.output_suffix) {
206 requestData.stop = [instructPreset.output_suffix];
207 requestData.stopping_strings = [instructPreset.output_suffix];
208 }
209 } else {
210 console.warn(`Instruct preset "${instructName}" not found, using basic formatting`);
211 requestData.prompt = prompt.map(x => x.content).join('\n\n');
212 }
213 } else if (typeof prompt === 'string') {
214 requestData.prompt = prompt;
215 } else {
216 requestData.prompt = prompt.map(x => x.content).join('\n\n');
113 }217 }
114218
115 const data = this.createRequestData({ ...preset, ...custom });219 // @ts-ignore
220 const data = this.createRequestData(requestData);
116221
117 return await this.sendRequest(data, extractData);222 return await this.sendRequest(data, extractData);
118 }223 }
224
225 /**
226 * Converts a preset to a valid text completion payload.
227 * Only supports temperature.
228 * @param {Object} preset - The preset configuration
229 * @param {Object} customPreset - Additional parameters to override preset values
230 * @returns {Object} - Formatted payload for text completion API
231 */
232 static presetToGeneratePayload(preset, customPreset = {}) {
233 if (!preset || typeof preset !== 'object') {
234 throw new Error('Invalid preset: must be an object');
235 }
236
237 // Merge preset with custom parameters
238 const settings = { ...preset, ...customPreset };
239
240 // Initialize base payload with common parameters
241 let payload = {
242 'temperature': settings.temp ? Number(settings.temp) : undefined,
243 'min_p': settings.min_p ? Number(settings.min_p) : undefined,
244 };
245
246 // Remove undefined values to avoid API errors
247 Object.keys(payload).forEach(key => {
248 if (payload[key] === undefined) {
249 delete payload[key];
250 }
251 });
252
253 return payload;
254 }
119}255}
120256
121/**257/**
@@ -129,7 +265,7 @@ export class ChatCompletionService {
129 * @returns {ChatCompletionPayload}265 * @returns {ChatCompletionPayload}
130 */266 */
131 static createRequestData({ messages, model, chat_completion_source, max_tokens, temperature, ...props }) {267 static createRequestData({ messages, model, chat_completion_source, max_tokens, temperature, ...props }) {
132 return {268 const payload = {
133 ...props,269 ...props,
134 messages,270 messages,
135 model,271 model,
@@ -138,13 +274,22 @@ export class ChatCompletionService {
138 temperature,274 temperature,
139 stream: false,275 stream: false,
140 };276 };
277
278 // Remove undefined values to avoid API errors
279 Object.keys(payload).forEach(key => {
280 if (payload[key] === undefined) {
281 delete payload[key];
282 }
283 });
284
285 return payload;
141 }286 }
142287
143 /**288 /**
144 * Sends a chat completion request289 * Sends a chat completion request
145 * @param {ChatCompletionPayload} data Request data290 * @param {ChatCompletionPayload} data Request data
146 * @param {boolean?} extractData Extract message from the response. Default true291 * @param {boolean?} extractData Extract message from the response. Default true
147 * @returns {Promise<string | any>} Extracted data or the raw response292 * @returns {Promise<ExtractedData | any>} Extracted data or the raw response
148 * @throws {Error}293 * @throws {Error}
149 */294 */
150 static async sendRequest(data, extractData = true) {295 static async sendRequest(data, extractData = true) {
@@ -161,29 +306,82 @@ export class ChatCompletionService {
161 throw json;306 throw json;
162 }307 }
163308
164 return extractData ? extractMessageFromData(json, this.TYPE) : json;309 if (!extractData) {
310 return json;
311 }
312
313 return {
314 content: extractMessageFromData(json, this.TYPE),
315 reasoning: extractReasoningFromData(json, {
316 mainApi: this.TYPE,
317 textGenType: data.chat_completion_source,
318 ignoreShowThoughts: true,
319 }),
320 };
165 }321 }
166322
167 /**323 /**
168 * @param {string} presetName324 * Process and send a chat completion request with optional preset
169 * @param {ChatCompletionPayload} custom325 * @param {ChatCompletionPayload} custom
170 * @param {boolean} extractData Extract message from the response. Default true326 * @param {Object} options - Configuration options
171 * @returns {Promise<string | any>} Extracted data or the raw response327 * @param {string?} [options.presetName] - Name of the preset to use for generation settings
328 * @param {boolean} extractData - Whether to extract structured data from response
329 * @returns {Promise<ExtractedData | any>} Extracted data or the raw response
172 * @throws {Error}330 * @throws {Error}
173 */331 */
174 static async sendRequestWithPreset(presetName, custom, extractData = true) {332 static async processRequest(custom, options, extractData = true) {
175 const presetManager = getPresetManager(this.TYPE);333 const { presetName } = options;
176 if (!presetManager) {334 let requestData = { ...custom };
177 throw new Error('Preset manager not found');
178 }
179335
336 // Apply generation preset if specified
337 if (presetName) {
338 const presetManager = getPresetManager(this.TYPE);
339 if (presetManager) {
180 const preset = presetManager.getCompletionPresetByName(presetName);340 const preset = presetManager.getCompletionPresetByName(presetName);
181 if (!preset) {341 if (preset) {
182 throw new Error('Preset not found');342 // Convert preset to payload and merge with custom parameters
343 const presetPayload = this.presetToGeneratePayload(preset, {});
344 requestData = { ...presetPayload, ...requestData };
345 } else {
346 console.warn(`Preset "${presetName}" not found, continuing with default settings`);
347 }
348 } else {
349 console.warn('Preset manager not found, continuing with default settings');
350 }
183 }351 }
184352
185 const data = this.createRequestData({ ...preset, ...custom });353 const data = this.createRequestData(requestData);
186354
187 return await this.sendRequest(data, extractData);355 return await this.sendRequest(data, extractData);
188 }356 }
357
358 /**
359 * Converts a preset to a valid chat completion payload
360 * Only supports temperature.
361 * @param {Object} preset - The preset configuration
362 * @param {Object} customParams - Additional parameters to override preset values
363 * @returns {Object} - Formatted payload for chat completion API
364 */
365 static presetToGeneratePayload(preset, customParams = {}) {
366 if (!preset || typeof preset !== 'object') {
367 throw new Error('Invalid preset: must be an object');
368 }
369
370 // Merge preset with custom parameters
371 const settings = { ...preset, ...customParams };
372
373 // Initialize base payload with common parameters
374 const payload = {
375 temperature: settings.temperature ? Number(settings.temperature) : undefined,
376 };
377
378 // Remove undefined values to avoid API errors
379 Object.keys(payload).forEach(key => {
380 if (payload[key] === undefined) {
381 delete payload[key];
382 }
383 });
384
385 return payload;
386 }
189}387}
public/scripts/extensions/connection-manager/index.js+28 -6
@@ -1,4 +1,4 @@
1import { Fuse } from '../../../lib.js';1import { DOMPurify, Fuse } from '../../../lib.js';
22
3import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js';3import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js';
4import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';4import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
@@ -271,9 +271,14 @@ async function createConnectionProfile(forceName = null) {
271 });271 });
272 const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n);272 const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n);
273 const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken);273 const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken);
274 const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 });274 let name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 });
275275 // If it's cancelled, it will be false
276 if (!name) {
277 return null;
278 }
279 name = DOMPurify.sanitize(String(name));
276 if (!name) {280 if (!name) {
281 toastr.error('Name cannot be empty.');
277 return null;282 return null;
278 }283 }
279284
@@ -307,7 +312,8 @@ async function deleteConnectionProfile() {
307 return;312 return;
308 }313 }
309314
310 const name = extension_settings.connectionManager.profiles[index].name;315 const profile = extension_settings.connectionManager.profiles[index];
316 const name = profile.name;
311 const confirm = await Popup.show.confirm(t`Are you sure you want to delete the selected profile?`, name);317 const confirm = await Popup.show.confirm(t`Are you sure you want to delete the selected profile?`, name);
312318
313 if (!confirm) {319 if (!confirm) {
@@ -317,6 +323,8 @@ async function deleteConnectionProfile() {
317 extension_settings.connectionManager.profiles.splice(index, 1);323 extension_settings.connectionManager.profiles.splice(index, 1);
318 extension_settings.connectionManager.selectedProfile = null;324 extension_settings.connectionManager.selectedProfile = null;
319 saveSettingsDebounced();325 saveSettingsDebounced();
326
327 await eventSource.emit(event_types.CONNECTION_PROFILE_DELETED, profile);
320}328}
321329
322/**330/**
@@ -516,6 +524,7 @@ async function renderDetailsContent(detailsContent) {
516 saveSettingsDebounced();524 saveSettingsDebounced();
517 renderConnectionProfiles(profiles);525 renderConnectionProfiles(profiles);
518 await renderDetailsContent(detailsContent);526 await renderDetailsContent(detailsContent);
527 await eventSource.emit(event_types.CONNECTION_PROFILE_CREATED, profile);
519 await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);528 await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);
520 });529 });
521530
@@ -527,9 +536,11 @@ async function renderDetailsContent(detailsContent) {
527 console.log('No profile selected');536 console.log('No profile selected');
528 return;537 return;
529 }538 }
539 const oldProfile = structuredClone(profile);
530 await updateConnectionProfile(profile);540 await updateConnectionProfile(profile);
531 await renderDetailsContent(detailsContent);541 await renderDetailsContent(detailsContent);
532 saveSettingsDebounced();542 saveSettingsDebounced();
543 await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
533 await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);544 await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);
534 toastr.success('Connection profile updated', '', { timeOut: 1500 });545 toastr.success('Connection profile updated', '', { timeOut: 1500 });
535 });546 });
@@ -563,7 +574,7 @@ async function renderDetailsContent(detailsContent) {
563 return acc;574 return acc;
564 }, {});575 }, {});
565 const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings }));576 const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings }));
566 const newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, {577 let newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, {
567 rows: 2,578 rows: 2,
568 customButtons: [{579 customButtons: [{
569 text: t`Save and Update`,580 text: t`Save and Update`,
@@ -575,7 +586,13 @@ async function renderDetailsContent(detailsContent) {
575 }],586 }],
576 });587 });
577588
589 // If it's cancelled, it will be false
590 if (!newName) {
591 return;
592 }
593 newName = DOMPurify.sanitize(String(newName));
578 if (!newName) {594 if (!newName) {
595 toastr.error('Name cannot be empty.');
579 return;596 return;
580 }597 }
581598
@@ -588,6 +605,7 @@ async function renderDetailsContent(detailsContent) {
588 return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0];605 return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0];
589 }).get();606 }).get();
590607
608 const oldProfile = structuredClone(profile);
591 if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) {609 if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) {
592 profile.exclude = newExcludeList;610 profile.exclude = newExcludeList;
593 for (const command of newExcludeList) {611 for (const command of newExcludeList) {
@@ -602,10 +620,11 @@ async function renderDetailsContent(detailsContent) {
602620
603 if (profile.name !== newName) {621 if (profile.name !== newName) {
604 toastr.success('Connection profile renamed.');622 toastr.success('Connection profile renamed.');
605 profile.name = String(newName);623 profile.name = newName;
606 }624 }
607625
608 saveSettingsDebounced();626 saveSettingsDebounced();
627 await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
609 renderConnectionProfiles(profiles);628 renderConnectionProfiles(profiles);
610 await renderDetailsContent(detailsContent);629 await renderDetailsContent(detailsContent);
611 });630 });
@@ -708,6 +727,7 @@ async function renderDetailsContent(detailsContent) {
708 saveSettingsDebounced();727 saveSettingsDebounced();
709 renderConnectionProfiles(profiles);728 renderConnectionProfiles(profiles);
710 await renderDetailsContent(detailsContent);729 await renderDetailsContent(detailsContent);
730 await eventSource.emit(event_types.CONNECTION_PROFILE_CREATED, profile);
711 return profile.name;731 return profile.name;
712 },732 },
713 }));733 }));
@@ -722,9 +742,11 @@ async function renderDetailsContent(detailsContent) {
722 toastr.warning('No profile selected.');742 toastr.warning('No profile selected.');
723 return '';743 return '';
724 }744 }
745 const oldProfile = structuredClone(profile);
725 await updateConnectionProfile(profile);746 await updateConnectionProfile(profile);
726 await renderDetailsContent(detailsContent);747 await renderDetailsContent(detailsContent);
727 saveSettingsDebounced();748 saveSettingsDebounced();
749 await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
728 return profile.name;750 return profile.name;
729 },751 },
730 }));752 }));
public/scripts/extensions/shared.js+308 -1
@@ -1,5 +1,6 @@
1import { getRequestHeaders } from '../../script.js';1import { CONNECT_API_MAP, getRequestHeaders } from '../../script.js';
2import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';2import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';
3import { t } from '../i18n.js';
3import { oai_settings } from '../openai.js';4import { oai_settings } from '../openai.js';
4import { SECRET_KEYS, secret_state } from '../secrets.js';5import { SECRET_KEYS, secret_state } from '../secrets.js';
5import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';6import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
@@ -273,3 +274,309 @@ export async function getWebLlmContextSize() {
273 const model = await engine.getCurrentModelInfo();274 const model = await engine.getCurrentModelInfo();
274 return model?.context_size;275 return model?.context_size;
275}276}
277
278/**
279 * It uses the profiles to send a generate request to the API. Doesn't support streaming.
280 */
281export class ConnectionManagerRequestService {
282 static defaultSendRequestParams = {
283 extractData: true,
284 includePreset: true,
285 includeInstruct: true,
286 };
287
288 static getAllowedTypes() {
289 return {
290 openai: t`Chat Completion`,
291 textgenerationwebui: t`Text Completion`,
292 };
293 }
294
295 /**
296 * @param {string} profileId
297 * @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt
298 * @param {number} maxTokens
299 * @param {{extractData?: boolean, includePreset?: boolean, includeInstruct?: boolean}} custom - default values are true
300 * @returns {Promise<import('../custom-request.js').ExtractedData | any>} Extracted data or the raw response
301 */
302 static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) {
303 const { extractData, includePreset, includeInstruct } = { ...this.defaultSendRequestParams, ...custom };
304
305 const context = SillyTavern.getContext();
306 if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
307 throw new Error('Connection Manager is not available');
308 }
309
310 const profile = context.extensionSettings.connectionManager.profiles.find((p) => p.id === profileId);
311 const selectedApiMap = this.validateProfile(profile);
312
313 try {
314 switch (selectedApiMap.selected) {
315 case 'openai': {
316 if (!selectedApiMap.source) {
317 throw new Error(`API type ${selectedApiMap.selected} does not support chat completions`);
318 }
319
320 const messages = Array.isArray(prompt) ? prompt : [{ role: 'user', content: prompt }];
321 return await context.ChatCompletionService.processRequest({
322 messages,
323 max_tokens: maxTokens,
324 model: profile.model,
325 chat_completion_source: selectedApiMap.source,
326 }, {
327 presetName: includePreset ? profile.preset : undefined,
328 }, extractData);
329 }
330 case 'textgenerationwebui': {
331 if (!selectedApiMap.type) {
332 throw new Error(`API type ${selectedApiMap.selected} does not support text completions`);
333 }
334
335 return await context.TextCompletionService.processRequest({
336 prompt,
337 max_tokens: maxTokens,
338 model: profile.model,
339 api_type: selectedApiMap.type,
340 api_server: profile['api-url'],
341 }, {
342 instructName: includeInstruct ? profile.instruct : undefined,
343 presetName: includePreset ? profile.preset : undefined,
344 }, extractData);
345 }
346 default: {
347 throw new Error(`Unknown API type ${selectedApiMap.selected}`);
348 }
349 }
350 } catch (error) {
351 throw new Error('API request failed', { cause: error });
352 }
353 }
354
355 /**
356 * Respects allowed types.
357 * @returns {import('./connection-manager/index.js').ConnectionProfile[]}
358 */
359 static getSupportedProfiles() {
360 const context = SillyTavern.getContext();
361 if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
362 throw new Error('Connection Manager is not available');
363 }
364
365 const profiles = context.extensionSettings.connectionManager.profiles;
366 return profiles.filter((p) => this.isProfileSupported(p));
367 }
368
369 /**
370 * @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]
371 * @returns {boolean}
372 */
373 static isProfileSupported(profile) {
374 if (!profile) {
375 return false;
376 }
377
378 const apiMap = CONNECT_API_MAP[profile.api];
379 if (!Object.hasOwn(this.getAllowedTypes(), apiMap.selected)) {
380 return false;
381 }
382
383 // Some providers not need model, like koboldcpp. But I don't want to check by provider.
384 switch (apiMap.selected) {
385 case 'openai':
386 return !!apiMap.source;
387 case 'textgenerationwebui':
388 return !!apiMap.type;
389 }
390
391 return false;
392 }
393
394 /**
395 * @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]
396 * @return {import('../../script.js').ConnectAPIMap}
397 * @throws {Error}
398 */
399 static validateProfile(profile) {
400 if (!profile) {
401 throw new Error('Could not find profile.');
402 }
403 if (!profile.api) {
404 throw new Error('Select a connection profile that has an API');
405 }
406
407 const context = SillyTavern.getContext();
408 const selectedApiMap = context.CONNECT_API_MAP[profile.api];
409 if (!selectedApiMap) {
410 throw new Error(`Unknown API type ${profile.api}`);
411 }
412 if (!Object.hasOwn(this.getAllowedTypes(), selectedApiMap.selected)) {
413 throw new Error(`API type ${selectedApiMap.selected} is not supported. Supported types: ${Object.values(this.getAllowedTypes()).join(', ')}`);
414 }
415
416 return selectedApiMap;
417 }
418
419 /**
420 * Create profiles dropdown and updates select element accordingly. Use onChange, onCreate, unUpdate, onDelete callbacks for custom behaviour. e.g updating extension settings.
421 * @param {string} selector
422 * @param {string} initialSelectedProfileId
423 * @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.
424 * @param {(profile: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} onCreate
425 * @param {(oldProfile: import('./connection-manager/index.js').ConnectionProfile, newProfile: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} unUpdate
426 * @param {(profile: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} onDelete
427 */
428 static handleDropdown(
429 selector,
430 initialSelectedProfileId,
431 onChange = () => { },
432 onCreate = () => { },
433 unUpdate = () => { },
434 onDelete = () => { },
435 ) {
436 const context = SillyTavern.getContext();
437 if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
438 throw new Error('Connection Manager is not available');
439 }
440
441 /**
442 * @type {JQuery<HTMLSelectElement>}
443 */
444 const dropdown = $(selector);
445
446 if (!dropdown || !dropdown.length) {
447 throw new Error(`Could not find dropdown with selector ${selector}`);
448 }
449
450 dropdown.empty();
451
452 // Create default option using document.createElement
453 const defaultOption = document.createElement('option');
454 defaultOption.value = '';
455 defaultOption.textContent = 'Select a Connection Profile';
456 defaultOption.dataset.i18n = 'Select a Connection Profile';
457 dropdown.append(defaultOption);
458
459 const profiles = context.extensionSettings.connectionManager.profiles;
460
461 // Create optgroups using document.createElement
462 const groups = {};
463 for (const [apiType, groupLabel] of Object.entries(this.getAllowedTypes())) {
464 const optgroup = document.createElement('optgroup');
465 optgroup.label = groupLabel;
466 groups[apiType] = optgroup;
467 }
468
469 const sortedProfilesByGroup = {};
470 for (const apiType of Object.keys(this.getAllowedTypes())) {
471 sortedProfilesByGroup[apiType] = [];
472 }
473
474 for (const profile of profiles) {
475 if (this.isProfileSupported(profile)) {
476 const apiMap = CONNECT_API_MAP[profile.api];
477 if (sortedProfilesByGroup[apiMap.selected]) {
478 sortedProfilesByGroup[apiMap.selected].push(profile);
479 }
480 }
481 }
482
483 // Sort each group alphabetically and add to dropdown
484 for (const [apiType, groupProfiles] of Object.entries(sortedProfilesByGroup)) {
485 if (groupProfiles.length === 0) continue;
486
487 groupProfiles.sort((a, b) => a.name.localeCompare(b.name));
488
489 const group = groups[apiType];
490 for (const profile of groupProfiles) {
491 const option = document.createElement('option');
492 option.value = profile.id;
493 option.textContent = profile.name;
494 group.appendChild(option);
495 }
496 }
497
498 for (const group of Object.values(groups)) {
499 if (group.children.length > 0) {
500 dropdown.append(group);
501 }
502 }
503
504 const selectedProfile = profiles.find((p) => p.id === initialSelectedProfileId);
505 if (selectedProfile) {
506 dropdown.val(selectedProfile.id);
507 }
508
509 context.eventSource.on(context.eventTypes.CONNECTION_PROFILE_CREATED, async (profile) => {
510 const isSupported = this.isProfileSupported(profile);
511 if (!isSupported) {
512 return;
513 }
514
515 const group = groups[CONNECT_API_MAP[profile.api].selected];
516 const option = document.createElement('option');
517 option.value = profile.id;
518 option.textContent = profile.name;
519 group.appendChild(option);
520
521 await onCreate(profile);
522 });
523
524 context.eventSource.on(context.eventTypes.CONNECTION_PROFILE_UPDATED, async (oldProfile, newProfile) => {
525 const currentSelected = dropdown.val();
526 const isSelectedProfile = currentSelected === oldProfile.id;
527 await unUpdate(oldProfile, newProfile);
528
529 if (!this.isProfileSupported(newProfile)) {
530 if (isSelectedProfile) {
531 dropdown.val('');
532 dropdown.trigger('change');
533 }
534 return;
535 }
536
537 const group = groups[CONNECT_API_MAP[newProfile.api].selected];
538 const oldOption = group.querySelector(`option[value="${oldProfile.id}"]`);
539 if (oldOption) {
540 oldOption.remove();
541 }
542
543 const option = document.createElement('option');
544 option.value = newProfile.id;
545 option.textContent = newProfile.name;
546 group.appendChild(option);
547
548 if (isSelectedProfile) {
549 // Ackchyually, we don't need to reselect but what if id changes? It is not possible for now I couldn't stop myself.
550 dropdown.val(newProfile.id);
551 dropdown.trigger('change');
552 }
553 });
554
555 context.eventSource.on(context.eventTypes.CONNECTION_PROFILE_DELETED, async (profile) => {
556 const currentSelected = dropdown.val();
557 const isSelectedProfile = currentSelected === profile.id;
558 if (!this.isProfileSupported(profile)) {
559 return;
560 }
561
562 const group = groups[CONNECT_API_MAP[profile.api].selected];
563 const optionToRemove = group.querySelector(`option[value="${profile.id}"]`);
564 if (optionToRemove) {
565 optionToRemove.remove();
566 }
567
568 if (isSelectedProfile) {
569 dropdown.val('');
570 dropdown.trigger('change');
571 }
572
573 await onDelete(profile);
574 });
575
576 dropdown.on('change', async () => {
577 const profileId = dropdown.val();
578 const profile = context.extensionSettings.connectionManager.profiles.find((p) => p.id === profileId);
579 await onChange(profile);
580 });
581 }
582}
public/scripts/instruct-mode.js+35 -31
@@ -320,59 +320,61 @@ export const force_output_sequence = {
320 * @param {string} name1 User name.320 * @param {string} name1 User name.
321 * @param {string} name2 Character name.321 * @param {string} name2 Character name.
322 * @param {boolean|number} forceOutputSequence Force to use first/last output sequence (if configured).322 * @param {boolean|number} forceOutputSequence Force to use first/last output sequence (if configured).
323 * @param {InstructSettings} customInstruct Custom instruct mode settings.
323 * @returns {string} Formatted instruct mode chat message.324 * @returns {string} Formatted instruct mode chat message.
324 */325 */
325export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2, forceOutputSequence) {326export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2, forceOutputSequence, customInstruct = null) {
326 let includeNames = isNarrator ? false : power_user.instruct.names_behavior === names_behavior_types.ALWAYS;327 const instruct = structuredClone(customInstruct ?? power_user.instruct);
328 let includeNames = isNarrator ? false : instruct.names_behavior === names_behavior_types.ALWAYS;
327329
328 if (!isNarrator && power_user.instruct.names_behavior === names_behavior_types.FORCE && ((selected_group && name !== name1) || (forceAvatar && name !== name1))) {330 if (!isNarrator && instruct.names_behavior === names_behavior_types.FORCE && ((selected_group && name !== name1) || (forceAvatar && name !== name1))) {
329 includeNames = true;331 includeNames = true;
330 }332 }
331333
332 function getPrefix() {334 function getPrefix() {
333 if (isNarrator) {335 if (isNarrator) {
334 return power_user.instruct.system_same_as_user ? power_user.instruct.input_sequence : power_user.instruct.system_sequence;336 return instruct.system_same_as_user ? instruct.input_sequence : instruct.system_sequence;
335 }337 }
336338
337 if (isUser) {339 if (isUser) {
338 if (forceOutputSequence === force_output_sequence.FIRST) {340 if (forceOutputSequence === force_output_sequence.FIRST) {
339 return power_user.instruct.first_input_sequence || power_user.instruct.input_sequence;341 return instruct.first_input_sequence || instruct.input_sequence;
340 }342 }
341343
342 if (forceOutputSequence === force_output_sequence.LAST) {344 if (forceOutputSequence === force_output_sequence.LAST) {
343 return power_user.instruct.last_input_sequence || power_user.instruct.input_sequence;345 return instruct.last_input_sequence || instruct.input_sequence;
344 }346 }
345347
346 return power_user.instruct.input_sequence;348 return instruct.input_sequence;
347 }349 }
348350
349 if (forceOutputSequence === force_output_sequence.FIRST) {351 if (forceOutputSequence === force_output_sequence.FIRST) {
350 return power_user.instruct.first_output_sequence || power_user.instruct.output_sequence;352 return instruct.first_output_sequence || instruct.output_sequence;
351 }353 }
352354
353 if (forceOutputSequence === force_output_sequence.LAST) {355 if (forceOutputSequence === force_output_sequence.LAST) {
354 return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;356 return instruct.last_output_sequence || instruct.output_sequence;
355 }357 }
356358
357 return power_user.instruct.output_sequence;359 return instruct.output_sequence;
358 }360 }
359361
360 function getSuffix() {362 function getSuffix() {
361 if (isNarrator) {363 if (isNarrator) {
362 return power_user.instruct.system_same_as_user ? power_user.instruct.input_suffix : power_user.instruct.system_suffix;364 return instruct.system_same_as_user ? instruct.input_suffix : instruct.system_suffix;
363 }365 }
364366
365 if (isUser) {367 if (isUser) {
366 return power_user.instruct.input_suffix;368 return instruct.input_suffix;
367 }369 }
368370
369 return power_user.instruct.output_suffix;371 return instruct.output_suffix;
370 }372 }
371373
372 let prefix = getPrefix() || '';374 let prefix = getPrefix() || '';
373 let suffix = getSuffix() || '';375 let suffix = getSuffix() || '';
374376
375 if (power_user.instruct.macro) {377 if (instruct.macro) {
376 prefix = substituteParams(prefix, name1, name2);378 prefix = substituteParams(prefix, name1, name2);
377 prefix = prefix.replace(/{{name}}/gi, name || 'System');379 prefix = prefix.replace(/{{name}}/gi, name || 'System');
378380
@@ -380,11 +382,11 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
380 suffix = suffix.replace(/{{name}}/gi, name || 'System');382 suffix = suffix.replace(/{{name}}/gi, name || 'System');
381 }383 }
382384
383 if (!suffix && power_user.instruct.wrap) {385 if (!suffix && instruct.wrap) {
384 suffix = '\n';386 suffix = '\n';
385 }387 }
386388
387 const separator = power_user.instruct.wrap ? '\n' : '';389 const separator = instruct.wrap ? '\n' : '';
388390
389 // Don't include the name if it's empty391 // Don't include the name if it's empty
390 const textArray = includeNames && name ? [prefix, `${name}: ${mes}` + suffix] : [prefix, mes + suffix];392 const textArray = includeNames && name ? [prefix, `${name}: ${mes}` + suffix] : [prefix, mes + suffix];
@@ -504,30 +506,32 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
504 * @param {string} name2 Character name.506 * @param {string} name2 Character name.
505 * @param {boolean} isQuiet Is quiet mode generation.507 * @param {boolean} isQuiet Is quiet mode generation.
506 * @param {boolean} isQuietToLoud Is quiet to loud generation.508 * @param {boolean} isQuietToLoud Is quiet to loud generation.
509 * @param {InstructSettings} customInstruct Custom instruct settings.
507 * @returns {string} Formatted instruct mode last prompt line.510 * @returns {string} Formatted instruct mode last prompt line.
508 */511 */
509export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, isQuietToLoud) {512export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, isQuietToLoud, customInstruct = null) {
510 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);513 const instruct = structuredClone(customInstruct ?? power_user.instruct);
514 const includeNames = name && (instruct.names_behavior === names_behavior_types.ALWAYS || (!!selected_group && instruct.names_behavior === names_behavior_types.FORCE)) && !(isQuiet && !isQuietToLoud);
511515
512 function getSequence() {516 function getSequence() {
513 // User impersonation prompt517 // User impersonation prompt
514 if (isImpersonate) {518 if (isImpersonate) {
515 return power_user.instruct.input_sequence;519 return instruct.input_sequence;
516 }520 }
517521
518 // Neutral / system / quiet prompt522 // Neutral / system / quiet prompt
519 // Use a special quiet instruct sequence if defined, or assistant's output sequence otherwise523 // Use a special quiet instruct sequence if defined, or assistant's output sequence otherwise
520 if (isQuiet && !isQuietToLoud) {524 if (isQuiet && !isQuietToLoud) {
521 return power_user.instruct.last_system_sequence || power_user.instruct.output_sequence;525 return instruct.last_system_sequence || instruct.output_sequence;
522 }526 }
523527
524 // Quiet in-character prompt528 // Quiet in-character prompt
525 if (isQuiet && isQuietToLoud) {529 if (isQuiet && isQuietToLoud) {
526 return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;530 return instruct.last_output_sequence || instruct.output_sequence;
527 }531 }
528532
529 // Default AI response533 // Default AI response
530 return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;534 return instruct.last_output_sequence || instruct.output_sequence;
531 }535 }
532536
533 let sequence = getSequence() || '';537 let sequence = getSequence() || '';
@@ -536,21 +540,21 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
536 // A hack for Mistral's formatting that has a normal output sequence ending with a space540 // A hack for Mistral's formatting that has a normal output sequence ending with a space
537 if (541 if (
538 includeNames &&542 includeNames &&
539 power_user.instruct.last_output_sequence &&543 instruct.last_output_sequence &&
540 power_user.instruct.output_sequence &&544 instruct.output_sequence &&
541 sequence === power_user.instruct.last_output_sequence &&545 sequence === instruct.last_output_sequence &&
542 /\s$/.test(power_user.instruct.output_sequence) &&546 /\s$/.test(instruct.output_sequence) &&
543 !/\s$/.test(power_user.instruct.last_output_sequence)547 !/\s$/.test(instruct.last_output_sequence)
544 ) {548 ) {
545 nameFiller = power_user.instruct.output_sequence.slice(-1);549 nameFiller = instruct.output_sequence.slice(-1);
546 }550 }
547551
548 if (power_user.instruct.macro) {552 if (instruct.macro) {
549 sequence = substituteParams(sequence, name1, name2);553 sequence = substituteParams(sequence, name1, name2);
550 sequence = sequence.replace(/{{name}}/gi, name || 'System');554 sequence = sequence.replace(/{{name}}/gi, name || 'System');
551 }555 }
552556
553 const separator = power_user.instruct.wrap ? '\n' : '';557 const separator = instruct.wrap ? '\n' : '';
554 let text = includeNames ? (separator + sequence + separator + nameFiller + `${name}:`) : (separator + sequence);558 let text = includeNames ? (separator + sequence + separator + nameFiller + `${name}:`) : (separator + sequence);
555559
556 // Quiet prompt already has a newline at the end560 // Quiet prompt already has a newline at the end
@@ -562,7 +566,7 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
562 text += (includeNames ? promptBias : (separator + promptBias.trimStart()));566 text += (includeNames ? promptBias : (separator + promptBias.trimStart()));
563 }567 }
564568
565 return (power_user.instruct.wrap ? text.trimEnd() : text) + (includeNames ? '' : separator);569 return (instruct.wrap ? text.trimEnd() : text) + (includeNames ? '' : separator);
566}570}
567571
568/**572/**
public/scripts/power-user.js+2 -0
@@ -219,7 +219,9 @@ let power_user = {
219 system_sequence: '',219 system_sequence: '',
220 system_suffix: '',220 system_suffix: '',
221 last_system_sequence: '',221 last_system_sequence: '',
222 first_input_sequence: '',
222 first_output_sequence: '',223 first_output_sequence: '',
224 last_input_sequence: '',
223 last_output_sequence: '',225 last_output_sequence: '',
224 system_sequence_prefix: '',226 system_sequence_prefix: '',
225 system_sequence_suffix: '',227 system_sequence_suffix: '',
public/scripts/reasoning.js+10 -5
@@ -87,19 +87,24 @@ function toggleReasoningAutoExpand() {
87 * @param {object} data Response data87 * @param {object} data Response data
88 * @returns {string} Extracted reasoning88 * @returns {string} Extracted reasoning
89 */89 */
90export function extractReasoningFromData(data) {90export function extractReasoningFromData(data, {
91 switch (main_api) {91 mainApi = null,
92 ignoreShowThoughts = false,
93 textGenType = null,
94 chatCompletionSource = null
95} = {}) {
96 switch (mainApi ?? main_api) {
92 case 'textgenerationwebui':97 case 'textgenerationwebui':
93 switch (textgenerationwebui_settings.type) {98 switch (textGenType ?? textgenerationwebui_settings.type) {
94 case textgen_types.OPENROUTER:99 case textgen_types.OPENROUTER:
95 return data?.choices?.[0]?.reasoning ?? '';100 return data?.choices?.[0]?.reasoning ?? '';
96 }101 }
97 break;102 break;
98103
99 case 'openai':104 case 'openai':
100 if (!oai_settings.show_thoughts) break;105 if (!ignoreShowThoughts && !oai_settings.show_thoughts) break;
101106
102 switch (oai_settings.chat_completion_source) {107 switch (chatCompletionSource ?? oai_settings.chat_completion_source) {
103 case chat_completion_sources.DEEPSEEK:108 case chat_completion_sources.DEEPSEEK:
104 return data?.choices?.[0]?.message?.reasoning_content ?? '';109 return data?.choices?.[0]?.message?.reasoning_content ?? '';
105 case chat_completion_sources.OPENROUTER:110 case chat_completion_sources.OPENROUTER:
public/scripts/st-context.js+2 -0
@@ -80,6 +80,7 @@ import { timestampToMoment, uuidv4 } from './utils.js';
80import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';80import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
81import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';81import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
82import { ChatCompletionService, TextCompletionService } from './custom-request.js';82import { ChatCompletionService, TextCompletionService } from './custom-request.js';
83import { ConnectionManagerRequestService } from './extensions/shared.js';
83import { updateReasoningUI, parseReasoningFromString } from './reasoning.js';84import { updateReasoningUI, parseReasoningFromString } from './reasoning.js';
8485
85export function getContext() {86export function getContext() {
@@ -215,6 +216,7 @@ export function getContext() {
215 clearChat,216 clearChat,
216 ChatCompletionService,217 ChatCompletionService,
217 TextCompletionService,218 TextCompletionService,
219 ConnectionManagerRequestService,
218 updateReasoningUI,220 updateReasoningUI,
219 parseReasoningFromString,221 parseReasoningFromString,
220 unshallowCharacter,222 unshallowCharacter,
public/scripts/textgen-settings.js+1 -1
@@ -86,7 +86,7 @@ const OOBA_DEFAULT_ORDER = [
86 'encoder_repetition_penalty',86 'encoder_repetition_penalty',
87 'no_repeat_ngram',87 'no_repeat_ngram',
88];88];
89const APHRODITE_DEFAULT_ORDER = [89export const APHRODITE_DEFAULT_ORDER = [
90 'dry',90 'dry',
91 'penalties',91 'penalties',
92 'no_repeat_ngram',92 'no_repeat_ngram',