mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	New connection manager events, ConnectionManagerRequestService (#3603)
This commit is contained in:
		
							
								
								
									
										4
									
								
								public/global.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								public/global.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,7 +1,11 @@ | |||||||
| import libs from './lib'; | import libs from './lib'; | ||||||
| import getContext from './scripts/st-context'; | import getContext from './scripts/st-context'; | ||||||
|  | import { power_user } from './scripts/power-user'; | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|  |     // Custom types | ||||||
|  |     declare type InstructSettings = typeof power_user.instruct; | ||||||
|  |  | ||||||
|     // Global namespace modules |     // Global namespace modules | ||||||
|     interface Window { |     interface Window { | ||||||
|         ai: any; |         ai: any; | ||||||
|   | |||||||
| @@ -514,6 +514,9 @@ export const event_types = { | |||||||
|     ONLINE_STATUS_CHANGED: 'online_status_changed', |     ONLINE_STATUS_CHANGED: 'online_status_changed', | ||||||
|     IMAGE_SWIPED: 'image_swiped', |     IMAGE_SWIPED: 'image_swiped', | ||||||
|     CONNECTION_PROFILE_LOADED: 'connection_profile_loaded', |     CONNECTION_PROFILE_LOADED: 'connection_profile_loaded', | ||||||
|  |     CONNECTION_PROFILE_CREATED: 'connection_profile_created', | ||||||
|  |     CONNECTION_PROFILE_DELETED: 'connection_profile_deleted', | ||||||
|  |     CONNECTION_PROFILE_UPDATED: 'connection_profile_updated', | ||||||
|     TOOL_CALLS_PERFORMED: 'tool_calls_performed', |     TOOL_CALLS_PERFORMED: 'tool_calls_performed', | ||||||
|     TOOL_CALLS_RENDERED: 'tool_calls_rendered', |     TOOL_CALLS_RENDERED: 'tool_calls_rendered', | ||||||
| }; | }; | ||||||
| @@ -9196,6 +9199,17 @@ function swipe_right(_event, { source, repeated } = {}) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ConnectAPIMap | ||||||
|  |  * @property {string} selected - API name (e.g. "textgenerationwebui", "openai") | ||||||
|  |  * @property {string?} [button] - CSS selector for the API button | ||||||
|  |  * @property {string?} [type] - API type, mostly used by text completion. (e.g. "openrouter") | ||||||
|  |  * @property {string?} [source] - API source, mostly used by chat completion. (e.g. "openai") | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @type {Record<string, ConnectAPIMap>} | ||||||
|  |  */ | ||||||
| export const CONNECT_API_MAP = { | export const CONNECT_API_MAP = { | ||||||
|     // Default APIs not contined inside text gen / chat gen |     // Default APIs not contined inside text gen / chat gen | ||||||
|     'kobold': { |     'kobold': { | ||||||
|   | |||||||
| @@ -1,20 +1,20 @@ | |||||||
| import { getPresetManager } from './preset-manager.js'; | import { getPresetManager } from './preset-manager.js'; | ||||||
| import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js'; | import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js'; | ||||||
| import { getTextGenServer } from './textgen-settings.js'; | import { getTextGenServer } from './textgen-settings.js'; | ||||||
|  | import { extractReasoningFromData } from './reasoning.js'; | ||||||
|  | import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js'; | ||||||
|  |  | ||||||
| // #region Type Definitions | // #region Type Definitions | ||||||
| /** | /** | ||||||
|  * @typedef {Object} TextCompletionRequestBase |  * @typedef {Object} TextCompletionRequestBase | ||||||
|  * @property {string} prompt - The text prompt for completion |  | ||||||
|  * @property {number} max_tokens - Maximum number of tokens to generate |  * @property {number} max_tokens - Maximum number of tokens to generate | ||||||
|  * @property {string} [model] - Optional model name |  * @property {string} [model] - Optional model name | ||||||
|  * @property {string} api_type - Type of API to use |  * @property {string} api_type - Type of API to use | ||||||
|  * @property {string} [api_server] - Optional API server URL |  * @property {string} [api_server] - Optional API server URL | ||||||
|  * @property {number} [temperature] - Optional temperature parameter |  * @property {number} [temperature] - Optional temperature parameter | ||||||
|  |  * @property {number} [min_p] - Optional min_p parameter | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
| /** @typedef {Record<string, any> & TextCompletionRequestBase} TextCompletionRequest */ |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @typedef {Object} TextCompletionPayloadBase |  * @typedef {Object} TextCompletionPayloadBase | ||||||
|  * @property {string} prompt - The text prompt for completion |  * @property {string} prompt - The text prompt for completion | ||||||
| @@ -44,6 +44,13 @@ import { getTextGenServer } from './textgen-settings.js'; | |||||||
|  */ |  */ | ||||||
|  |  | ||||||
| /** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */ | /** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {Object} ExtractedData | ||||||
|  |  * @property {string} content - Extracted content. | ||||||
|  |  * @property {string} reasoning - Extracted reasoning. | ||||||
|  |  */ | ||||||
|  |  | ||||||
| // #endregion | // #endregion | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -53,11 +60,11 @@ export class TextCompletionService { | |||||||
|     static TYPE = 'textgenerationwebui'; |     static TYPE = 'textgenerationwebui'; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * @param {TextCompletionRequest} custom |      * @param {Record<string, any> & TextCompletionRequestBase & {prompt: string}} custom | ||||||
|      * @returns {TextCompletionPayload} |      * @returns {TextCompletionPayload} | ||||||
|      */ |      */ | ||||||
|     static createRequestData({ prompt, max_tokens, model, api_type, api_server, temperature, ...props }) { |     static createRequestData({ prompt, max_tokens, model, api_type, api_server, temperature, min_p, ...props }) { | ||||||
|         return { |         const payload = { | ||||||
|             ...props, |             ...props, | ||||||
|             prompt, |             prompt, | ||||||
|             max_tokens, |             max_tokens, | ||||||
| @@ -66,15 +73,25 @@ export class TextCompletionService { | |||||||
|             api_type, |             api_type, | ||||||
|             api_server: api_server ?? getTextGenServer(api_type), |             api_server: api_server ?? getTextGenServer(api_type), | ||||||
|             temperature, |             temperature, | ||||||
|  |             min_p, | ||||||
|             stream: false, |             stream: false, | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|  |         // Remove undefined values to avoid API errors | ||||||
|  |         Object.keys(payload).forEach(key => { | ||||||
|  |             if (payload[key] === undefined) { | ||||||
|  |                 delete payload[key]; | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         return payload; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Sends a text completion request to the specified server |      * Sends a text completion request to the specified server | ||||||
|      * @param {TextCompletionPayload} data Request data |      * @param {TextCompletionPayload} data Request data | ||||||
|      * @param {boolean?} extractData Extract message from the response. Default true |      * @param {boolean?} extractData Extract message from the response. Default true | ||||||
|      * @returns {Promise<string | any>} Extracted data or the raw response |      * @returns {Promise<ExtractedData | any>} Extracted data or the raw response | ||||||
|      * @throws {Error} |      * @throws {Error} | ||||||
|      */ |      */ | ||||||
|     static async sendRequest(data, extractData = true) { |     static async sendRequest(data, extractData = true) { | ||||||
| @@ -91,31 +108,150 @@ export class TextCompletionService { | |||||||
|             throw json; |             throw json; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return extractData ? extractMessageFromData(json, this.TYPE) : json; |         if (!extractData) { | ||||||
|  |             return json; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return { | ||||||
|  |             content: extractMessageFromData(json, this.TYPE), | ||||||
|  |             reasoning: extractReasoningFromData(json, { | ||||||
|  |                 mainApi: this.TYPE, | ||||||
|  |                 textGenType: data.api_type, | ||||||
|  |                 ignoreShowThoughts: true, | ||||||
|  |             }), | ||||||
|  |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * @param {string} presetName |      * Process and send a text completion request with optional preset & instruct | ||||||
|      * @param {TextCompletionRequest} custom |      * @param {Record<string, any> & TextCompletionRequestBase & {prompt: (ChatCompletionMessage & {ignoreInstruct?: boolean})[] |string}} custom | ||||||
|      * @param {boolean?} extractData Extract message from the response. Default true |      * @param {Object} options - Configuration options | ||||||
|      * @returns {Promise<string | any>} Extracted data or the raw response |      * @param {string?} [options.presetName] - Name of the preset to use for generation settings | ||||||
|  |      * @param {string?} [options.instructName] - Name of instruct preset for message formatting | ||||||
|  |      * @param {boolean} extractData - Whether to extract structured data from response | ||||||
|  |      * @returns {Promise<ExtractedData | any>} Extracted data or the raw response | ||||||
|      * @throws {Error} |      * @throws {Error} | ||||||
|      */ |      */ | ||||||
|     static async sendRequestWithPreset(presetName, custom, extractData = true) { |     static async processRequest( | ||||||
|  |         custom, | ||||||
|  |         options = {}, | ||||||
|  |         extractData = true, | ||||||
|  |     ) { | ||||||
|  |         const { presetName, instructName } = options; | ||||||
|  |         let requestData = { ...custom }; | ||||||
|  |         const prompt = custom.prompt; | ||||||
|  |  | ||||||
|  |         // Apply generation preset if specified | ||||||
|  |         if (presetName) { | ||||||
|             const presetManager = getPresetManager(this.TYPE); |             const presetManager = getPresetManager(this.TYPE); | ||||||
|         if (!presetManager) { |             if (presetManager) { | ||||||
|             throw new Error('Preset manager not found'); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|                 const preset = presetManager.getCompletionPresetByName(presetName); |                 const preset = presetManager.getCompletionPresetByName(presetName); | ||||||
|         if (!preset) { |                 if (preset) { | ||||||
|             throw new Error('Preset not found'); |                     // Convert preset to payload and merge with custom parameters | ||||||
|  |                     const presetPayload = this.presetToGeneratePayload(preset, {}); | ||||||
|  |                     requestData = { ...presetPayload, ...requestData }; | ||||||
|  |                 } else { | ||||||
|  |                     console.warn(`Preset "${presetName}" not found, continuing with default settings`); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 console.warn('Preset manager not found, continuing with default settings'); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const data = this.createRequestData({ ...preset, ...custom }); |         // Handle instruct formatting if requested | ||||||
|  |         if (Array.isArray(prompt) && instructName) { | ||||||
|  |             const instructPresetManager = getPresetManager('instruct'); | ||||||
|  |             let instructPreset = instructPresetManager?.getCompletionPresetByName(instructName); | ||||||
|  |             if (instructPreset) { | ||||||
|  |                 // Clone the preset to avoid modifying the original | ||||||
|  |                 instructPreset = structuredClone(instructPreset); | ||||||
|  |                 instructPreset.macro = false; | ||||||
|  |                 instructPreset.names_behavior = names_behavior_types.NONE; | ||||||
|  |  | ||||||
|  |                 // Format messages using instruct formatting | ||||||
|  |                 const formattedMessages = []; | ||||||
|  |                 for (const message of prompt) { | ||||||
|  |                     let messageContent = message.content; | ||||||
|  |                     if (!message.ignoreInstruct) { | ||||||
|  |                         messageContent = formatInstructModeChat( | ||||||
|  |                             message.role, | ||||||
|  |                             message.content, | ||||||
|  |                             message.role === 'user', | ||||||
|  |                             false, | ||||||
|  |                             undefined, | ||||||
|  |                             undefined, | ||||||
|  |                             undefined, | ||||||
|  |                             undefined, | ||||||
|  |                             instructPreset, | ||||||
|  |                         ); | ||||||
|  |  | ||||||
|  |                         // Add prompt formatting for the last message | ||||||
|  |                         if (message === prompt[prompt.length - 1]) { | ||||||
|  |                             messageContent += formatInstructModePrompt( | ||||||
|  |                                 undefined, | ||||||
|  |                                 false, | ||||||
|  |                                 undefined, | ||||||
|  |                                 undefined, | ||||||
|  |                                 undefined, | ||||||
|  |                                 false, | ||||||
|  |                                 false, | ||||||
|  |                                 instructPreset, | ||||||
|  |                             ); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     formattedMessages.push(messageContent); | ||||||
|  |                 } | ||||||
|  |                 requestData.prompt = formattedMessages.join(''); | ||||||
|  |                 if (instructPreset.output_suffix) { | ||||||
|  |                     requestData.stop = [instructPreset.output_suffix]; | ||||||
|  |                     requestData.stopping_strings = [instructPreset.output_suffix]; | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 console.warn(`Instruct preset "${instructName}" not found, using basic formatting`); | ||||||
|  |                 requestData.prompt = prompt.map(x => x.content).join('\n\n'); | ||||||
|  |             } | ||||||
|  |         } else if (typeof prompt === 'string') { | ||||||
|  |             requestData.prompt = prompt; | ||||||
|  |         } else { | ||||||
|  |             requestData.prompt = prompt.map(x => x.content).join('\n\n'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // @ts-ignore | ||||||
|  |         const data = this.createRequestData(requestData); | ||||||
|  |  | ||||||
|         return await this.sendRequest(data, extractData); |         return await this.sendRequest(data, extractData); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Converts a preset to a valid text completion payload. | ||||||
|  |      * Only supports temperature. | ||||||
|  |      * @param {Object} preset - The preset configuration | ||||||
|  |      * @param {Object} customPreset - Additional parameters to override preset values | ||||||
|  |      * @returns {Object} - Formatted payload for text completion API | ||||||
|  |      */ | ||||||
|  |     static presetToGeneratePayload(preset, customPreset = {}) { | ||||||
|  |         if (!preset || typeof preset !== 'object') { | ||||||
|  |             throw new Error('Invalid preset: must be an object'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Merge preset with custom parameters | ||||||
|  |         const settings = { ...preset, ...customPreset }; | ||||||
|  |  | ||||||
|  |         // Initialize base payload with common parameters | ||||||
|  |         let payload = { | ||||||
|  |             'temperature': settings.temp ? Number(settings.temp) : undefined, | ||||||
|  |             'min_p': settings.min_p ? Number(settings.min_p) : undefined, | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         // Remove undefined values to avoid API errors | ||||||
|  |         Object.keys(payload).forEach(key => { | ||||||
|  |             if (payload[key] === undefined) { | ||||||
|  |                 delete payload[key]; | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         return payload; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -129,7 +265,7 @@ export class ChatCompletionService { | |||||||
|      * @returns {ChatCompletionPayload} |      * @returns {ChatCompletionPayload} | ||||||
|      */ |      */ | ||||||
|     static createRequestData({ messages, model, chat_completion_source, max_tokens, temperature, ...props }) { |     static createRequestData({ messages, model, chat_completion_source, max_tokens, temperature, ...props }) { | ||||||
|         return { |         const payload = { | ||||||
|             ...props, |             ...props, | ||||||
|             messages, |             messages, | ||||||
|             model, |             model, | ||||||
| @@ -138,13 +274,22 @@ export class ChatCompletionService { | |||||||
|             temperature, |             temperature, | ||||||
|             stream: false, |             stream: false, | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|  |         // Remove undefined values to avoid API errors | ||||||
|  |         Object.keys(payload).forEach(key => { | ||||||
|  |             if (payload[key] === undefined) { | ||||||
|  |                 delete payload[key]; | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         return payload; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Sends a chat completion request |      * Sends a chat completion request | ||||||
|      * @param {ChatCompletionPayload} data Request data |      * @param {ChatCompletionPayload} data Request data | ||||||
|      * @param {boolean?} extractData Extract message from the response. Default true |      * @param {boolean?} extractData Extract message from the response. Default true | ||||||
|      * @returns {Promise<string | any>} Extracted data or the raw response |      * @returns {Promise<ExtractedData | any>} Extracted data or the raw response | ||||||
|      * @throws {Error} |      * @throws {Error} | ||||||
|      */ |      */ | ||||||
|     static async sendRequest(data, extractData = true) { |     static async sendRequest(data, extractData = true) { | ||||||
| @@ -161,29 +306,82 @@ export class ChatCompletionService { | |||||||
|             throw json; |             throw json; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return extractData ? extractMessageFromData(json, this.TYPE) : json; |         if (!extractData) { | ||||||
|  |             return json; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return { | ||||||
|  |             content: extractMessageFromData(json, this.TYPE), | ||||||
|  |             reasoning: extractReasoningFromData(json, { | ||||||
|  |                 mainApi: this.TYPE, | ||||||
|  |                 textGenType: data.chat_completion_source, | ||||||
|  |                 ignoreShowThoughts: true, | ||||||
|  |             }), | ||||||
|  |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * @param {string} presetName |      * Process and send a chat completion request with optional preset | ||||||
|      * @param {ChatCompletionPayload} custom |      * @param {ChatCompletionPayload} custom | ||||||
|      * @param {boolean} extractData Extract message from the response. Default true |      * @param {Object} options - Configuration options | ||||||
|      * @returns {Promise<string | any>} Extracted data or the raw response |      * @param {string?} [options.presetName] - Name of the preset to use for generation settings | ||||||
|  |      * @param {boolean} extractData - Whether to extract structured data from response | ||||||
|  |      * @returns {Promise<ExtractedData | any>} Extracted data or the raw response | ||||||
|      * @throws {Error} |      * @throws {Error} | ||||||
|      */ |      */ | ||||||
|     static async sendRequestWithPreset(presetName, custom, extractData = true) { |     static async processRequest(custom, options, extractData = true) { | ||||||
|  |         const { presetName } = options; | ||||||
|  |         let requestData = { ...custom }; | ||||||
|  |  | ||||||
|  |         // Apply generation preset if specified | ||||||
|  |         if (presetName) { | ||||||
|             const presetManager = getPresetManager(this.TYPE); |             const presetManager = getPresetManager(this.TYPE); | ||||||
|         if (!presetManager) { |             if (presetManager) { | ||||||
|             throw new Error('Preset manager not found'); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|                 const preset = presetManager.getCompletionPresetByName(presetName); |                 const preset = presetManager.getCompletionPresetByName(presetName); | ||||||
|         if (!preset) { |                 if (preset) { | ||||||
|             throw new Error('Preset not found'); |                     // Convert preset to payload and merge with custom parameters | ||||||
|  |                     const presetPayload = this.presetToGeneratePayload(preset, {}); | ||||||
|  |                     requestData = { ...presetPayload, ...requestData }; | ||||||
|  |                 } else { | ||||||
|  |                     console.warn(`Preset "${presetName}" not found, continuing with default settings`); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 console.warn('Preset manager not found, continuing with default settings'); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const data = this.createRequestData({ ...preset, ...custom }); |         const data = this.createRequestData(requestData); | ||||||
|  |  | ||||||
|         return await this.sendRequest(data, extractData); |         return await this.sendRequest(data, extractData); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Converts a preset to a valid chat completion payload | ||||||
|  |      * Only supports temperature. | ||||||
|  |      * @param {Object} preset - The preset configuration | ||||||
|  |      * @param {Object} customParams - Additional parameters to override preset values | ||||||
|  |      * @returns {Object} - Formatted payload for chat completion API | ||||||
|  |      */ | ||||||
|  |     static presetToGeneratePayload(preset, customParams = {}) { | ||||||
|  |         if (!preset || typeof preset !== 'object') { | ||||||
|  |             throw new Error('Invalid preset: must be an object'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Merge preset with custom parameters | ||||||
|  |         const settings = { ...preset, ...customParams }; | ||||||
|  |  | ||||||
|  |         // Initialize base payload with common parameters | ||||||
|  |         const payload = { | ||||||
|  |             temperature: settings.temperature ? Number(settings.temperature) : undefined, | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         // Remove undefined values to avoid API errors | ||||||
|  |         Object.keys(payload).forEach(key => { | ||||||
|  |             if (payload[key] === undefined) { | ||||||
|  |                 delete payload[key]; | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         return payload; | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { Fuse } from '../../../lib.js'; | import { DOMPurify, Fuse } from '../../../lib.js'; | ||||||
|  |  | ||||||
| import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js'; | import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js'; | ||||||
| import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js'; | import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js'; | ||||||
| @@ -267,11 +267,16 @@ async function createConnectionProfile(forceName = null) { | |||||||
|     }); |     }); | ||||||
|     const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n); |     const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n); | ||||||
|     const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken); |     const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken); | ||||||
|     const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 }); |     let name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 }); | ||||||
|  |     // If it's cancelled, it will be false | ||||||
|     if (!name) { |     if (!name) { | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
|  |     name = DOMPurify.sanitize(String(name)); | ||||||
|  |     if (!name) { | ||||||
|  |         toastr.error('Name cannot be empty.'); | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (isNameTaken(name) || name === NONE) { |     if (isNameTaken(name) || name === NONE) { | ||||||
|         toastr.error('A profile with the same name already exists.'); |         toastr.error('A profile with the same name already exists.'); | ||||||
| @@ -303,7 +308,8 @@ async function deleteConnectionProfile() { | |||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const name = extension_settings.connectionManager.profiles[index].name; |     const profile = extension_settings.connectionManager.profiles[index]; | ||||||
|  |     const name = profile.name; | ||||||
|     const confirm = await Popup.show.confirm(t`Are you sure you want to delete the selected profile?`, name); |     const confirm = await Popup.show.confirm(t`Are you sure you want to delete the selected profile?`, name); | ||||||
|  |  | ||||||
|     if (!confirm) { |     if (!confirm) { | ||||||
| @@ -313,6 +319,8 @@ async function deleteConnectionProfile() { | |||||||
|     extension_settings.connectionManager.profiles.splice(index, 1); |     extension_settings.connectionManager.profiles.splice(index, 1); | ||||||
|     extension_settings.connectionManager.selectedProfile = null; |     extension_settings.connectionManager.selectedProfile = null; | ||||||
|     saveSettingsDebounced(); |     saveSettingsDebounced(); | ||||||
|  |  | ||||||
|  |     await eventSource.emit(event_types.CONNECTION_PROFILE_DELETED, profile); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -512,6 +520,7 @@ async function renderDetailsContent(detailsContent) { | |||||||
|         saveSettingsDebounced(); |         saveSettingsDebounced(); | ||||||
|         renderConnectionProfiles(profiles); |         renderConnectionProfiles(profiles); | ||||||
|         await renderDetailsContent(detailsContent); |         await renderDetailsContent(detailsContent); | ||||||
|  |         await eventSource.emit(event_types.CONNECTION_PROFILE_CREATED, profile); | ||||||
|         await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); |         await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
| @@ -523,9 +532,11 @@ async function renderDetailsContent(detailsContent) { | |||||||
|             console.log('No profile selected'); |             console.log('No profile selected'); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |         const oldProfile = structuredClone(profile); | ||||||
|         await updateConnectionProfile(profile); |         await updateConnectionProfile(profile); | ||||||
|         await renderDetailsContent(detailsContent); |         await renderDetailsContent(detailsContent); | ||||||
|         saveSettingsDebounced(); |         saveSettingsDebounced(); | ||||||
|  |         await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile); | ||||||
|         await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); |         await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); | ||||||
|         toastr.success('Connection profile updated', '', { timeOut: 1500 }); |         toastr.success('Connection profile updated', '', { timeOut: 1500 }); | ||||||
|     }); |     }); | ||||||
| @@ -559,7 +570,7 @@ async function renderDetailsContent(detailsContent) { | |||||||
|             return acc; |             return acc; | ||||||
|         }, {}); |         }, {}); | ||||||
|         const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings })); |         const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings })); | ||||||
|         const newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, { |         let newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, { | ||||||
|             rows: 2, |             rows: 2, | ||||||
|             customButtons: [{ |             customButtons: [{ | ||||||
|                 text: t`Save and Update`, |                 text: t`Save and Update`, | ||||||
| @@ -571,9 +582,15 @@ async function renderDetailsContent(detailsContent) { | |||||||
|             }], |             }], | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  |         // If it's cancelled, it will be false | ||||||
|         if (!newName) { |         if (!newName) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |         newName = DOMPurify.sanitize(String(newName)); | ||||||
|  |         if (!newName) { | ||||||
|  |             toastr.error('Name cannot be empty.'); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         if (profile.name !== newName && extension_settings.connectionManager.profiles.some(p => p.name === newName)) { |         if (profile.name !== newName && extension_settings.connectionManager.profiles.some(p => p.name === newName)) { | ||||||
|             toastr.error('A profile with the same name already exists.'); |             toastr.error('A profile with the same name already exists.'); | ||||||
| @@ -584,6 +601,7 @@ async function renderDetailsContent(detailsContent) { | |||||||
|             return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0]; |             return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0]; | ||||||
|         }).get(); |         }).get(); | ||||||
|  |  | ||||||
|  |         const oldProfile = structuredClone(profile); | ||||||
|         if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) { |         if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) { | ||||||
|             profile.exclude = newExcludeList; |             profile.exclude = newExcludeList; | ||||||
|             for (const command of newExcludeList) { |             for (const command of newExcludeList) { | ||||||
| @@ -598,10 +616,11 @@ async function renderDetailsContent(detailsContent) { | |||||||
|  |  | ||||||
|         if (profile.name !== newName) { |         if (profile.name !== newName) { | ||||||
|             toastr.success('Connection profile renamed.'); |             toastr.success('Connection profile renamed.'); | ||||||
|             profile.name = String(newName); |             profile.name = newName; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         saveSettingsDebounced(); |         saveSettingsDebounced(); | ||||||
|  |         await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile); | ||||||
|         renderConnectionProfiles(profiles); |         renderConnectionProfiles(profiles); | ||||||
|         await renderDetailsContent(detailsContent); |         await renderDetailsContent(detailsContent); | ||||||
|     }); |     }); | ||||||
| @@ -704,6 +723,7 @@ async function renderDetailsContent(detailsContent) { | |||||||
|             saveSettingsDebounced(); |             saveSettingsDebounced(); | ||||||
|             renderConnectionProfiles(profiles); |             renderConnectionProfiles(profiles); | ||||||
|             await renderDetailsContent(detailsContent); |             await renderDetailsContent(detailsContent); | ||||||
|  |             await eventSource.emit(event_types.CONNECTION_PROFILE_CREATED, profile); | ||||||
|             return profile.name; |             return profile.name; | ||||||
|         }, |         }, | ||||||
|     })); |     })); | ||||||
| @@ -718,9 +738,11 @@ async function renderDetailsContent(detailsContent) { | |||||||
|                 toastr.warning('No profile selected.'); |                 toastr.warning('No profile selected.'); | ||||||
|                 return ''; |                 return ''; | ||||||
|             } |             } | ||||||
|  |             const oldProfile = structuredClone(profile); | ||||||
|             await updateConnectionProfile(profile); |             await updateConnectionProfile(profile); | ||||||
|             await renderDetailsContent(detailsContent); |             await renderDetailsContent(detailsContent); | ||||||
|             saveSettingsDebounced(); |             saveSettingsDebounced(); | ||||||
|  |             await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile); | ||||||
|             return profile.name; |             return profile.name; | ||||||
|         }, |         }, | ||||||
|     })); |     })); | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import { getRequestHeaders } from '../../script.js'; | import { CONNECT_API_MAP, getRequestHeaders } from '../../script.js'; | ||||||
| import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js'; | import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js'; | ||||||
|  | import { t } from '../i18n.js'; | ||||||
| import { oai_settings } from '../openai.js'; | import { oai_settings } from '../openai.js'; | ||||||
| import { SECRET_KEYS, secret_state } from '../secrets.js'; | import { SECRET_KEYS, secret_state } from '../secrets.js'; | ||||||
| import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js'; | import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js'; | ||||||
| @@ -273,3 +274,309 @@ export async function getWebLlmContextSize() { | |||||||
|     const model = await engine.getCurrentModelInfo(); |     const model = await engine.getCurrentModelInfo(); | ||||||
|     return model?.context_size; |     return model?.context_size; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * It uses the profiles to send a generate request to the API. Doesn't support streaming. | ||||||
|  |  */ | ||||||
|  | export class ConnectionManagerRequestService { | ||||||
|  |     static defaultSendRequestParams = { | ||||||
|  |         extractData: true, | ||||||
|  |         includePreset: true, | ||||||
|  |         includeInstruct: true, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     static getAllowedTypes() { | ||||||
|  |         return { | ||||||
|  |             openai: t`Chat Completion`, | ||||||
|  |             textgenerationwebui: t`Text Completion`, | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param {string} profileId | ||||||
|  |      * @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt | ||||||
|  |      * @param {number} maxTokens | ||||||
|  |      * @param {{extractData?: boolean, includePreset?: boolean, includeInstruct?: boolean}} custom - default values are true | ||||||
|  |      * @returns {Promise<import('../custom-request.js').ExtractedData | any>} Extracted data or the raw response | ||||||
|  |      */ | ||||||
|  |     static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) { | ||||||
|  |         const { extractData, includePreset, includeInstruct } = { ...this.defaultSendRequestParams, ...custom }; | ||||||
|  |  | ||||||
|  |         const context = SillyTavern.getContext(); | ||||||
|  |         if (context.extensionSettings.disabledExtensions.includes('connection-manager')) { | ||||||
|  |             throw new Error('Connection Manager is not available'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const profile = context.extensionSettings.connectionManager.profiles.find((p) => p.id === profileId); | ||||||
|  |         const selectedApiMap = this.validateProfile(profile); | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             switch (selectedApiMap.selected) { | ||||||
|  |                 case 'openai': { | ||||||
|  |                     if (!selectedApiMap.source) { | ||||||
|  |                         throw new Error(`API type ${selectedApiMap.selected} does not support chat completions`); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     const messages = Array.isArray(prompt) ? prompt : [{ role: 'user', content: prompt }]; | ||||||
|  |                     return await context.ChatCompletionService.processRequest({ | ||||||
|  |                         messages, | ||||||
|  |                         max_tokens: maxTokens, | ||||||
|  |                         model: profile.model, | ||||||
|  |                         chat_completion_source: selectedApiMap.source, | ||||||
|  |                     }, { | ||||||
|  |                         presetName: includePreset ? profile.preset : undefined, | ||||||
|  |                     }, extractData); | ||||||
|  |                 } | ||||||
|  |                 case 'textgenerationwebui': { | ||||||
|  |                     if (!selectedApiMap.type) { | ||||||
|  |                         throw new Error(`API type ${selectedApiMap.selected} does not support text completions`); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     return await context.TextCompletionService.processRequest({ | ||||||
|  |                         prompt, | ||||||
|  |                         max_tokens: maxTokens, | ||||||
|  |                         model: profile.model, | ||||||
|  |                         api_type: selectedApiMap.type, | ||||||
|  |                         api_server: profile['api-url'], | ||||||
|  |                     }, { | ||||||
|  |                         instructName: includeInstruct ? profile.instruct : undefined, | ||||||
|  |                         presetName: includePreset ? profile.preset : undefined, | ||||||
|  |                     }, extractData); | ||||||
|  |                 } | ||||||
|  |                 default: { | ||||||
|  |                     throw new Error(`Unknown API type ${selectedApiMap.selected}`); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } catch (error) { | ||||||
|  |             throw new Error('API request failed', { cause: error }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Respects allowed types. | ||||||
|  |      * @returns {import('./connection-manager/index.js').ConnectionProfile[]} | ||||||
|  |      */ | ||||||
|  |     static getSupportedProfiles() { | ||||||
|  |         const context = SillyTavern.getContext(); | ||||||
|  |         if (context.extensionSettings.disabledExtensions.includes('connection-manager')) { | ||||||
|  |             throw new Error('Connection Manager is not available'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const profiles = context.extensionSettings.connectionManager.profiles; | ||||||
|  |         return profiles.filter((p) => this.isProfileSupported(p)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param {import('./connection-manager/index.js').ConnectionProfile?} [profile] | ||||||
|  |      * @returns {boolean} | ||||||
|  |      */ | ||||||
|  |     static isProfileSupported(profile) { | ||||||
|  |         if (!profile) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const apiMap = CONNECT_API_MAP[profile.api]; | ||||||
|  |         if (!Object.hasOwn(this.getAllowedTypes(), apiMap.selected)) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Some providers not need model, like koboldcpp. But I don't want to check by provider. | ||||||
|  |         switch (apiMap.selected) { | ||||||
|  |             case 'openai': | ||||||
|  |                 return !!apiMap.source; | ||||||
|  |             case 'textgenerationwebui': | ||||||
|  |                 return !!apiMap.type; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param {import('./connection-manager/index.js').ConnectionProfile?} [profile] | ||||||
|  |      * @return {import('../../script.js').ConnectAPIMap} | ||||||
|  |      * @throws {Error} | ||||||
|  |      */ | ||||||
|  |     static validateProfile(profile) { | ||||||
|  |         if (!profile) { | ||||||
|  |             throw new Error('Could not find profile.'); | ||||||
|  |         } | ||||||
|  |         if (!profile.api) { | ||||||
|  |             throw new Error('Select a connection profile that has an API'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const context = SillyTavern.getContext(); | ||||||
|  |         const selectedApiMap = context.CONNECT_API_MAP[profile.api]; | ||||||
|  |         if (!selectedApiMap) { | ||||||
|  |             throw new Error(`Unknown API type ${profile.api}`); | ||||||
|  |         } | ||||||
|  |         if (!Object.hasOwn(this.getAllowedTypes(), selectedApiMap.selected)) { | ||||||
|  |             throw new Error(`API type ${selectedApiMap.selected} is not supported. Supported types: ${Object.values(this.getAllowedTypes()).join(', ')}`); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return selectedApiMap; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Create profiles dropdown and updates select element accordingly. Use onChange, onCreate, unUpdate, onDelete callbacks for custom behaviour. e.g updating extension settings. | ||||||
|  |      * @param {string} selector | ||||||
|  |      * @param {string} initialSelectedProfileId | ||||||
|  |      * @param {(profile?: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} onChange - 3 cases. 1- When user selects new profile. 2- When user deletes selected profile. 3- When user updates selected profile. | ||||||
|  |      * @param {(profile: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} onCreate | ||||||
|  |      * @param {(oldProfile: import('./connection-manager/index.js').ConnectionProfile, newProfile: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} unUpdate | ||||||
|  |      * @param {(profile: import('./connection-manager/index.js').ConnectionProfile) => Promise<void> | void} onDelete | ||||||
|  |      */ | ||||||
|  |     static handleDropdown( | ||||||
|  |         selector, | ||||||
|  |         initialSelectedProfileId, | ||||||
|  |         onChange = () => { }, | ||||||
|  |         onCreate = () => { }, | ||||||
|  |         unUpdate = () => { }, | ||||||
|  |         onDelete = () => { }, | ||||||
|  |     ) { | ||||||
|  |         const context = SillyTavern.getContext(); | ||||||
|  |         if (context.extensionSettings.disabledExtensions.includes('connection-manager')) { | ||||||
|  |             throw new Error('Connection Manager is not available'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * @type {JQuery<HTMLSelectElement>} | ||||||
|  |          */ | ||||||
|  |         const dropdown = $(selector); | ||||||
|  |  | ||||||
|  |         if (!dropdown || !dropdown.length) { | ||||||
|  |             throw new Error(`Could not find dropdown with selector ${selector}`); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         dropdown.empty(); | ||||||
|  |  | ||||||
|  |         // Create default option using document.createElement | ||||||
|  |         const defaultOption = document.createElement('option'); | ||||||
|  |         defaultOption.value = ''; | ||||||
|  |         defaultOption.textContent = 'Select a Connection Profile'; | ||||||
|  |         defaultOption.dataset.i18n = 'Select a Connection Profile'; | ||||||
|  |         dropdown.append(defaultOption); | ||||||
|  |  | ||||||
|  |         const profiles = context.extensionSettings.connectionManager.profiles; | ||||||
|  |  | ||||||
|  |         // Create optgroups using document.createElement | ||||||
|  |         const groups = {}; | ||||||
|  |         for (const [apiType, groupLabel] of Object.entries(this.getAllowedTypes())) { | ||||||
|  |             const optgroup = document.createElement('optgroup'); | ||||||
|  |             optgroup.label = groupLabel; | ||||||
|  |             groups[apiType] = optgroup; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const sortedProfilesByGroup = {}; | ||||||
|  |         for (const apiType of Object.keys(this.getAllowedTypes())) { | ||||||
|  |             sortedProfilesByGroup[apiType] = []; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         for (const profile of profiles) { | ||||||
|  |             if (this.isProfileSupported(profile)) { | ||||||
|  |                 const apiMap = CONNECT_API_MAP[profile.api]; | ||||||
|  |                 if (sortedProfilesByGroup[apiMap.selected]) { | ||||||
|  |                     sortedProfilesByGroup[apiMap.selected].push(profile); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Sort each group alphabetically and add to dropdown | ||||||
|  |         for (const [apiType, groupProfiles] of Object.entries(sortedProfilesByGroup)) { | ||||||
|  |             if (groupProfiles.length === 0) continue; | ||||||
|  |  | ||||||
|  |             groupProfiles.sort((a, b) => a.name.localeCompare(b.name)); | ||||||
|  |  | ||||||
|  |             const group = groups[apiType]; | ||||||
|  |             for (const profile of groupProfiles) { | ||||||
|  |                 const option = document.createElement('option'); | ||||||
|  |                 option.value = profile.id; | ||||||
|  |                 option.textContent = profile.name; | ||||||
|  |                 group.appendChild(option); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         for (const group of Object.values(groups)) { | ||||||
|  |             if (group.children.length > 0) { | ||||||
|  |                 dropdown.append(group); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const selectedProfile = profiles.find((p) => p.id === initialSelectedProfileId); | ||||||
|  |         if (selectedProfile) { | ||||||
|  |             dropdown.val(selectedProfile.id); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         context.eventSource.on(context.eventTypes.CONNECTION_PROFILE_CREATED, async (profile) => { | ||||||
|  |             const isSupported = this.isProfileSupported(profile); | ||||||
|  |             if (!isSupported) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const group = groups[CONNECT_API_MAP[profile.api].selected]; | ||||||
|  |             const option = document.createElement('option'); | ||||||
|  |             option.value = profile.id; | ||||||
|  |             option.textContent = profile.name; | ||||||
|  |             group.appendChild(option); | ||||||
|  |  | ||||||
|  |             await onCreate(profile); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         context.eventSource.on(context.eventTypes.CONNECTION_PROFILE_UPDATED, async (oldProfile, newProfile) => { | ||||||
|  |             const currentSelected = dropdown.val(); | ||||||
|  |             const isSelectedProfile = currentSelected === oldProfile.id; | ||||||
|  |             await unUpdate(oldProfile, newProfile); | ||||||
|  |  | ||||||
|  |             if (!this.isProfileSupported(newProfile)) { | ||||||
|  |                 if (isSelectedProfile) { | ||||||
|  |                     dropdown.val(''); | ||||||
|  |                     dropdown.trigger('change'); | ||||||
|  |                 } | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const group = groups[CONNECT_API_MAP[newProfile.api].selected]; | ||||||
|  |             const oldOption = group.querySelector(`option[value="${oldProfile.id}"]`); | ||||||
|  |             if (oldOption) { | ||||||
|  |                 oldOption.remove(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const option = document.createElement('option'); | ||||||
|  |             option.value = newProfile.id; | ||||||
|  |             option.textContent = newProfile.name; | ||||||
|  |             group.appendChild(option); | ||||||
|  |  | ||||||
|  |             if (isSelectedProfile) { | ||||||
|  |                 // Ackchyually, we don't need to reselect but what if id changes? It is not possible for now I couldn't stop myself. | ||||||
|  |                 dropdown.val(newProfile.id); | ||||||
|  |                 dropdown.trigger('change'); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         context.eventSource.on(context.eventTypes.CONNECTION_PROFILE_DELETED, async (profile) => { | ||||||
|  |             const currentSelected = dropdown.val(); | ||||||
|  |             const isSelectedProfile = currentSelected === profile.id; | ||||||
|  |             if (!this.isProfileSupported(profile)) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const group = groups[CONNECT_API_MAP[profile.api].selected]; | ||||||
|  |             const optionToRemove = group.querySelector(`option[value="${profile.id}"]`); | ||||||
|  |             if (optionToRemove) { | ||||||
|  |                 optionToRemove.remove(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (isSelectedProfile) { | ||||||
|  |                 dropdown.val(''); | ||||||
|  |                 dropdown.trigger('change'); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             await onDelete(profile); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         dropdown.on('change', async () => { | ||||||
|  |             const profileId = dropdown.val(); | ||||||
|  |             const profile = context.extensionSettings.connectionManager.profiles.find((p) => p.id === profileId); | ||||||
|  |             await onChange(profile); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -320,59 +320,61 @@ export const force_output_sequence = { | |||||||
|  * @param {string} name1 User name. |  * @param {string} name1 User name. | ||||||
|  * @param {string} name2 Character name. |  * @param {string} name2 Character name. | ||||||
|  * @param {boolean|number} forceOutputSequence Force to use first/last output sequence (if configured). |  * @param {boolean|number} forceOutputSequence Force to use first/last output sequence (if configured). | ||||||
|  |  * @param {InstructSettings} customInstruct Custom instruct mode settings. | ||||||
|  * @returns {string} Formatted instruct mode chat message. |  * @returns {string} Formatted instruct mode chat message. | ||||||
|  */ |  */ | ||||||
| export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2, forceOutputSequence) { | export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2, forceOutputSequence, customInstruct = null) { | ||||||
|     let includeNames = isNarrator ? false : power_user.instruct.names_behavior === names_behavior_types.ALWAYS; |     const instruct = structuredClone(customInstruct ?? power_user.instruct); | ||||||
|  |     let includeNames = isNarrator ? false : instruct.names_behavior === names_behavior_types.ALWAYS; | ||||||
|  |  | ||||||
|     if (!isNarrator && power_user.instruct.names_behavior === names_behavior_types.FORCE && ((selected_group && name !== name1) || (forceAvatar && name !== name1))) { |     if (!isNarrator && instruct.names_behavior === names_behavior_types.FORCE && ((selected_group && name !== name1) || (forceAvatar && name !== name1))) { | ||||||
|         includeNames = true; |         includeNames = true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function getPrefix() { |     function getPrefix() { | ||||||
|         if (isNarrator) { |         if (isNarrator) { | ||||||
|             return power_user.instruct.system_same_as_user ? power_user.instruct.input_sequence : power_user.instruct.system_sequence; |             return instruct.system_same_as_user ? instruct.input_sequence : instruct.system_sequence; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (isUser) { |         if (isUser) { | ||||||
|             if (forceOutputSequence === force_output_sequence.FIRST) { |             if (forceOutputSequence === force_output_sequence.FIRST) { | ||||||
|                 return power_user.instruct.first_input_sequence || power_user.instruct.input_sequence; |                 return instruct.first_input_sequence || instruct.input_sequence; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             if (forceOutputSequence === force_output_sequence.LAST) { |             if (forceOutputSequence === force_output_sequence.LAST) { | ||||||
|                 return power_user.instruct.last_input_sequence || power_user.instruct.input_sequence; |                 return instruct.last_input_sequence || instruct.input_sequence; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             return power_user.instruct.input_sequence; |             return instruct.input_sequence; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (forceOutputSequence === force_output_sequence.FIRST) { |         if (forceOutputSequence === force_output_sequence.FIRST) { | ||||||
|             return power_user.instruct.first_output_sequence || power_user.instruct.output_sequence; |             return instruct.first_output_sequence || instruct.output_sequence; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (forceOutputSequence === force_output_sequence.LAST) { |         if (forceOutputSequence === force_output_sequence.LAST) { | ||||||
|             return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence; |             return instruct.last_output_sequence || instruct.output_sequence; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return power_user.instruct.output_sequence; |         return instruct.output_sequence; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function getSuffix() { |     function getSuffix() { | ||||||
|         if (isNarrator) { |         if (isNarrator) { | ||||||
|             return power_user.instruct.system_same_as_user ? power_user.instruct.input_suffix : power_user.instruct.system_suffix; |             return instruct.system_same_as_user ? instruct.input_suffix : instruct.system_suffix; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (isUser) { |         if (isUser) { | ||||||
|             return power_user.instruct.input_suffix; |             return instruct.input_suffix; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return power_user.instruct.output_suffix; |         return instruct.output_suffix; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let prefix = getPrefix() || ''; |     let prefix = getPrefix() || ''; | ||||||
|     let suffix = getSuffix() || ''; |     let suffix = getSuffix() || ''; | ||||||
|  |  | ||||||
|     if (power_user.instruct.macro) { |     if (instruct.macro) { | ||||||
|         prefix = substituteParams(prefix, name1, name2); |         prefix = substituteParams(prefix, name1, name2); | ||||||
|         prefix = prefix.replace(/{{name}}/gi, name || 'System'); |         prefix = prefix.replace(/{{name}}/gi, name || 'System'); | ||||||
|  |  | ||||||
| @@ -380,11 +382,11 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata | |||||||
|         suffix = suffix.replace(/{{name}}/gi, name || 'System'); |         suffix = suffix.replace(/{{name}}/gi, name || 'System'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!suffix && power_user.instruct.wrap) { |     if (!suffix && instruct.wrap) { | ||||||
|         suffix = '\n'; |         suffix = '\n'; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const separator = power_user.instruct.wrap ? '\n' : ''; |     const separator = instruct.wrap ? '\n' : ''; | ||||||
|  |  | ||||||
|     // Don't include the name if it's empty |     // Don't include the name if it's empty | ||||||
|     const textArray = includeNames && name ? [prefix, `${name}: ${mes}` + suffix] : [prefix, mes + suffix]; |     const textArray = includeNames && name ? [prefix, `${name}: ${mes}` + suffix] : [prefix, mes + suffix]; | ||||||
| @@ -504,30 +506,32 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { | |||||||
|  * @param {string} name2 Character name. |  * @param {string} name2 Character name. | ||||||
|  * @param {boolean} isQuiet Is quiet mode generation. |  * @param {boolean} isQuiet Is quiet mode generation. | ||||||
|  * @param {boolean} isQuietToLoud Is quiet to loud generation. |  * @param {boolean} isQuietToLoud Is quiet to loud generation. | ||||||
|  |  * @param {InstructSettings} customInstruct Custom instruct settings. | ||||||
|  * @returns {string} Formatted instruct mode last prompt line. |  * @returns {string} Formatted instruct mode last prompt line. | ||||||
|  */ |  */ | ||||||
| export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, isQuietToLoud) { | export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, isQuietToLoud, customInstruct = null) { | ||||||
|     const includeNames = name && (power_user.instruct.names_behavior === names_behavior_types.ALWAYS || (!!selected_group && power_user.instruct.names_behavior === names_behavior_types.FORCE)) && !(isQuiet && !isQuietToLoud); |     const instruct = structuredClone(customInstruct ?? power_user.instruct); | ||||||
|  |     const includeNames = name && (instruct.names_behavior === names_behavior_types.ALWAYS || (!!selected_group && instruct.names_behavior === names_behavior_types.FORCE)) && !(isQuiet && !isQuietToLoud); | ||||||
|  |  | ||||||
|     function getSequence() { |     function getSequence() { | ||||||
|         // User impersonation prompt |         // User impersonation prompt | ||||||
|         if (isImpersonate) { |         if (isImpersonate) { | ||||||
|             return power_user.instruct.input_sequence; |             return instruct.input_sequence; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Neutral / system / quiet prompt |         // Neutral / system / quiet prompt | ||||||
|         // Use a special quiet instruct sequence if defined, or assistant's output sequence otherwise |         // Use a special quiet instruct sequence if defined, or assistant's output sequence otherwise | ||||||
|         if (isQuiet && !isQuietToLoud) { |         if (isQuiet && !isQuietToLoud) { | ||||||
|             return power_user.instruct.last_system_sequence || power_user.instruct.output_sequence; |             return instruct.last_system_sequence || instruct.output_sequence; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Quiet in-character prompt |         // Quiet in-character prompt | ||||||
|         if (isQuiet && isQuietToLoud) { |         if (isQuiet && isQuietToLoud) { | ||||||
|             return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence; |             return instruct.last_output_sequence || instruct.output_sequence; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Default AI response |         // Default AI response | ||||||
|         return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence; |         return instruct.last_output_sequence || instruct.output_sequence; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let sequence = getSequence() || ''; |     let sequence = getSequence() || ''; | ||||||
| @@ -536,21 +540,21 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, | |||||||
|     // A hack for Mistral's formatting that has a normal output sequence ending with a space |     // A hack for Mistral's formatting that has a normal output sequence ending with a space | ||||||
|     if ( |     if ( | ||||||
|         includeNames && |         includeNames && | ||||||
|         power_user.instruct.last_output_sequence && |         instruct.last_output_sequence && | ||||||
|         power_user.instruct.output_sequence && |         instruct.output_sequence && | ||||||
|         sequence === power_user.instruct.last_output_sequence && |         sequence === instruct.last_output_sequence && | ||||||
|         /\s$/.test(power_user.instruct.output_sequence) && |         /\s$/.test(instruct.output_sequence) && | ||||||
|         !/\s$/.test(power_user.instruct.last_output_sequence) |         !/\s$/.test(instruct.last_output_sequence) | ||||||
|     ) { |     ) { | ||||||
|         nameFiller = power_user.instruct.output_sequence.slice(-1); |         nameFiller = instruct.output_sequence.slice(-1); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (power_user.instruct.macro) { |     if (instruct.macro) { | ||||||
|         sequence = substituteParams(sequence, name1, name2); |         sequence = substituteParams(sequence, name1, name2); | ||||||
|         sequence = sequence.replace(/{{name}}/gi, name || 'System'); |         sequence = sequence.replace(/{{name}}/gi, name || 'System'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const separator = power_user.instruct.wrap ? '\n' : ''; |     const separator = instruct.wrap ? '\n' : ''; | ||||||
|     let text = includeNames ? (separator + sequence + separator + nameFiller + `${name}:`) : (separator + sequence); |     let text = includeNames ? (separator + sequence + separator + nameFiller + `${name}:`) : (separator + sequence); | ||||||
|  |  | ||||||
|     // Quiet prompt already has a newline at the end |     // Quiet prompt already has a newline at the end | ||||||
| @@ -562,7 +566,7 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, | |||||||
|         text += (includeNames ? promptBias : (separator + promptBias.trimStart())); |         text += (includeNames ? promptBias : (separator + promptBias.trimStart())); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return (power_user.instruct.wrap ? text.trimEnd() : text) + (includeNames ? '' : separator); |     return (instruct.wrap ? text.trimEnd() : text) + (includeNames ? '' : separator); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
| @@ -218,7 +218,9 @@ let power_user = { | |||||||
|         system_sequence: '', |         system_sequence: '', | ||||||
|         system_suffix: '', |         system_suffix: '', | ||||||
|         last_system_sequence: '', |         last_system_sequence: '', | ||||||
|  |         first_input_sequence: '', | ||||||
|         first_output_sequence: '', |         first_output_sequence: '', | ||||||
|  |         last_input_sequence: '', | ||||||
|         last_output_sequence: '', |         last_output_sequence: '', | ||||||
|         system_sequence_prefix: '', |         system_sequence_prefix: '', | ||||||
|         system_sequence_suffix: '', |         system_sequence_suffix: '', | ||||||
|   | |||||||
| @@ -57,19 +57,24 @@ function toggleReasoningAutoExpand() { | |||||||
|  * @param {object} data Response data |  * @param {object} data Response data | ||||||
|  * @returns {string} Extracted reasoning |  * @returns {string} Extracted reasoning | ||||||
|  */ |  */ | ||||||
| export function extractReasoningFromData(data) { | export function extractReasoningFromData(data, { | ||||||
|     switch (main_api) { |     mainApi = null, | ||||||
|  |     ignoreShowThoughts = false, | ||||||
|  |     textGenType = null, | ||||||
|  |     chatCompletionSource = null | ||||||
|  | } = {}) { | ||||||
|  |     switch (mainApi ?? main_api) { | ||||||
|         case 'textgenerationwebui': |         case 'textgenerationwebui': | ||||||
|             switch (textgenerationwebui_settings.type) { |             switch (textGenType ?? textgenerationwebui_settings.type) { | ||||||
|                 case textgen_types.OPENROUTER: |                 case textgen_types.OPENROUTER: | ||||||
|                     return data?.choices?.[0]?.reasoning ?? ''; |                     return data?.choices?.[0]?.reasoning ?? ''; | ||||||
|             } |             } | ||||||
|             break; |             break; | ||||||
|  |  | ||||||
|         case 'openai': |         case 'openai': | ||||||
|             if (!oai_settings.show_thoughts) break; |             if (!ignoreShowThoughts && !oai_settings.show_thoughts) break; | ||||||
|  |  | ||||||
|             switch (oai_settings.chat_completion_source) { |             switch (chatCompletionSource ?? oai_settings.chat_completion_source) { | ||||||
|                 case chat_completion_sources.DEEPSEEK: |                 case chat_completion_sources.DEEPSEEK: | ||||||
|                     return data?.choices?.[0]?.message?.reasoning_content ?? ''; |                     return data?.choices?.[0]?.message?.reasoning_content ?? ''; | ||||||
|                 case chat_completion_sources.OPENROUTER: |                 case chat_completion_sources.OPENROUTER: | ||||||
|   | |||||||
| @@ -80,6 +80,7 @@ import { timestampToMoment, uuidv4 } from './utils.js'; | |||||||
| import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js'; | import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js'; | ||||||
| import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js'; | import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js'; | ||||||
| import { ChatCompletionService, TextCompletionService } from './custom-request.js'; | import { ChatCompletionService, TextCompletionService } from './custom-request.js'; | ||||||
|  | import { ConnectionManagerRequestService } from './extensions/shared.js'; | ||||||
| import { updateReasoningUI, parseReasoningFromString } from './reasoning.js'; | import { updateReasoningUI, parseReasoningFromString } from './reasoning.js'; | ||||||
|  |  | ||||||
| export function getContext() { | export function getContext() { | ||||||
| @@ -215,6 +216,7 @@ export function getContext() { | |||||||
|         clearChat, |         clearChat, | ||||||
|         ChatCompletionService, |         ChatCompletionService, | ||||||
|         TextCompletionService, |         TextCompletionService, | ||||||
|  |         ConnectionManagerRequestService, | ||||||
|         updateReasoningUI, |         updateReasoningUI, | ||||||
|         parseReasoningFromString, |         parseReasoningFromString, | ||||||
|         unshallowCharacter, |         unshallowCharacter, | ||||||
|   | |||||||
| @@ -86,7 +86,7 @@ const OOBA_DEFAULT_ORDER = [ | |||||||
|     'encoder_repetition_penalty', |     'encoder_repetition_penalty', | ||||||
|     'no_repeat_ngram', |     'no_repeat_ngram', | ||||||
| ]; | ]; | ||||||
| const APHRODITE_DEFAULT_ORDER = [ | export const APHRODITE_DEFAULT_ORDER = [ | ||||||
|     'dry', |     'dry', | ||||||
|     'penalties', |     'penalties', | ||||||
|     'no_repeat_ngram', |     'no_repeat_ngram', | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user