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