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:
@ -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 { t } from '../i18n.js';
|
||||
import { oai_settings } from '../openai.js';
|
||||
import { SECRET_KEYS, secret_state } from '../secrets.js';
|
||||
import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
|
||||
@ -273,3 +274,309 @@ export async function getWebLlmContextSize() {
|
||||
const model = await engine.getCurrentModelInfo();
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user