Merge pull request #2675 from SillyTavern/api-url-slash-command `/api-url` slash command to get/set the API server url for text gen and OpenAI Custom

b2ddcaa6963e6cc8a826d8b13ceec257f99a87f3

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

Signed
3 files changed, +152 -9Ignore whitespace
public/script.js+5 -5
@@ -8392,6 +8392,9 @@ const CONNECT_API_MAP = {
83928392 },
83938393};
83948394
8395+// Collect all unique API names in an array
8396+export const UNIQUE_APIS = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
8397+
83958398// Fill connections map from textgen_types and chat_completion_sources
83968399for (const textGenType of Object.values(textgen_types)) {
83978400 if (CONNECT_API_MAP[textGenType]) continue;
@@ -8966,9 +8969,6 @@ jQuery(async function () {
89668969 return '';
89678970 }
89688971
8969- // Collect all unique API names in an array
8970- const uniqueAPIs = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
8971-
89728972 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
89738973 name: 'dupe',
89748974 callback: duplicateCharacter,
@@ -8977,13 +8977,13 @@ jQuery(async function () {
89778977 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
89788978 name: 'api',
89798979 callback: connectAPISlash,
8980+ returns: 'the current API',
89808981 unnamedArgumentList: [
89818982 SlashCommandArgument.fromProps({
89828983 description: 'API to connect to',
89838984 typeList: [ARGUMENT_TYPE.STRING],
8984- isRequired: false,
89858985 enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>
89868986 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(uniqueAPIsUNIQUE_APIS.findIndex(x => x === selected)),
89878987 selected[0].toUpperCase() ?? enumIcons.default)),
89888988 }),
89898989 ],
public/scripts/slash-commands.js+146 -3
@@ -1,7 +1,9 @@
11import {
22 Generate,
3+ UNIQUE_APIS,
34 activateSendButtons,
45 addOneMessage,
6+ api_server,
57 callPopup,
68 characters,
79 chat,
@@ -49,7 +51,7 @@ import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSel
4951import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';
5052import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';
5153import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
5254import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
5355import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';
5456import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
5557import { registerVariableCommands, resolveVariable } from './variables.js';
@@ -1496,8 +1498,9 @@ export function initDefaultSlashCommands() {
14961498 ],
14971499 helpString: 'Sets the specified prompt manager entry/entries on or off.',
14981500 }));
14991501 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pick-icon',
1500- callback: async()=>((await showFontAwesomePicker()) ?? false).toString(),
1502+ name: 'pick-icon',
1503+ callback: async () => ((await showFontAwesomePicker()) ?? false).toString(),
15011504 returns: 'The chosen icon name or false if cancelled.',
15021505 helpString: `
15031506 <div>Opens a popup with all the available Font Awesome icons and returns the selected icon's name.</div>
@@ -1511,6 +1514,50 @@ export function initDefaultSlashCommands() {
15111514 </div>
15121515 `,
15131516 }));
1517+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1518+ name: 'api-url',
1519+ callback: setApiUrlCallback,
1520+ returns: 'the current API url',
1521+ aliases: ['server'],
1522+ namedArgumentList: [
1523+ SlashCommandNamedArgument.fromProps({
1524+ name: 'api',
1525+ description: 'API to set/get the URL for - if not provided, current API is used',
1526+ typeList: [ARGUMENT_TYPE.STRING],
1527+ enumList: [
1528+ new SlashCommandEnumValue('custom', 'custom OpenAI-compatible', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'openai')), 'O'),
1529+ new SlashCommandEnumValue('kobold', 'KoboldAI Classic', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'kobold')), 'K'),
1530+ ...Object.values(textgen_types).map(api => new SlashCommandEnumValue(api, null, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'textgenerationwebui')), 'T')),
1531+ ],
1532+ }),
1533+ SlashCommandNamedArgument.fromProps({
1534+ name: 'connect',
1535+ description: 'Whether to auto-connect to the API after setting the URL',
1536+ typeList: [ARGUMENT_TYPE.BOOLEAN],
1537+ defaultValue: 'true',
1538+ enumList: commonEnumProviders.boolean('trueFalse')(),
1539+ }),
1540+ ],
1541+ unnamedArgumentList: [
1542+ SlashCommandArgument.fromProps({
1543+ description: 'API url to connect to',
1544+ typeList: [ARGUMENT_TYPE.STRING],
1545+ }),
1546+ ],
1547+ helpString: `
1548+ <div>
1549+ Set the API url / server url for the currently selected API, including the port. If no argument is provided, it will return the current API url.
1550+ </div>
1551+ <div>
1552+ If a manual API is provided to <b>set</b> the URL, make sure to set <code>connect=false</code>, as auto-connect only works for the currently selected API,
1553+ or consider switching to it with <code>/api</code> first.
1554+ </div>
1555+ <div>
1556+ This slash command works for most of the Text Completion sources, KoboldAI Classic, and also Custom OpenAI compatible for the Chat Completion sources. If unsure which APIs are supported,
1557+ check the auto-completion of the optional <code>api</code> argument of this command.
1558+ </div>
1559+ `,
1560+ }));
15141561
15151562 registerVariableCommands();
15161563}
@@ -3418,6 +3465,102 @@ function setPromptEntryCallback(args, targetState) {
34183465 return '';
34193466}
34203467
3468+/**
3469+ * Sets the API URL and triggers the text generation web UI button click.
3470+ *
3471+ * @param {object} args - named args
3472+ * @param {string?} [args.api=null] - the API name to set/get the URL for
3473+ * @param {string?} [args.connect=true] - whether to connect to the API after setting
3474+ * @param {string} url - the API URL to set
3475+ * @returns {Promise<string>}
3476+ */
3477+async function setApiUrlCallback({ api = null, connect = 'true' }, url) {
3478+ const autoConnect = isTrueBoolean(connect);
3479+
3480+ // Special handling for Chat Completion Custom OpenAI compatible, that one can also support API url handling
3481+ const isCurrentlyCustomOpenai = main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.CUSTOM;
3482+ if (api === chat_completion_sources.CUSTOM || (!api && isCurrentlyCustomOpenai)) {
3483+ if (!url) {
3484+ return oai_settings.custom_url ?? '';
3485+ }
3486+
3487+ if (!isCurrentlyCustomOpenai && autoConnect) {
3488+ toastr.warning('Custom OpenAI API is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.');
3489+ return '';
3490+ }
3491+
3492+ $('#custom_api_url_text').val(url).trigger('input');
3493+
3494+ if (autoConnect) {
3495+ $('#api_button_openai').trigger('click');
3496+ }
3497+
3498+ return url;
3499+ }
3500+
3501+ // Special handling for Kobold Classic API
3502+ const isCurrentlyKoboldClassic = main_api === 'kobold';
3503+ if (api === 'kobold' || (!api && isCurrentlyKoboldClassic)) {
3504+ if (!url) {
3505+ return api_server ?? '';
3506+ }
3507+
3508+ if (!isCurrentlyKoboldClassic && autoConnect) {
3509+ toastr.warning('Kobold Classic API is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.');
3510+ return '';
3511+ }
3512+
3513+ $('#api_url_text').val(url).trigger('input');
3514+ // trigger blur debounced, so we hide the autocomplete menu
3515+ setTimeout(() => $('#api_url_text').trigger('blur'), 1);
3516+
3517+ if (autoConnect) {
3518+ $('#api_button').trigger('click');
3519+ }
3520+
3521+ return api_server ?? '';
3522+ }
3523+
3524+ // Do some checks and get the api type we are targeting with this command
3525+ if (api && !Object.values(textgen_types).includes(api)) {
3526+ toastr.warning(`API '${api}' is not a valid text_gen API.`);
3527+ return '';
3528+ }
3529+ if (!api && !Object.values(textgen_types).includes(textgenerationwebui_settings.type)) {
3530+ toastr.warning(`API '${textgenerationwebui_settings.type}' is not a valid text_gen API.`);
3531+ return '';
3532+ }
3533+ if (api && url && autoConnect && api !== textgenerationwebui_settings.type) {
3534+ toastr.warning(`API '${api}' is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.`);
3535+ return '';
3536+ }
3537+ const type = api || textgenerationwebui_settings.type;
3538+
3539+ const inputSelector = SERVER_INPUTS[type];
3540+ if (!inputSelector) {
3541+ toastr.warning(`API '${type}' does not have a server url input.`);
3542+ return '';
3543+ }
3544+
3545+ // If no url was provided, return the current one
3546+ if (!url) {
3547+ return textgenerationwebui_settings.server_urls[type] ?? '';
3548+ }
3549+
3550+ // else, we want to actually set the url
3551+ $(inputSelector).val(url).trigger('input');
3552+ // trigger blur debounced, so we hide the autocomplete menu
3553+ setTimeout(() => $(inputSelector).trigger('blur'), 1);
3554+
3555+ // Trigger the auto connect via connect button, if requested
3556+ if (autoConnect) {
3557+ $('#api_button_textgenerationwebui').trigger('click');
3558+ }
3559+
3560+ // We still re-acquire the value, as it might have been modified by the validation on connect
3561+ return textgenerationwebui_settings.server_urls[type] ?? '';
3562+}
3563+
34213564export let isExecutingCommandsFromChatInput = false;
34223565export let commandsFromChatInputAbortController;
34233566
public/scripts/textgen-settings.js+1 -1
@@ -94,7 +94,7 @@ let DREAMGEN_SERVER = 'https://dreamgen.com';
9494let OPENROUTER_SERVER = 'https://openrouter.ai/api';
9595let FEATHERLESS_SERVER = 'https://api.featherless.ai/v1';
9696
9797export const SERVER_INPUTS = {
9898 [textgen_types.OOBA]: '#textgenerationwebui_api_url_text',
9999 [textgen_types.VLLM]: '#vllm_api_url_text',
100100 [textgen_types.APHRODITE]: '#aphrodite_api_url_text',