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 = {
8392 },8392 },
8393};8393};
83948394
8395// Collect all unique API names in an array
8396export const UNIQUE_APIS = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
8397
8395// Fill connections map from textgen_types and chat_completion_sources8398// Fill connections map from textgen_types and chat_completion_sources
8396for (const textGenType of Object.values(textgen_types)) {8399for (const textGenType of Object.values(textgen_types)) {
8397 if (CONNECT_API_MAP[textGenType]) continue;8400 if (CONNECT_API_MAP[textGenType]) continue;
@@ -8966,9 +8969,6 @@ jQuery(async function () {
8966 return '';8969 return '';
8967 }8970 }
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
8972 SlashCommandParser.addCommandObject(SlashCommand.fromProps({8972 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
8973 name: 'dupe',8973 name: 'dupe',
8974 callback: duplicateCharacter,8974 callback: duplicateCharacter,
@@ -8977,13 +8977,13 @@ jQuery(async function () {
8977 SlashCommandParser.addCommandObject(SlashCommand.fromProps({8977 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
8978 name: 'api',8978 name: 'api',
8979 callback: connectAPISlash,8979 callback: connectAPISlash,
8980 returns: 'the current API',
8980 unnamedArgumentList: [8981 unnamedArgumentList: [
8981 SlashCommandArgument.fromProps({8982 SlashCommandArgument.fromProps({
8982 description: 'API to connect to',8983 description: 'API to connect to',
8983 typeList: [ARGUMENT_TYPE.STRING],8984 typeList: [ARGUMENT_TYPE.STRING],
8984 isRequired: false,
8985 enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>8985 enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>
8986 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(uniqueAPIs.findIndex(x => x === selected)),8986 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === selected)),
8987 selected[0].toUpperCase() ?? enumIcons.default)),8987 selected[0].toUpperCase() ?? enumIcons.default)),
8988 }),8988 }),
8989 ],8989 ],
public/scripts/slash-commands.js+146 -3
@@ -1,7 +1,9 @@
1import {1import {
2 Generate,2 Generate,
3 UNIQUE_APIS,
3 activateSendButtons,4 activateSendButtons,
4 addOneMessage,5 addOneMessage,
6 api_server,
5 callPopup,7 callPopup,
6 characters,8 characters,
7 chat,9 chat,
@@ -49,7 +51,7 @@ import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSel
49import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';51import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';
50import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';52import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';
51import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';53import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
52import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';54import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
53import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';55import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';
54import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';56import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
55import { registerVariableCommands, resolveVariable } from './variables.js';57import { registerVariableCommands, resolveVariable } from './variables.js';
@@ -1496,8 +1498,9 @@ export function initDefaultSlashCommands() {
1496 ],1498 ],
1497 helpString: 'Sets the specified prompt manager entry/entries on or off.',1499 helpString: 'Sets the specified prompt manager entry/entries on or off.',
1498 }));1500 }));
1499 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pick-icon',1501 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1500 callback: async()=>((await showFontAwesomePicker()) ?? false).toString(),1502 name: 'pick-icon',
1503 callback: async () => ((await showFontAwesomePicker()) ?? false).toString(),
1501 returns: 'The chosen icon name or false if cancelled.',1504 returns: 'The chosen icon name or false if cancelled.',
1502 helpString: `1505 helpString: `
1503 <div>Opens a popup with all the available Font Awesome icons and returns the selected icon's name.</div>1506 <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() {
1511 </div>1514 </div>
1512 `,1515 `,
1513 }));1516 }));
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
1515 registerVariableCommands();1562 registerVariableCommands();
1516}1563}
@@ -3418,6 +3465,102 @@ function setPromptEntryCallback(args, targetState) {
3418 return '';3465 return '';
3419}3466}
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 */
3477async 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
3421export let isExecutingCommandsFromChatInput = false;3564export let isExecutingCommandsFromChatInput = false;
3422export let commandsFromChatInputAbortController;3565export let commandsFromChatInputAbortController;
34233566
public/scripts/textgen-settings.js+1 -1
@@ -94,7 +94,7 @@ let DREAMGEN_SERVER = 'https://dreamgen.com';
94let OPENROUTER_SERVER = 'https://openrouter.ai/api';94let OPENROUTER_SERVER = 'https://openrouter.ai/api';
95let FEATHERLESS_SERVER = 'https://api.featherless.ai/v1';95let FEATHERLESS_SERVER = 'https://api.featherless.ai/v1';
9696
97const SERVER_INPUTS = {97export const SERVER_INPUTS = {
98 [textgen_types.OOBA]: '#textgenerationwebui_api_url_text',98 [textgen_types.OOBA]: '#textgenerationwebui_api_url_text',
99 [textgen_types.VLLM]: '#vllm_api_url_text',99 [textgen_types.VLLM]: '#vllm_api_url_text',
100 [textgen_types.APHRODITE]: '#aphrodite_api_url_text',100 [textgen_types.APHRODITE]: '#aphrodite_api_url_text',