New connection manager events, ConnectionManagerRequestService (#3603)

d42a81f97cab485a9442cf576d9a95a023c2f5c3

bmen25124 <bmen25124@gmail.com>

Signed
10 files changed, +635 -77Showing whitespace changes
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+14 -0
@@ -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};
@@ -9196,6 +9199,17 @@ function swipe_right(_event, { source, repeated } = {}) {
9196 }9199 }
9197}9200}
91989201
9202/**
9203 * @typedef {object} ConnectAPIMap
9204 * @property {string} selected - API name (e.g. "textgenerationwebui", "openai")
9205 * @property {string?} [button] - CSS selector for the API button
9206 * @property {string?} [type] - API type, mostly used by text completion. (e.g. "openrouter")
9207 * @property {string?} [source] - API source, mostly used by chat completion. (e.g. "openai")
9208 */
9209
9210/**
9211 * @type {Record<string, ConnectAPIMap>}
9212 */
9199export const CONNECT_API_MAP = {9213export const CONNECT_API_MAP = {
9200 // Default APIs not contined inside text gen / chat gen9214 // Default APIs not contined inside text gen / chat gen
9201 'kobold': {9215 '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';
@@ -267,9 +267,14 @@ async function createConnectionProfile(forceName = null) {
267 });267 });
268 const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n);268 const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n);
269 const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken);269 const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken);
270 const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 });270 let name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 });
271271 // If it's cancelled, it will be false
272 if (!name) {
273 return null;
274 }
275 name = DOMPurify.sanitize(String(name));
272 if (!name) {276 if (!name) {
277 toastr.error('Name cannot be empty.');
273 return null;278 return null;
274 }279 }
275280
@@ -303,7 +308,8 @@ async function deleteConnectionProfile() {
303 return;308 return;
304 }309 }
305310
306 const name = extension_settings.connectionManager.profiles[index].name;311 const profile = extension_settings.connectionManager.profiles[index];
312 const name = profile.name;
307 const confirm = await Popup.show.confirm(t`Are you sure you want to delete the selected profile?`, name);313 const confirm = await Popup.show.confirm(t`Are you sure you want to delete the selected profile?`, name);
308314
309 if (!confirm) {315 if (!confirm) {
@@ -313,6 +319,8 @@ async function deleteConnectionProfile() {
313 extension_settings.connectionManager.profiles.splice(index, 1);319 extension_settings.connectionManager.profiles.splice(index, 1);
314 extension_settings.connectionManager.selectedProfile = null;320 extension_settings.connectionManager.selectedProfile = null;
315 saveSettingsDebounced();321 saveSettingsDebounced();
322
323 await eventSource.emit(event_types.CONNECTION_PROFILE_DELETED, profile);
316}324}
317325
318/**326/**
@@ -512,6 +520,7 @@ async function renderDetailsContent(detailsContent) {
512 saveSettingsDebounced();520 saveSettingsDebounced();
513 renderConnectionProfiles(profiles);521 renderConnectionProfiles(profiles);
514 await renderDetailsContent(detailsContent);522 await renderDetailsContent(detailsContent);
523 await eventSource.emit(event_types.CONNECTION_PROFILE_CREATED, profile);
515 await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);524 await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);
516 });525 });
517526
@@ -523,9 +532,11 @@ async function renderDetailsContent(detailsContent) {
523 console.log('No profile selected');532 console.log('No profile selected');
524 return;533 return;
525 }534 }
535 const oldProfile = structuredClone(profile);
526 await updateConnectionProfile(profile);536 await updateConnectionProfile(profile);
527 await renderDetailsContent(detailsContent);537 await renderDetailsContent(detailsContent);
528 saveSettingsDebounced();538 saveSettingsDebounced();
539 await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
529 await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);540 await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);
530 toastr.success('Connection profile updated', '', { timeOut: 1500 });541 toastr.success('Connection profile updated', '', { timeOut: 1500 });
531 });542 });
@@ -559,7 +570,7 @@ async function renderDetailsContent(detailsContent) {
559 return acc;570 return acc;
560 }, {});571 }, {});
561 const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings }));572 const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings }));
562 const newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, {573 let newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, {
563 rows: 2,574 rows: 2,
564 customButtons: [{575 customButtons: [{
565 text: t`Save and Update`,576 text: t`Save and Update`,
@@ -571,7 +582,13 @@ async function renderDetailsContent(detailsContent) {
571 }],582 }],
572 });583 });
573584
585 // If it's cancelled, it will be false
586 if (!newName) {
587 return;
588 }
589 newName = DOMPurify.sanitize(String(newName));
574 if (!newName) {590 if (!newName) {
591 toastr.error('Name cannot be empty.');
575 return;592 return;
576 }593 }
577594
@@ -584,6 +601,7 @@ async function renderDetailsContent(detailsContent) {
584 return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0];601 return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0];
585 }).get();602 }).get();
586603
604 const oldProfile = structuredClone(profile);
587 if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) {605 if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) {
588 profile.exclude = newExcludeList;606 profile.exclude = newExcludeList;
589 for (const command of newExcludeList) {607 for (const command of newExcludeList) {
@@ -598,10 +616,11 @@ async function renderDetailsContent(detailsContent) {
598616
599 if (profile.name !== newName) {617 if (profile.name !== newName) {
600 toastr.success('Connection profile renamed.');618 toastr.success('Connection profile renamed.');
601 profile.name = String(newName);619 profile.name = newName;
602 }620 }
603621
604 saveSettingsDebounced();622 saveSettingsDebounced();
623 await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
605 renderConnectionProfiles(profiles);624 renderConnectionProfiles(profiles);
606 await renderDetailsContent(detailsContent);625 await renderDetailsContent(detailsContent);
607 });626 });
@@ -704,6 +723,7 @@ async function renderDetailsContent(detailsContent) {
704 saveSettingsDebounced();723 saveSettingsDebounced();
705 renderConnectionProfiles(profiles);724 renderConnectionProfiles(profiles);
706 await renderDetailsContent(detailsContent);725 await renderDetailsContent(detailsContent);
726 await eventSource.emit(event_types.CONNECTION_PROFILE_CREATED, profile);
707 return profile.name;727 return profile.name;
708 },728 },
709 }));729 }));
@@ -718,9 +738,11 @@ async function renderDetailsContent(detailsContent) {
718 toastr.warning('No profile selected.');738 toastr.warning('No profile selected.');
719 return '';739 return '';
720 }740 }
741 const oldProfile = structuredClone(profile);
721 await updateConnectionProfile(profile);742 await updateConnectionProfile(profile);
722 await renderDetailsContent(detailsContent);743 await renderDetailsContent(detailsContent);
723 saveSettingsDebounced();744 saveSettingsDebounced();
745 await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile);
724 return profile.name;746 return profile.name;
725 },747 },
726 }));748 }));
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
@@ -218,7 +218,9 @@ let power_user = {
218 system_sequence: '',218 system_sequence: '',
219 system_suffix: '',219 system_suffix: '',
220 last_system_sequence: '',220 last_system_sequence: '',
221 first_input_sequence: '',
221 first_output_sequence: '',222 first_output_sequence: '',
223 last_input_sequence: '',
222 last_output_sequence: '',224 last_output_sequence: '',
223 system_sequence_prefix: '',225 system_sequence_prefix: '',
224 system_sequence_suffix: '',226 system_sequence_suffix: '',
public/scripts/reasoning.js+10 -5
@@ -57,19 +57,24 @@ function toggleReasoningAutoExpand() {
57 * @param {object} data Response data57 * @param {object} data Response data
58 * @returns {string} Extracted reasoning58 * @returns {string} Extracted reasoning
59 */59 */
60export function extractReasoningFromData(data) {60export function extractReasoningFromData(data, {
61 switch (main_api) {61 mainApi = null,
62 ignoreShowThoughts = false,
63 textGenType = null,
64 chatCompletionSource = null
65} = {}) {
66 switch (mainApi ?? main_api) {
62 case 'textgenerationwebui':67 case 'textgenerationwebui':
63 switch (textgenerationwebui_settings.type) {68 switch (textGenType ?? textgenerationwebui_settings.type) {
64 case textgen_types.OPENROUTER:69 case textgen_types.OPENROUTER:
65 return data?.choices?.[0]?.reasoning ?? '';70 return data?.choices?.[0]?.reasoning ?? '';
66 }71 }
67 break;72 break;
6873
69 case 'openai':74 case 'openai':
70 if (!oai_settings.show_thoughts) break;75 if (!ignoreShowThoughts && !oai_settings.show_thoughts) break;
7176
72 switch (oai_settings.chat_completion_source) {77 switch (chatCompletionSource ?? oai_settings.chat_completion_source) {
73 case chat_completion_sources.DEEPSEEK:78 case chat_completion_sources.DEEPSEEK:
74 return data?.choices?.[0]?.message?.reasoning_content ?? '';79 return data?.choices?.[0]?.message?.reasoning_content ?? '';
75 case chat_completion_sources.OPENROUTER:80 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',