Merge branch 'staging' into reasoning-template

cf2671c6d77b53a165bf8a5862d85e18625b7394

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

11 files changed, +694 -105Ignore whitespace
.github/workflows/pr-auto-manager.yml+14 -4
@@ -39,9 +39,13 @@ jobs:
3939
4040 label-by-branches:
4141 name: 🏷️ Label PR by Branches
42+ needs: [label-by-size]
4243 runs-on: ubuntu-latest
44+ # Run, even if the previous jobs were skipped/failed
4345 # 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
4650 steps:
4751 - name: Checkout Repository
@@ -59,7 +63,10 @@ jobs:
5963
6064 label-by-files:
6165 name: 🏷️ Label PR by Files
66+ needs: [label-by-branches]
6267 runs-on: ubuntu-latest
68+ # Run, even if the previous jobs were skipped/failed
69+ if: always()
6370
6471 steps:
6572 - name: Checkout Repository
@@ -77,9 +84,12 @@ jobs:
7784
7885 remove-stale-label:
7986 name: 🗑️ Remove Stale Label on Comment
87+ needs: [label-by-files]
8088 runs-on: ubuntu-latest
8189 # 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
8494 steps:
8595 - name: Remove Stale Label
@@ -94,7 +104,7 @@ jobs:
94104
95105 check-merge-blocking-labels:
96106 name: 🚫 Check Merge Blocking Labels
97107 needs: [label-by-size, label-by-branches, label-by-files, remove-stale-label]
98108 runs-on: ubuntu-latest
99109 # Run, even if the previous jobs were skipped/failed
100110 if: always()
@@ -144,7 +154,7 @@ jobs:
144154
145155 write-auto-comments:
146156 name: 💬 Post PR Comments Based on Labels
147157 needs: [label-by-size, label-by-branches, label-by-files, remove-stale-label]
148158 runs-on: ubuntu-latest
149159 # Run, even if the previous jobs were skipped/failed
150160 if: always()
public/global.d.ts+4 -0
@@ -1,7 +1,11 @@
11import libs from './lib';
22import getContext from './scripts/st-context';
3+import { power_user } from './scripts/power-user';
34
45declare global {
6+ // Custom types
7+ declare type InstructSettings = typeof power_user.instruct;
8+
59 // Global namespace modules
610 interface Window {
711 ai: any;
public/script.js+57 -22
@@ -514,6 +514,9 @@ export const event_types = {
514514 ONLINE_STATUS_CHANGED: 'online_status_changed',
515515 IMAGE_SWIPED: 'image_swiped',
516516 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',
517520 TOOL_CALLS_PERFORMED: 'tool_calls_performed',
518521 TOOL_CALLS_RENDERED: 'tool_calls_rendered',
519522};
@@ -1370,8 +1373,11 @@ export function resultCheckStatus() {
13701373 * If the character ID doesn't exist, if the chat is being saved, or if a group is being generated, this function does nothing.
13711374 * If the character is different from the currently selected one, it will clear the chat and reset any selected character or group.
13721375 * @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.
13731379 */
13741380export async function selectCharacterById(id, { switchMenu = true } = {}) {
13751381 if (characters[id] === undefined) {
13761382 return;
13771383 }
@@ -1400,9 +1406,9 @@ export async function selectCharacterById(id) {
14001406 }
14011407 } else {
14021408 //if clicked on character that was already selected
14031409 switchMenu && (selected_button = 'character_edit');
14041410 await unshallowCharacter(this_chid);
14051411 select_selected_character(this_chid, { switchMenu });
14061412 }
14071413}
14081414
@@ -1787,6 +1793,7 @@ export async function getCharacters() {
17871793 body: JSON.stringify({}),
17881794 });
17891795 if (response.ok === true) {
1796+ const previousAvatar = this_chid !== undefined ? characters[this_chid]?.avatar : null;
17901797 characters.splice(0, characters.length);
17911798 const getData = await response.json();
17921799 for (let i = 0; i < getData.length; i++) {
@@ -1800,8 +1807,16 @@ export async function getCharacters() {
18001807
18011808 characters[i]['chat'] = String(characters[i]['chat']);
18021809 }
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+ }
18051820 }
18061821
18071822 await getGroups();
@@ -6527,6 +6542,8 @@ export async function renameCharacter(name = null, { silent = false, renameChats
65276542
65286543 await eventSource.emit(event_types.CHARACTER_RENAMED, oldAvatar, newAvatar);
65296544
6545+ // Unload current character
6546+ setCharacterId(undefined);
65306547 // Reload characters list
65316548 await getCharacters();
65326549
@@ -6535,7 +6552,6 @@ export async function renameCharacter(name = null, { silent = false, renameChats
65356552
65366553 if (newChId !== -1) {
65376554 // Select the character after the renaming
6538- setCharacterId(undefined);
65396555 await selectCharacterById(newChId);
65406556
65416557 // Async delay to update UI
@@ -7882,14 +7898,19 @@ export function select_rm_info(type, charId, previousCharId = null) {
78827898 }
78837899}
78847900
7885-export 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+ */
7907+export function select_selected_character(chid, { switchMenu = true } = {}) {
78867908 //character select
78877909 //console.log('select_selected_character() -- starting with input of -- ' + chid + ' (name:' + characters[chid].name + ')');
78887910 select_rm_create({ switchMenu });
78897911 switchMenu && setMenuType('character_edit');
78907912 $('#delete_button').css('display', 'flex');
78917913 $('#export_button').css('display', 'flex');
7892- var display_name = characters[chid].name;
78937914
78947915 //create text poles
78957916 $('#rm_button_back').css('display', 'none');
@@ -7904,7 +7925,7 @@ export function select_selected_character(chid) {
79047925
79057926 // Don't update the navbar name if we're peeking the group member defs
79067927 if (!selected_group) {
79077928 $('#rm_button_selected_ch').children('h2').text(display_namecharacters[chid].name);
79087929 }
79097930
79107931 $('#add_avatar_button').val('');
@@ -7935,22 +7956,20 @@ export function select_selected_character(chid) {
79357956 $('#chat_import_avatar_url').val(characters[chid].avatar);
79367957 $('#chat_import_character_name').val(characters[chid].name);
79377958 $('#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
79437960 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;
79467963 $('#name_divavatar_load_preview').removeClassattr('displayBlocksrc', avatarUrl);
7947- $('#name_div').addClass('displayNone');
7948- $('#renameCharButton').css('display', '');
79497964 $('.open_alternate_greetings').data('chid', chid);
79507965 $('#set_character_world').data('chid', chid);
79517966 setWorldInfoButtonClass(chid);
79527967 checkEmbeddedWorld(chid);
79537968
7969+ $('#name_div').removeClass('displayBlock');
7970+ $('#name_div').addClass('displayNone');
7971+ $('#renameCharButton').css('display', '');
7972+
79547973 $('#form_create').attr('actiontype', 'editcharacter');
79557974 $('.form_create_bottom_buttons_block .chat_lorebook_button').show();
79567975
@@ -7962,8 +7981,13 @@ export function select_selected_character(chid) {
79627981 saveSettingsDebounced();
79637982}
79647983
7965-function 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+ */
7989+function select_rm_create({ switchMenu = true } = {}) {
7990+ switchMenu && setMenuType('create');
79677991
79687992 //console.log('select_rm_Create() -- selected button: '+selected_button);
79697993 if (selected_button == 'create') {
@@ -7973,7 +7997,7 @@ function select_rm_create() {
79737997 }
79747998 }
79757999
79768000 switchMenu && selectRightMenuWithAnimation('rm_ch_create_block');
79778001
79788002 $('#set_chat_scenario').hide();
79798003 $('#delete_button_div').css('display', 'none');
@@ -9196,6 +9220,17 @@ function swipe_right(_event, { source, repeated } = {}) {
91969220 }
91979221}
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+ */
91999234export const CONNECT_API_MAP = {
92009235 // Default APIs not contined inside text gen / chat gen
92019236 'kobold': {
public/scripts/custom-request.js+233 -35
@@ -1,20 +1,20 @@
11import { getPresetManager } from './preset-manager.js';
22import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js';
33import { getTextGenServer } from './textgen-settings.js';
4+import { extractReasoningFromData } from './reasoning.js';
5+import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js';
46
57// #region Type Definitions
68/**
79 * @typedef {Object} TextCompletionRequestBase
8- * @property {string} prompt - The text prompt for completion
910 * @property {number} max_tokens - Maximum number of tokens to generate
1011 * @property {string} [model] - Optional model name
1112 * @property {string} api_type - Type of API to use
1213 * @property {string} [api_server] - Optional API server URL
1314 * @property {number} [temperature] - Optional temperature parameter
15+ * @property {number} [min_p] - Optional min_p parameter
1416 */
1517
16-/** @typedef {Record<string, any> & TextCompletionRequestBase} TextCompletionRequest */
17-
1818/**
1919 * @typedef {Object} TextCompletionPayloadBase
2020 * @property {string} prompt - The text prompt for completion
@@ -44,6 +44,13 @@ import { getTextGenServer } from './textgen-settings.js';
4444 */
4545
4646/** @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+
4754// #endregion
4855
4956/**
@@ -53,11 +60,11 @@ export class TextCompletionService {
5360 static TYPE = 'textgenerationwebui';
5461
5562 /**
56- * @param {TextCompletionRequest} custom
63+ * @param {Record<string, any> & TextCompletionRequestBase & {prompt: string}} custom
5764 * @returns {TextCompletionPayload}
5865 */
5966 static createRequestData({ prompt, max_tokens, model, api_type, api_server, temperature, min_p, ...props }) {
6067 returnconst payload = {
6168 ...props,
6269 prompt,
6370 max_tokens,
@@ -66,15 +73,25 @@ export class TextCompletionService {
6673 api_type,
6774 api_server: api_server ?? getTextGenServer(api_type),
6875 temperature,
76+ min_p,
6977 stream: false,
7078 };
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;
7188 }
7289
7390 /**
7491 * Sends a text completion request to the specified server
7592 * @param {TextCompletionPayload} data Request data
7693 * @param {boolean?} extractData Extract message from the response. Default true
7794 * @returns {Promise<stringExtractedData | any>} Extracted data or the raw response
7895 * @throws {Error}
7996 */
8097 static async sendRequest(data, extractData = true) {
@@ -91,31 +108,150 @@ export class TextCompletionService {
91108 throw json;
92109 }
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+ };
95123 }
96124
97125 /**
98- * @param {string} presetName
126+ * Process and send a text completion request with optional preset & instruct
99- * @param {TextCompletionRequest} custom
127+ * @param {Record<string, any> & TextCompletionRequestBase & {prompt: (ChatCompletionMessage & {ignoreInstruct?: boolean})[] |string}} custom
100128 * @param {boolean?Object} extractData Extract message from theoptions response.- DefaultConfiguration trueoptions
101- * @returns {Promise<string | any>} Extracted data or the raw response
129+ * @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
102133 * @throws {Error}
103134 */
104- static async sendRequestWithPreset(presetName, custom, extractData = true) {
135+ static async processRequest(
105- const presetManager = getPresetManager(this.TYPE);
136+ custom,
106- if (!presetManager) {
137+ options = {},
107- throw new Error('Preset manager not found');
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) {
146+ const presetManager = getPresetManager(this.TYPE);
147+ if (presetManager) {
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+ }
108159 }
109160
110- const preset = presetManager.getCompletionPresetByName(presetName);
161+ // Handle instruct formatting if requested
111162 if (!presetArray.isArray(prompt) && instructName) {
112163 throwconst newinstructPresetManager Error= getPresetManager('Preset not foundinstruct');
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');
113217 }
114218
115- const data = this.createRequestData({ ...preset, ...custom });
219+ // @ts-ignore
220+ const data = this.createRequestData(requestData);
116221
117222 return await this.sendRequest(data, extractData);
118223 }
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+ }
119255}
120256
121257/**
@@ -129,7 +265,7 @@ export class ChatCompletionService {
129265 * @returns {ChatCompletionPayload}
130266 */
131267 static createRequestData({ messages, model, chat_completion_source, max_tokens, temperature, ...props }) {
132268 returnconst payload = {
133269 ...props,
134270 messages,
135271 model,
@@ -138,13 +274,22 @@ export class ChatCompletionService {
138274 temperature,
139275 stream: false,
140276 };
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;
141286 }
142287
143288 /**
144289 * Sends a chat completion request
145290 * @param {ChatCompletionPayload} data Request data
146291 * @param {boolean?} extractData Extract message from the response. Default true
147292 * @returns {Promise<stringExtractedData | any>} Extracted data or the raw response
148293 * @throws {Error}
149294 */
150295 static async sendRequest(data, extractData = true) {
@@ -161,29 +306,82 @@ export class ChatCompletionService {
161306 throw json;
162307 }
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+ };
165321 }
166322
167323 /**
168- * @param {string} presetName
324+ * Process and send a chat completion request with optional preset
169325 * @param {ChatCompletionPayload} custom
170326 * @param {booleanObject} extractData Extract message from theoptions response.- DefaultConfiguration trueoptions
171- * @returns {Promise<string | any>} Extracted data or the raw response
327+ * @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
172330 * @throws {Error}
173331 */
174332 static async sendRequestWithPresetprocessRequest(presetNamecustom, customoptions, extractData = true) {
175333 const presetManager{ presetName } = getPresetManager(this.TYPE)options;
176- if (!presetManager) {
334+ let requestData = { ...custom };
177- throw new Error('Preset manager not found');
178- }
179335
180- const preset = presetManager.getCompletionPresetByName(presetName);
336+ // Apply generation preset if specified
181337 if (!presetpresetName) {
182- throw new Error('Preset not found');
338+ const presetManager = getPresetManager(this.TYPE);
339+ if (presetManager) {
340+ const preset = presetManager.getCompletionPresetByName(presetName);
341+ if (preset) {
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+ }
183351 }
184352
185353 const data = this.createRequestData({ ...preset, ...custom }requestData);
186354
187355 return await this.sendRequest(data, extractData);
188356 }
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+ }
189387}
public/scripts/extensions/connection-manager/index.js+28 -6
@@ -1,4 +1,4 @@
11import { DOMPurify, Fuse } from '../../../lib.js';
22
33import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js';
44import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
@@ -271,9 +271,14 @@ async function createConnectionProfile(forceName = null) {
271271 });
272272 const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n);
273273 const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken);
274274 constlet name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 });
275-
275+ // If it's cancelled, it will be false
276+ if (!name) {
277+ return null;
278+ }
279+ name = DOMPurify.sanitize(String(name));
276280 if (!name) {
281+ toastr.error('Name cannot be empty.');
277282 return null;
278283 }
279284
@@ -307,7 +312,8 @@ async function deleteConnectionProfile() {
307312 return;
308313 }
309314
310315 const nameprofile = extension_settings.connectionManager.profiles[index].name;
316+ const name = profile.name;
311317 const confirm = await Popup.show.confirm(t`Are you sure you want to delete the selected profile?`, name);
312318
313319 if (!confirm) {
@@ -317,6 +323,8 @@ async function deleteConnectionProfile() {
317323 extension_settings.connectionManager.profiles.splice(index, 1);
318324 extension_settings.connectionManager.selectedProfile = null;
319325 saveSettingsDebounced();
326+
327+ await eventSource.emit(event_types.CONNECTION_PROFILE_DELETED, profile);
320328}
321329
322330/**
@@ -516,6 +524,7 @@ async function renderDetailsContent(detailsContent) {
516524 saveSettingsDebounced();
517525 renderConnectionProfiles(profiles);
518526 await renderDetailsContent(detailsContent);
527+ await eventSource.emit(event_types.CONNECTION_PROFILE_CREATED, profile);
519528 await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);
520529 });
521530
@@ -527,9 +536,11 @@ async function renderDetailsContent(detailsContent) {
527536 console.log('No profile selected');
528537 return;
529538 }
539+ const oldProfile = structuredClone(profile);
530540 await updateConnectionProfile(profile);
531541 await renderDetailsContent(detailsContent);
532542 saveSettingsDebounced();
543+ await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
533544 await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);
534545 toastr.success('Connection profile updated', '', { timeOut: 1500 });
535546 });
@@ -563,7 +574,7 @@ async function renderDetailsContent(detailsContent) {
563574 return acc;
564575 }, {});
565576 const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings }));
566577 constlet newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, {
567578 rows: 2,
568579 customButtons: [{
569580 text: t`Save and Update`,
@@ -575,7 +586,13 @@ async function renderDetailsContent(detailsContent) {
575586 }],
576587 });
577588
589+ // If it's cancelled, it will be false
590+ if (!newName) {
591+ return;
592+ }
593+ newName = DOMPurify.sanitize(String(newName));
578594 if (!newName) {
595+ toastr.error('Name cannot be empty.');
579596 return;
580597 }
581598
@@ -588,6 +605,7 @@ async function renderDetailsContent(detailsContent) {
588605 return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0];
589606 }).get();
590607
608+ const oldProfile = structuredClone(profile);
591609 if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) {
592610 profile.exclude = newExcludeList;
593611 for (const command of newExcludeList) {
@@ -602,10 +620,11 @@ async function renderDetailsContent(detailsContent) {
602620
603621 if (profile.name !== newName) {
604622 toastr.success('Connection profile renamed.');
605623 profile.name = String(newName);
606624 }
607625
608626 saveSettingsDebounced();
627+ await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
609628 renderConnectionProfiles(profiles);
610629 await renderDetailsContent(detailsContent);
611630 });
@@ -708,6 +727,7 @@ async function renderDetailsContent(detailsContent) {
708727 saveSettingsDebounced();
709728 renderConnectionProfiles(profiles);
710729 await renderDetailsContent(detailsContent);
730+ await eventSource.emit(event_types.CONNECTION_PROFILE_CREATED, profile);
711731 return profile.name;
712732 },
713733 }));
@@ -722,9 +742,11 @@ async function renderDetailsContent(detailsContent) {
722742 toastr.warning('No profile selected.');
723743 return '';
724744 }
745+ const oldProfile = structuredClone(profile);
725746 await updateConnectionProfile(profile);
726747 await renderDetailsContent(detailsContent);
727748 saveSettingsDebounced();
749+ await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
728750 return profile.name;
729751 },
730752 }));
public/scripts/extensions/shared.js+308 -1
@@ -1,5 +1,6 @@
11import { CONNECT_API_MAP, getRequestHeaders } from '../../script.js';
22import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';
3+import { t } from '../i18n.js';
34import { oai_settings } from '../openai.js';
45import { SECRET_KEYS, secret_state } from '../secrets.js';
56import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
@@ -273,3 +274,309 @@ export async function getWebLlmContextSize() {
273274 const model = await engine.getCurrentModelInfo();
274275 return model?.context_size;
275276}
277+
278+/**
279+ * It uses the profiles to send a generate request to the API. Doesn't support streaming.
280+ */
281+export 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 = {
320320 * @param {string} name1 User name.
321321 * @param {string} name2 Character name.
322322 * @param {boolean|number} forceOutputSequence Force to use first/last output sequence (if configured).
323+ * @param {InstructSettings} customInstruct Custom instruct mode settings.
323324 * @returns {string} Formatted instruct mode chat message.
324325 */
325326export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2, forceOutputSequence, customInstruct = null) {
326327 letconst includeNamesinstruct = isNarratorstructuredClone(customInstruct ? false :? power_user.instruct.names_behavior === names_behavior_types.ALWAYS);
328+ let includeNames = isNarrator ? false : instruct.names_behavior === names_behavior_types.ALWAYS;
327329
328330 if (!isNarrator && power_user.instruct.names_behavior === names_behavior_types.FORCE && ((selected_group && name !== name1) || (forceAvatar && name !== name1))) {
329331 includeNames = true;
330332 }
331333
332334 function getPrefix() {
333335 if (isNarrator) {
334336 return power_user.instruct.system_same_as_user ? power_user.instruct.input_sequence : power_user.instruct.system_sequence;
335337 }
336338
337339 if (isUser) {
338340 if (forceOutputSequence === force_output_sequence.FIRST) {
339341 return power_user.instruct.first_input_sequence || power_user.instruct.input_sequence;
340342 }
341343
342344 if (forceOutputSequence === force_output_sequence.LAST) {
343345 return power_user.instruct.last_input_sequence || power_user.instruct.input_sequence;
344346 }
345347
346348 return power_user.instruct.input_sequence;
347349 }
348350
349351 if (forceOutputSequence === force_output_sequence.FIRST) {
350352 return power_user.instruct.first_output_sequence || power_user.instruct.output_sequence;
351353 }
352354
353355 if (forceOutputSequence === force_output_sequence.LAST) {
354356 return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;
355357 }
356358
357359 return power_user.instruct.output_sequence;
358360 }
359361
360362 function getSuffix() {
361363 if (isNarrator) {
362364 return power_user.instruct.system_same_as_user ? power_user.instruct.input_suffix : power_user.instruct.system_suffix;
363365 }
364366
365367 if (isUser) {
366368 return power_user.instruct.input_suffix;
367369 }
368370
369371 return power_user.instruct.output_suffix;
370372 }
371373
372374 let prefix = getPrefix() || '';
373375 let suffix = getSuffix() || '';
374376
375377 if (power_user.instruct.macro) {
376378 prefix = substituteParams(prefix, name1, name2);
377379 prefix = prefix.replace(/{{name}}/gi, name || 'System');
378380
@@ -380,11 +382,11 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
380382 suffix = suffix.replace(/{{name}}/gi, name || 'System');
381383 }
382384
383385 if (!suffix && power_user.instruct.wrap) {
384386 suffix = '\n';
385387 }
386388
387389 const separator = power_user.instruct.wrap ? '\n' : '';
388390
389391 // Don't include the name if it's empty
390392 const textArray = includeNames && name ? [prefix, `${name}: ${mes}` + suffix] : [prefix, mes + suffix];
@@ -504,30 +506,32 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
504506 * @param {string} name2 Character name.
505507 * @param {boolean} isQuiet Is quiet mode generation.
506508 * @param {boolean} isQuietToLoud Is quiet to loud generation.
509+ * @param {InstructSettings} customInstruct Custom instruct settings.
507510 * @returns {string} Formatted instruct mode last prompt line.
508511 */
509512export 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
512516 function getSequence() {
513517 // User impersonation prompt
514518 if (isImpersonate) {
515519 return power_user.instruct.input_sequence;
516520 }
517521
518522 // Neutral / system / quiet prompt
519523 // Use a special quiet instruct sequence if defined, or assistant's output sequence otherwise
520524 if (isQuiet && !isQuietToLoud) {
521525 return power_user.instruct.last_system_sequence || power_user.instruct.output_sequence;
522526 }
523527
524528 // Quiet in-character prompt
525529 if (isQuiet && isQuietToLoud) {
526530 return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;
527531 }
528532
529533 // Default AI response
530534 return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;
531535 }
532536
533537 let sequence = getSequence() || '';
@@ -536,21 +540,21 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
536540 // A hack for Mistral's formatting that has a normal output sequence ending with a space
537541 if (
538542 includeNames &&
539543 power_user.instruct.last_output_sequence &&
540544 power_user.instruct.output_sequence &&
541545 sequence === power_user.instruct.last_output_sequence &&
542546 /\s$/.test(power_user.instruct.output_sequence) &&
543547 !/\s$/.test(power_user.instruct.last_output_sequence)
544548 ) {
545549 nameFiller = power_user.instruct.output_sequence.slice(-1);
546550 }
547551
548552 if (power_user.instruct.macro) {
549553 sequence = substituteParams(sequence, name1, name2);
550554 sequence = sequence.replace(/{{name}}/gi, name || 'System');
551555 }
552556
553557 const separator = power_user.instruct.wrap ? '\n' : '';
554558 let text = includeNames ? (separator + sequence + separator + nameFiller + `${name}:`) : (separator + sequence);
555559
556560 // Quiet prompt already has a newline at the end
@@ -562,7 +566,7 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
562566 text += (includeNames ? promptBias : (separator + promptBias.trimStart()));
563567 }
564568
565569 return (power_user.instruct.wrap ? text.trimEnd() : text) + (includeNames ? '' : separator);
566570}
567571
568572/**
public/scripts/power-user.js+2 -0
@@ -219,7 +219,9 @@ let power_user = {
219219 system_sequence: '',
220220 system_suffix: '',
221221 last_system_sequence: '',
222+ first_input_sequence: '',
222223 first_output_sequence: '',
224+ last_input_sequence: '',
223225 last_output_sequence: '',
224226 system_sequence_prefix: '',
225227 system_sequence_suffix: '',
public/scripts/reasoning.js+10 -5
@@ -87,19 +87,24 @@ function toggleReasoningAutoExpand() {
8787 * @param {object} data Response data
8888 * @returns {string} Extracted reasoning
8989 */
9090export 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) {
9297 case 'textgenerationwebui':
9398 switch (textGenType ?? textgenerationwebui_settings.type) {
9499 case textgen_types.OPENROUTER:
95100 return data?.choices?.[0]?.reasoning ?? '';
96101 }
97102 break;
98103
99104 case 'openai':
100105 if (!ignoreShowThoughts && !oai_settings.show_thoughts) break;
101106
102107 switch (chatCompletionSource ?? oai_settings.chat_completion_source) {
103108 case chat_completion_sources.DEEPSEEK:
104109 return data?.choices?.[0]?.message?.reasoning_content ?? '';
105110 case chat_completion_sources.OPENROUTER:
public/scripts/st-context.js+2 -0
@@ -80,6 +80,7 @@ import { timestampToMoment, uuidv4 } from './utils.js';
8080import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
8181import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
8282import { ChatCompletionService, TextCompletionService } from './custom-request.js';
83+import { ConnectionManagerRequestService } from './extensions/shared.js';
8384import { updateReasoningUI, parseReasoningFromString } from './reasoning.js';
8485
8586export function getContext() {
@@ -215,6 +216,7 @@ export function getContext() {
215216 clearChat,
216217 ChatCompletionService,
217218 TextCompletionService,
219+ ConnectionManagerRequestService,
218220 updateReasoningUI,
219221 parseReasoningFromString,
220222 unshallowCharacter,
public/scripts/textgen-settings.js+1 -1
@@ -86,7 +86,7 @@ const OOBA_DEFAULT_ORDER = [
8686 'encoder_repetition_penalty',
8787 'no_repeat_ngram',
8888];
8989export const APHRODITE_DEFAULT_ORDER = [
9090 'dry',
9191 'penalties',
9292 'no_repeat_ngram',