/* * CODE FOR OPENAI SUPPORT * By CncAnon (@CncAnon1) * https://github.com/CncAnon1/TavernAITurbo */ import { Fuse, DOMPurify } from '../lib.js'; import { abortStatusCheck, cancelStatusCheck, characters, event_types, eventSource, extension_prompt_roles, extension_prompt_types, Generate, getExtensionPrompt, getExtensionPromptMaxDepth, getMediaDisplay, getMediaIndex, getRequestHeaders, is_send_press, main_api, name1, name2, resultCheckStatus, saveSettingsDebounced, setOnlineStatus, startStatusLoading, substituteParams, substituteParamsExtended, system_message_types, this_chid, } from '../script.js'; import { getGroupNames, selected_group } from './group-chats.js'; import { chatCompletionDefaultPrompts, INJECTION_POSITION, Prompt, PromptManager, promptManagerDefaultPromptOrders, } from './PromptManager.js'; import { forceCharacterEditorTokenize, getCustomStoppingStrings, persona_description_positions, power_user } from './power-user.js'; import { SECRET_KEYS, secret_state, writeSecret } from './secrets.js'; import { getEventSourceStream } from './sse-stream.js'; import { clamp, createThumbnail, delay, download, getAudioDurationFromDataURL, getBase64Async, getFileText, getImageSizeFromDataURL, getSortableDelay, getStringHash, getVideoDurationFromDataURL, isDataURL, isUuid, isValidUrl, parseJsonFile, resetScrollHeight, stringFormat, textValueMatcher, uuidv4, } from './utils.js'; import { countTokensOpenAIAsync, getTokenizerModel } from './tokenizers.js'; import { isMobile } from './RossAscends-mods.js'; import { saveLogprobsForActiveMessage } from './logprobs.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; import { renderTemplateAsync } from './templates.js'; import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; import { t } from './i18n.js'; import { ToolManager } from './tool-calling.js'; import { accountStorage } from './util/AccountStorage.js'; import { COMETAPI_IGNORE_PATTERNS, IGNORE_SYMBOL, MEDIA_DISPLAY, MEDIA_TYPE } from './constants.js'; import { syncNanoGptProvidersForModel, syncOpenRouterProvidersForModel, updateNanoGptProvidersWarning, updateOpenRouterProvidersWarning } from './textgen-models.js'; export { openai_messages_count, oai_settings, loadOpenAISettings, setOpenAIMessages, setOpenAIMessageExamples, setupChatCompletionPromptManager, sendOpenAIRequest, TokenHandler, IdentifierNotFoundError, Message, MessageCollection, }; let openai_messages_count = 0; const default_main_prompt = 'Write {{char}}\'s next reply in a fictional chat between {{charIfNotGroup}} and {{user}}.'; const default_nsfw_prompt = ''; const default_jailbreak_prompt = ''; const default_impersonation_prompt = '[Write your next reply from the point of view of {{user}}, using the chat history so far as a guideline for the writing style of {{user}}. Don\'t write as {{char}} or system. Don\'t describe actions of {{char}}.]'; const default_enhance_definitions_prompt = 'If you have more knowledge of {{char}}, add to the character\'s lore and personality to enhance them but keep the Character Sheet\'s definitions absolute.'; const default_wi_format = '{0}'; const default_new_chat_prompt = '[Start a new Chat]'; const default_new_group_chat_prompt = '[Start a new group chat. Group members: {{group}}]'; const default_new_example_chat_prompt = '[Example Chat]'; const default_continue_nudge_prompt = '[Continue your last message without repeating its original content.]'; const default_bias = 'Default (none)'; const default_personality_format = '{{personality}}'; const default_scenario_format = '{{scenario}}'; const default_group_nudge_prompt = '[Write the next reply only as {{char}}.]'; const default_bias_presets = { [default_bias]: [], 'Anti-bond': [ { id: '22154f79-dd98-41bc-8e34-87015d6a0eaf', text: ' bond', value: -50 }, { id: '8ad2d5c4-d8ef-49e4-bc5e-13e7f4690e0f', text: ' future', value: -50 }, { id: '52a4b280-0956-4940-ac52-4111f83e4046', text: ' bonding', value: -50 }, { id: 'e63037c7-c9d1-4724-ab2d-7756008b433b', text: ' connection', value: -25 }, ], }; const max_2k = 2047; const max_4k = 4095; const max_8k = 8191; const max_16k = 16383; const max_32k = 32767; const max_64k = 65535; const max_128k = 128 * 1000; const max_200k = 200 * 1000; const max_256k = 256 * 1000; const max_400k = 400 * 1000; const max_1mil = 1000 * 1000; const max_2mil = 2000 * 1000; const unlocked_max = max_2mil; const oai_max_temp = 2.0; const claude_max_temp = 1.0; const mistral_max_temp = 1.5; const openrouter_website_model = 'OR_Website'; const openai_max_stop_strings = 4; const textCompletionModels = [ 'gpt-3.5-turbo-instruct', 'gpt-3.5-turbo-instruct-0914', 'text-davinci-003', 'text-davinci-002', 'text-davinci-001', 'text-curie-001', 'text-babbage-001', 'text-ada-001', 'code-davinci-002', 'code-davinci-001', 'code-cushman-002', 'code-cushman-001', 'text-davinci-edit-001', 'code-davinci-edit-001', 'text-embedding-ada-002', 'text-similarity-davinci-001', 'text-similarity-curie-001', 'text-similarity-babbage-001', 'text-similarity-ada-001', 'text-search-davinci-doc-001', 'text-search-curie-doc-001', 'text-search-babbage-doc-001', 'text-search-ada-doc-001', 'code-search-babbage-code-001', 'code-search-ada-code-001', ]; let biasCache = undefined; export let model_list = []; export const chat_completion_sources = { OPENAI: 'openai', CLAUDE: 'claude', OPENROUTER: 'openrouter', AI21: 'ai21', MAKERSUITE: 'makersuite', VERTEXAI: 'vertexai', MISTRALAI: 'mistralai', CUSTOM: 'custom', COHERE: 'cohere', PERPLEXITY: 'perplexity', GROQ: 'groq', ELECTRONHUB: 'electronhub', CHUTES: 'chutes', NANOGPT: 'nanogpt', DEEPSEEK: 'deepseek', AIMLAPI: 'aimlapi', XAI: 'xai', POLLINATIONS: 'pollinations', MOONSHOT: 'moonshot', FIREWORKS: 'fireworks', COMETAPI: 'cometapi', AZURE_OPENAI: 'azure_openai', ZAI: 'zai', SILICONFLOW: 'siliconflow', WORKERS_AI: 'workers_ai', MINIMAX: 'minimax', }; const character_names_behavior = { NONE: -1, DEFAULT: 0, COMPLETION: 1, CONTENT: 2, }; const continue_postfix_types = { NONE: '', SPACE: ' ', NEWLINE: '\n', DOUBLE_NEWLINE: '\n\n', }; export const custom_prompt_post_processing_types = { NONE: '', /** @deprecated Use MERGE instead. */ CLAUDE: 'claude', MERGE: 'merge', MERGE_TOOLS: 'merge_tools', SEMI: 'semi', SEMI_TOOLS: 'semi_tools', STRICT: 'strict', STRICT_TOOLS: 'strict_tools', SINGLE: 'single', }; const openrouter_middleout_types = { AUTO: 'auto', ON: 'on', OFF: 'off', }; export const reasoning_effort_types = { auto: 'auto', low: 'low', medium: 'medium', high: 'high', min: 'min', max: 'max', }; export const verbosity_levels = { auto: 'auto', low: 'low', medium: 'medium', high: 'high', }; export const tool_reasoning_modes = { DISABLED: 'disabled', SINCE_LAST_USER: 'since_last_user', ACTIVE_CHAIN: 'active_chain', }; // Providers that support interleaved reasoning forwarding in tool-call chains. const interleaved_reasoning_providers = [ chat_completion_sources.OPENROUTER, chat_completion_sources.CUSTOM, ]; export const ZAI_ENDPOINT = { COMMON: 'common', CODING: 'coding', }; export const SILICONFLOW_ENDPOINT = { GLOBAL: 'global', CN: 'cn', }; export const MINIMAX_ENDPOINT = { GLOBAL: 'global', CN: 'cn', }; const sensitiveFields = [ 'reverse_proxy', 'proxy_password', 'custom_url', 'custom_include_body', 'custom_exclude_body', 'custom_include_headers', 'vertexai_region', 'vertexai_express_project_id', 'azure_base_url', 'azure_deployment_name', 'workers_ai_account_id', ]; /** * preset_name -> [selector, setting_name, is_checkbox, is_connection] * @type {Record} */ export const settingsToUpdate = { chat_completion_source: ['#chat_completion_source', 'chat_completion_source', false, true], temperature: ['#temp_openai', 'temp_openai', false, false], frequency_penalty: ['#freq_pen_openai', 'freq_pen_openai', false, false], presence_penalty: ['#pres_pen_openai', 'pres_pen_openai', false, false], top_p: ['#top_p_openai', 'top_p_openai', false, false], top_k: ['#top_k_openai', 'top_k_openai', false, false], top_a: ['#top_a_openai', 'top_a_openai', false, false], min_p: ['#min_p_openai', 'min_p_openai', false, false], repetition_penalty: ['#repetition_penalty_openai', 'repetition_penalty_openai', false, false], max_context_unlocked: ['#oai_max_context_unlocked', 'max_context_unlocked', true, false], group_models: ['#cc_group_models', 'group_models', true, true], sort_models: ['#cc_sort_models', 'sort_models', false, true], openai_model: ['#model_openai_select', 'openai_model', false, true], claude_model: ['#model_claude_select', 'claude_model', false, true], openrouter_model: ['#model_openrouter_select', 'openrouter_model', false, true], openrouter_use_fallback: ['#openrouter_use_fallback', 'openrouter_use_fallback', true, true], openrouter_providers: ['#openrouter_providers_chat', 'openrouter_providers', false, true], openrouter_quantizations: ['#openrouter_quantizations_chat', 'openrouter_quantizations', false, true], openrouter_allow_fallbacks: ['#openrouter_allow_fallbacks', 'openrouter_allow_fallbacks', true, true], openrouter_middleout: ['#openrouter_middleout', 'openrouter_middleout', false, true], tool_reasoning_mode: ['#tool_reasoning_mode', 'tool_reasoning_mode', false, false], ai21_model: ['#model_ai21_select', 'ai21_model', false, true], mistralai_model: ['#model_mistralai_select', 'mistralai_model', false, true], cohere_model: ['#model_cohere_select', 'cohere_model', false, true], perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true], groq_model: ['#model_groq_select', 'groq_model', false, true], chutes_model: ['#model_chutes_select', 'chutes_model', false, true], siliconflow_model: ['#model_siliconflow_select', 'siliconflow_model', false, true], siliconflow_endpoint: ['#siliconflow_endpoint', 'siliconflow_endpoint', false, true], minimax_model: ['#model_minimax_select', 'minimax_model', false, true], minimax_endpoint: ['#minimax_endpoint', 'minimax_endpoint', false, true], electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true], nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true], nanogpt_provider: ['#nanogpt_provider', 'nanogpt_provider', false, true], nanogpt_payg_override: ['#nanogpt_payg_override', 'nanogpt_payg_override', true, true], deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true], aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true], xai_model: ['#model_xai_select', 'xai_model', false, true], pollinations_model: ['#model_pollinations_select', 'pollinations_model', false, true], moonshot_model: ['#model_moonshot_select', 'moonshot_model', false, true], fireworks_model: ['#model_fireworks_select', 'fireworks_model', false, true], cometapi_model: ['#model_cometapi_select', 'cometapi_model', false, true], custom_model: ['#custom_model_id', 'custom_model', false, true], custom_url: ['#custom_api_url_text', 'custom_url', false, true], custom_include_body: ['#custom_include_body', 'custom_include_body', false, true], custom_exclude_body: ['#custom_exclude_body', 'custom_exclude_body', false, true], custom_include_headers: ['#custom_include_headers', 'custom_include_headers', false, true], custom_prompt_post_processing: ['#custom_prompt_post_processing', 'custom_prompt_post_processing', false, true], google_model: ['#model_google_select', 'google_model', false, true], vertexai_model: ['#model_vertexai_select', 'vertexai_model', false, true], zai_model: ['#model_zai_select', 'zai_model', false, true], zai_endpoint: ['#zai_endpoint', 'zai_endpoint', false, true], workers_ai_model: ['#model_workers_ai_select', 'workers_ai_model', false, true], workers_ai_account_id: ['#workers_ai_account_id', 'workers_ai_account_id', false, true], openai_max_context: ['#openai_max_context', 'openai_max_context', false, false], openai_max_tokens: ['#openai_max_tokens', 'openai_max_tokens', false, false], names_behavior: ['#names_behavior', 'names_behavior', false, false], send_if_empty: ['#send_if_empty_textarea', 'send_if_empty', false, false], impersonation_prompt: ['#impersonation_prompt_textarea', 'impersonation_prompt', false, false], new_chat_prompt: ['#newchat_prompt_textarea', 'new_chat_prompt', false, false], new_group_chat_prompt: ['#newgroupchat_prompt_textarea', 'new_group_chat_prompt', false, false], new_example_chat_prompt: ['#newexamplechat_prompt_textarea', 'new_example_chat_prompt', false, false], continue_nudge_prompt: ['#continue_nudge_prompt_textarea', 'continue_nudge_prompt', false, false], bias_preset_selected: ['#openai_logit_bias_preset', 'bias_preset_selected', false, false], reverse_proxy: ['#openai_reverse_proxy', 'reverse_proxy', false, true], wi_format: ['#wi_format_textarea', 'wi_format', false, false], scenario_format: ['#scenario_format_textarea', 'scenario_format', false, false], personality_format: ['#personality_format_textarea', 'personality_format', false, false], group_nudge_prompt: ['#group_nudge_prompt_textarea', 'group_nudge_prompt', false, false], stream_openai: ['#stream_toggle', 'stream_openai', true, false], prompts: ['', 'prompts', false, false], prompt_order: ['', 'prompt_order', false, false], show_external_models: ['#openai_show_external_models', 'show_external_models', true, true], proxy_password: ['#openai_proxy_password', 'proxy_password', false, true], assistant_prefill: ['#claude_assistant_prefill', 'assistant_prefill', false, false], assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false, false], use_sysprompt: ['#use_sysprompt', 'use_sysprompt', true, false], vertexai_auth_mode: ['#vertexai_auth_mode', 'vertexai_auth_mode', false, true], vertexai_region: ['#vertexai_region', 'vertexai_region', false, true], vertexai_express_project_id: ['#vertexai_express_project_id', 'vertexai_express_project_id', false, true], squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true, false], media_inlining: ['#openai_media_inlining', 'media_inlining', true, false], inline_image_quality: ['#openai_inline_image_quality', 'inline_image_quality', false, false], continue_prefill: ['#continue_prefill', 'continue_prefill', true, false], continue_postfix: ['#continue_postfix', 'continue_postfix', false, false], function_calling: ['#openai_function_calling', 'function_calling', true, false], tool_call_recurse_limit: ['#tool_call_recurse_limit', 'tool_call_recurse_limit', false, false], show_thoughts: ['#openai_show_thoughts', 'show_thoughts', true, false], reasoning_effort: ['#openai_reasoning_effort', 'reasoning_effort', false, false], verbosity: ['#openai_verbosity', 'verbosity', false, false], enable_web_search: ['#openai_enable_web_search', 'enable_web_search', true, false], seed: ['#seed_openai', 'seed', false, false], n: ['#n_openai', 'n', false, false], bypass_status_check: ['#openai_bypass_status_check', 'bypass_status_check', true, true], request_images: ['#openai_request_images', 'request_images', true, false], request_image_aspect_ratio: ['#request_image_aspect_ratio', 'request_image_aspect_ratio', false, false], request_image_resolution: ['#request_image_resolution', 'request_image_resolution', false, false], azure_base_url: ['#azure_base_url', 'azure_base_url', false, true], azure_deployment_name: ['#azure_deployment_name', 'azure_deployment_name', false, true], azure_api_version: ['#azure_api_version', 'azure_api_version', false, true], azure_openai_model: ['#azure_openai_model', 'azure_openai_model', false, true], extensions: ['#NULL_SELECTOR', 'extensions', false, false], }; const default_settings = { preset_settings_openai: 'Default', temp_openai: 1.0, freq_pen_openai: 0, pres_pen_openai: 0, top_p_openai: 1.0, top_k_openai: 0, min_p_openai: 0, top_a_openai: 0, repetition_penalty_openai: 1, stream_openai: false, openai_max_context: max_4k, openai_max_tokens: 300, ...chatCompletionDefaultPrompts, ...promptManagerDefaultPromptOrders, send_if_empty: '', impersonation_prompt: default_impersonation_prompt, new_chat_prompt: default_new_chat_prompt, new_group_chat_prompt: default_new_group_chat_prompt, new_example_chat_prompt: default_new_example_chat_prompt, continue_nudge_prompt: default_continue_nudge_prompt, bias_preset_selected: default_bias, bias_presets: default_bias_presets, wi_format: default_wi_format, group_nudge_prompt: default_group_nudge_prompt, scenario_format: default_scenario_format, personality_format: default_personality_format, sort_models: 'alphabetically', group_models: false, openai_model: 'gpt-4-turbo', claude_model: 'claude-sonnet-4-5', google_model: 'gemini-2.5-pro', vertexai_model: 'gemini-2.5-pro', ai21_model: 'jamba-large', mistralai_model: 'mistral-large-latest', cohere_model: 'command-r-plus', perplexity_model: 'sonar-pro', groq_model: 'llama-3.3-70b-versatile', chutes_model: 'deepseek-ai/DeepSeek-V3-0324', siliconflow_model: 'deepseek-ai/DeepSeek-V3', siliconflow_endpoint: SILICONFLOW_ENDPOINT.GLOBAL, minimax_model: 'MiniMax-M2.7', minimax_endpoint: MINIMAX_ENDPOINT.GLOBAL, electronhub_model: 'gpt-4o-mini', nanogpt_model: 'gpt-4o-mini', nanogpt_provider: '', nanogpt_payg_override: false, deepseek_model: 'deepseek-v4-flash', aimlapi_model: 'chatgpt-4o-latest', xai_model: 'grok-3-beta', pollinations_model: 'openai', cometapi_model: 'gpt-4o', moonshot_model: 'kimi-latest', fireworks_model: 'accounts/fireworks/models/kimi-k2-instruct', zai_model: 'glm-4.6', zai_endpoint: ZAI_ENDPOINT.COMMON, workers_ai_model: '@cf/meta/llama-3.3-70b-instruct-fp8-fast', workers_ai_account_id: '', azure_base_url: '', azure_deployment_name: '', azure_api_version: '2024-02-15-preview', azure_openai_model: '', custom_model: '', custom_url: '', custom_include_body: '', custom_exclude_body: '', custom_include_headers: '', openrouter_model: openrouter_website_model, openrouter_use_fallback: false, openrouter_providers: [], openrouter_quantizations: [], openrouter_allow_fallbacks: true, openrouter_middleout: openrouter_middleout_types.ON, tool_reasoning_mode: tool_reasoning_modes.DISABLED, reverse_proxy: '', chat_completion_source: chat_completion_sources.OPENAI, max_context_unlocked: false, show_external_models: false, proxy_password: '', assistant_prefill: '', assistant_impersonation: '', use_sysprompt: false, vertexai_auth_mode: 'express', vertexai_region: 'us-central1', vertexai_express_project_id: '', squash_system_messages: false, media_inlining: true, inline_image_quality: 'auto', bypass_status_check: false, continue_prefill: false, function_calling: false, tool_call_recurse_limit: 5, names_behavior: character_names_behavior.DEFAULT, continue_postfix: continue_postfix_types.SPACE, custom_prompt_post_processing: custom_prompt_post_processing_types.NONE, show_thoughts: true, reasoning_effort: reasoning_effort_types.auto, verbosity: verbosity_levels.auto, enable_web_search: false, request_images: false, request_image_aspect_ratio: '', request_image_resolution: '', seed: -1, n: 1, bind_preset_to_connection: true, extensions: {}, }; const oai_settings = structuredClone(default_settings); export let proxies = [ { name: 'None', url: '', password: '', }, ]; export let selected_proxy = proxies[0]; export let openai_setting_names; export let openai_settings; /** @type {import('./PromptManager.js').PromptManager} */ export let promptManager = null; async function validateReverseProxy() { if (!oai_settings.reverse_proxy) { return; } try { new URL(oai_settings.reverse_proxy); } catch (err) { toastr.error(t`Entered reverse proxy address is not a valid URL`); setOnlineStatus('no_connection'); resultCheckStatus(); throw err; } const rememberKey = `Proxy_SkipConfirm_${getStringHash(oai_settings.reverse_proxy)}`; const skipConfirm = accountStorage.getItem(rememberKey) === 'true'; const confirmation = skipConfirm || await Popup.show.confirm(t`Connecting To Proxy`, await renderTemplateAsync('proxyConnectionWarning', { proxyURL: DOMPurify.sanitize(oai_settings.reverse_proxy) })); if (!confirmation) { toastr.error(t`Update or remove your reverse proxy settings.`); setOnlineStatus('no_connection'); resultCheckStatus(); throw new Error('Proxy connection denied.'); } accountStorage.setItem(rememberKey, String(true)); } /** * Formats chat messages into chat completion messages. * @param {ChatMessage[]} chat - Array containing all messages. * @returns {object[]} - Array containing all messages formatted for chat completion. */ function setOpenAIMessages(chat) { let j = 0; // clean openai msgs const messages = []; // Get current API and model for thought signature validation const currentApi = oai_settings.chat_completion_source; const currentModel = getChatCompletionModel(); for (let i = chat.length - 1; i >= 0; i--) { let role = chat[j].is_user ? 'user' : 'assistant'; let content = chat[j].mes; // If this symbol flag is set, completely ignore the message. // This can be used to hide messages without affecting the number of messages in the chat. if (chat[j].extra?.[IGNORE_SYMBOL]) { j++; continue; } // 100% legal way to send a message as system if (chat[j].extra?.type === system_message_types.NARRATOR) { role = 'system'; } // for groups or sendas command - prepend a character's name switch (oai_settings.names_behavior) { case character_names_behavior.NONE: break; case character_names_behavior.DEFAULT: if ((selected_group && chat[j].name !== name1) || (chat[j].force_avatar && chat[j].name !== name1 && chat[j].extra?.type !== system_message_types.NARRATOR)) { content = `${chat[j].name}: ${content}`; } break; case character_names_behavior.CONTENT: if (chat[j].extra?.type !== system_message_types.NARRATOR) { content = `${chat[j].name}: ${content}`; } break; case character_names_behavior.COMPLETION: break; default: break; } // remove caret return (waste of tokens) content = content.replace(/\r/gm, ''); const name = chat[j].name; const media = chat[j]?.extra?.media; const mediaDisplay = getMediaDisplay(chat[j]); const mediaIndex = getMediaIndex(chat[j]); const invocations = chat[j]?.extra?.tool_invocations?.slice(); // Only send thought signatures if they were generated by the same API and model const originApi = chat[j]?.extra?.api; const originModel = chat[j]?.extra?.model; const isSameModel = originApi === currentApi && originModel === currentModel; // In group chats, only include reasoning from the currently generating character const isOtherGroupMember = selected_group && chat[j].name !== name2; const signature = isSameModel && !isOtherGroupMember ? chat[j]?.extra?.reasoning_signature : null; const reasoning = isSameModel && !isOtherGroupMember ? String(chat[j]?.extra?.reasoning ?? '') : ''; // Remove reasoning metadata from invocations if the API/model don't match if (Array.isArray(invocations) && invocations.length > 0) { invocations.forEach((invocation, index) => { if (!isSameModel && (invocation.signature || invocation.reasoning)) { const cloneInvocation = structuredClone(invocation); delete cloneInvocation.signature; delete cloneInvocation.reasoning; invocations[index] = cloneInvocation; } }); } messages[i] = { 'role': role, 'content': content, name: name, 'media': media, 'mediaDisplay': mediaDisplay, 'mediaIndex': mediaIndex, 'invocations': invocations, 'signature': signature, 'reasoning': reasoning }; j++; } return messages; } /** * Formats chat examples into chat completion messages. * @param {string[]} mesExamplesArray - Array containing all examples. * @returns {object[]} - Array containing all examples formatted for chat completion. */ function setOpenAIMessageExamples(mesExamplesArray) { // get a nice array of all blocks of all example messages = array of arrays (important!) const examples = []; for (let item of mesExamplesArray) { // remove {Example Dialogue:} and replace \r\n with just \n let replaced = item.replace(//i, '{Example Dialogue:}').replace(/\r/gm, ''); let parsed = parseExampleIntoIndividual(replaced, true); // add to the example message blocks array examples.push(parsed); } return examples; } /** * One-time setup for prompt manager module. * * @param openAiSettings * @returns {PromptManager|null} */ function setupChatCompletionPromptManager(openAiSettings) { // Do not set up prompt manager more than once if (promptManager) { promptManager.render(false); return promptManager; } promptManager = new PromptManager(); const configuration = { prefix: 'completion_', containerIdentifier: 'completion_prompt_manager', listIdentifier: 'completion_prompt_manager_list', toggleDisabled: [], sortableDelay: getSortableDelay(), defaultPrompts: { main: default_main_prompt, nsfw: default_nsfw_prompt, jailbreak: default_jailbreak_prompt, enhanceDefinitions: default_enhance_definitions_prompt, }, promptOrder: { strategy: 'global', dummyId: 100001, }, }; promptManager.saveServiceSettings = () => { saveSettingsDebounced(); return new Promise((resolve) => eventSource.once(event_types.SETTINGS_UPDATED, resolve)); }; promptManager.tryGenerate = () => { if (characters[this_chid]) { return Generate('normal', {}, true); } else { return Promise.resolve(); } }; promptManager.tokenHandler = tokenHandler; promptManager.init(configuration, openAiSettings); promptManager.render(false); return promptManager; } /** * Parses the example messages into individual messages. * @param {string} messageExampleString - The string containing the example messages * @param {boolean} appendNamesForGroup - Whether to append the character name for group chats * @returns {Message[]} Array of message objects */ export function parseExampleIntoIndividual(messageExampleString, appendNamesForGroup = true) { const groupBotNames = getGroupNames().map(name => `${name}:`); let result = []; // array of msgs let tmp = messageExampleString.split('\n'); let cur_msg_lines = []; let in_user = false; let in_bot = false; let botName = name2; // DRY my cock and balls :) function add_msg(name, role, system_name) { // join different newlines (we split them by \n and join by \n) // remove char name // strip to remove extra spaces let parsed_msg = cur_msg_lines.join('\n').replace(name + ':', '').trim(); if (appendNamesForGroup && selected_group && ['example_user', 'example_assistant'].includes(system_name)) { parsed_msg = `${name}: ${parsed_msg}`; } result.push({ 'role': role, 'content': parsed_msg, 'name': system_name }); cur_msg_lines = []; } // skip first line as it'll always be "This is how {bot name} should talk" for (let i = 1; i < tmp.length; i++) { let cur_str = tmp[i]; // if it's the user message, switch into user mode and out of bot mode // yes, repeated code, but I don't care if (cur_str.startsWith(name1 + ':')) { in_user = true; // we were in the bot mode previously, add the message if (in_bot) { add_msg(botName, 'system', 'example_assistant'); } in_bot = false; } else if (cur_str.startsWith(name2 + ':') || groupBotNames.some(n => cur_str.startsWith(n))) { if (!cur_str.startsWith(name2 + ':') && groupBotNames.length) { botName = cur_str.split(':')[0]; } in_bot = true; // we were in the user mode previously, add the message if (in_user) { add_msg(name1, 'system', 'example_user'); } in_user = false; } // push the current line into the current message array only after checking for presence of user/bot cur_msg_lines.push(cur_str); } // Special case for last message in a block because we don't have a new message to trigger the switch if (in_user) { add_msg(name1, 'system', 'example_user'); } else if (in_bot) { add_msg(botName, 'system', 'example_assistant'); } return result; } export function formatWorldInfo(value, { wiFormat = null } = {}) { if (!value) { return ''; } const format = wiFormat ?? oai_settings.wi_format; if (!format.trim()) { return value; } return stringFormat(format, value); } /** * This function populates the injections in the conversation. * * @param {Prompt[]} prompts - Array containing injection prompts. * @param {Object[]} messages - Array containing all messages. * @returns {Promise} - Array containing all messages with injections. */ async function populationInjectionPrompts(prompts, messages) { let totalInsertedMessages = 0; const roleTypes = { 'system': extension_prompt_roles.SYSTEM, 'user': extension_prompt_roles.USER, 'assistant': extension_prompt_roles.ASSISTANT, }; const maxDepth = getExtensionPromptMaxDepth(); for (let i = 0; i <= maxDepth; i++) { // Get prompts for current depth const depthPrompts = prompts.filter(prompt => prompt.injection_depth === i && prompt.content); const roleMessages = []; const separator = '\n'; const wrap = false; // Group prompts by priority const extensionPromptsOrder = '100'; const orderGroups = { [extensionPromptsOrder]: [], }; for (const prompt of depthPrompts) { const order = prompt.injection_order ?? 100; if (!orderGroups[order]) { orderGroups[order] = []; } orderGroups[order].push(prompt); } // Process each order group in order (b - a = low to high ; a - b = high to low) const orders = Object.keys(orderGroups).sort((a, b) => +b - +a); for (const order of orders) { const orderPrompts = orderGroups[order]; // Order of priority for roles (most important go lower) const roles = ['system', 'user', 'assistant']; for (const role of roles) { const rolePrompts = orderPrompts .filter(prompt => prompt.role === role) .map(x => x.content) .join(separator); // Get extension prompt const extensionPrompt = order === extensionPromptsOrder ? await getExtensionPrompt(extension_prompt_types.IN_CHAT, i, separator, roleTypes[role], wrap) : ''; const jointPrompt = [rolePrompts, extensionPrompt].filter(x => x).map(x => x.trim()).join(separator); if (jointPrompt && jointPrompt.length) { roleMessages.push({ 'role': role, 'content': jointPrompt, injected: true }); } } } if (roleMessages.length) { const injectIdx = i + totalInsertedMessages; messages.splice(injectIdx, 0, ...roleMessages); totalInsertedMessages += roleMessages.length; } } messages = messages.reverse(); return messages; } /** * Populates the chat history of the conversation. * @param {object[]} messages - Array containing all messages. * @param {import('./PromptManager').PromptCollection} prompts - Map object containing all prompts where the key is the prompt identifier and the value is the prompt object. * @param {ChatCompletion} chatCompletion - An instance of ChatCompletion class that will be populated with the prompts. * @param type * @param cyclePrompt */ async function populateChatHistory(messages, prompts, chatCompletion, type = null, cyclePrompt = null) { if (!prompts.has('chatHistory')) { return; } chatCompletion.add(new MessageCollection('chatHistory'), prompts.index('chatHistory')); // Reserve budget for new chat message const newChat = selected_group ? oai_settings.new_group_chat_prompt : oai_settings.new_chat_prompt; const newChatMessage = await Message.createAsync('system', substituteParams(newChat), 'newMainChat'); chatCompletion.reserveBudget(newChatMessage); // Reserve budget for group nudge let groupNudgeMessage = null; const noGroupNudgeTypes = ['impersonate']; if (selected_group && prompts.has('groupNudge') && !noGroupNudgeTypes.includes(type)) { groupNudgeMessage = await Message.fromPromptAsync(prompts.get('groupNudge')); chatCompletion.reserveBudget(groupNudgeMessage); } // Reserve budget for continue nudge let continueMessageCollection = null; if (type === 'continue' && cyclePrompt && !oai_settings.continue_prefill) { const promptObject = { identifier: 'continueNudge', role: 'system', content: substituteParamsExtended(oai_settings.continue_nudge_prompt, { lastChatMessage: String(cyclePrompt).trim() }), system_prompt: true, }; continueMessageCollection = new MessageCollection('continueNudge'); const continueMessageIndex = messages.findLastIndex(x => !x.injected); if (continueMessageIndex >= 0) { const continueMessage = messages.splice(continueMessageIndex, 1)[0]; const prompt = new Prompt(continueMessage); const chatMessage = await Message.fromPromptAsync(promptManager.preparePrompt(prompt)); continueMessageCollection.add(chatMessage); } const continueNudgePrompt = new Prompt(promptObject); const preparedNudgePrompt = promptManager.preparePrompt(continueNudgePrompt); const continueNudgeMessage = await Message.fromPromptAsync(preparedNudgePrompt); continueMessageCollection.add(continueNudgeMessage); chatCompletion.reserveBudget(continueMessageCollection); } const lastChatPrompt = messages[messages.length - 1]; const message = await Message.createAsync('user', oai_settings.send_if_empty, 'emptyUserMessageReplacement'); if (lastChatPrompt && lastChatPrompt.role === 'assistant' && oai_settings.send_if_empty && chatCompletion.canAfford(message)) { chatCompletion.insert(message, 'chatHistory'); } const imageInlining = isImageInliningSupported(); const videoInlining = isVideoInliningSupported(); const audioInlining = isAudioInliningSupported(); const canUseTools = ToolManager.isToolCallingSupported(); const includeSignature = isReasoningSignatureSupported(); const isToolReasoningProvider = interleaved_reasoning_providers.includes(oai_settings.chat_completion_source); const toolReasoningMode = isToolReasoningProvider ? getEffectiveToolReasoningMode() : tool_reasoning_modes.DISABLED; const includeToolReasoning = toolReasoningMode !== tool_reasoning_modes.DISABLED; const lastUserIdx = messages.findLastIndex(x => x.role === 'user'); // Insert chat messages as long as there is budget available const chatPool = [...messages].reverse(); for (let index = 0; index < chatPool.length; index++) { const chatPrompt = chatPool[index]; // We do not want to mutate the prompt const prompt = new Prompt(chatPrompt); prompt.identifier = `chatHistory-${messages.length - index}`; const chatMessage = await Message.fromPromptAsync(promptManager.preparePrompt(prompt)); if (promptManager.serviceSettings.names_behavior === character_names_behavior.COMPLETION && prompt.name) { const messageName = promptManager.isValidName(prompt.name) ? prompt.name : promptManager.sanitizeName(prompt.name); await chatMessage.setName(messageName); } /** * Inline a media attachment into the chat message. * @param {MediaAttachment} media - The media attachment to inline. */ async function inlineMediaAttachment(media) { if (!media || !media.url) { return; } if (!media.type) { media.type = MEDIA_TYPE.IMAGE; } if (imageInlining && media.type === MEDIA_TYPE.IMAGE) { await chatMessage.addImage(media.url); } if (videoInlining && media.type === MEDIA_TYPE.VIDEO) { await chatMessage.addVideo(media.url); } if (audioInlining && media.type === MEDIA_TYPE.AUDIO) { await chatMessage.addAudio(media.url); } } if (Array.isArray(chatPrompt.media) && chatPrompt.media.length) { if (chatPrompt.mediaDisplay === MEDIA_DISPLAY.LIST) { for (const media of chatPrompt.media) { await inlineMediaAttachment(media); } } if (chatPrompt.mediaDisplay === MEDIA_DISPLAY.GALLERY) { const media = chatPrompt.media[chatPrompt.mediaIndex]; await inlineMediaAttachment(media); } } if (canUseTools && Array.isArray(chatPrompt.invocations)) { const promptIdx = messages.indexOf(chatPrompt); const reasoningIsEligible = toolReasoningMode !== tool_reasoning_modes.DISABLED && promptIdx > lastUserIdx; let previousAssistantReasoning = ''; if (reasoningIsEligible) { if (toolReasoningMode === tool_reasoning_modes.ACTIVE_CHAIN) { // Strict chain mode: skip tool/tool-call messages, then use only the first assistant text boundary. for (let idx = promptIdx - 1; idx > lastUserIdx; idx--) { const candidate = messages[idx]; if (candidate?.role === 'tool') { continue; } if (candidate?.role === 'assistant' && Array.isArray(candidate.invocations)) { continue; } const hasAssistantText = candidate?.role === 'assistant' && !Array.isArray(candidate.invocations) && typeof candidate.content === 'string' && candidate.content.trim().length > 0; if (hasAssistantText) { previousAssistantReasoning = String(candidate.reasoning ?? ''); } break; } } else if (toolReasoningMode === tool_reasoning_modes.SINCE_LAST_USER) { // Broad mode: use the latest assistant text reasoning anywhere since the last user. for (let idx = promptIdx - 1; idx > lastUserIdx; idx--) { const candidate = messages[idx]; const hasAssistantText = candidate?.role === 'assistant' && !Array.isArray(candidate.invocations) && typeof candidate.content === 'string' && candidate.content.trim().length > 0; if (!hasAssistantText) { continue; } const candidateReasoning = String(candidate.reasoning ?? ''); if (candidateReasoning) { previousAssistantReasoning = candidateReasoning; break; } } } } /** @type {import('./tool-calling.js').ToolInvocation[]} */ const invocations = chatPrompt.invocations.map(invocation => { const clone = structuredClone(invocation); if (!reasoningIsEligible) { delete clone.reasoning; } else if (previousAssistantReasoning && !clone.reasoning) { // Fall back to adjacent assistant-text reasoning only when the invocation has none of its own. clone.reasoning = previousAssistantReasoning; } return clone; }); const toolCallMessage = await Message.createAsync(chatMessage.role, undefined, 'toolCall-' + chatMessage.identifier); const toolResultMessages = await Promise.all(invocations.slice().reverse().map((invocation) => Message.createAsync('tool', invocation.result || '[No content]', invocation.id))); await toolCallMessage.setToolCalls(invocations, includeSignature, includeToolReasoning); if (chatCompletion.canAffordAll([toolCallMessage, ...toolResultMessages])) { for (const resultMessage of toolResultMessages) { chatCompletion.insertAtStart(resultMessage, 'chatHistory'); } chatCompletion.insertAtStart(toolCallMessage, 'chatHistory'); } else { break; } continue; } if (includeSignature && chatPrompt.signature) { chatMessage.signature = chatPrompt.signature; } if (chatCompletion.canAfford(chatMessage)) { chatCompletion.insertAtStart(chatMessage, 'chatHistory'); } else { break; } } // Insert and free new chat chatCompletion.freeBudget(newChatMessage); chatCompletion.insertAtStart(newChatMessage, 'chatHistory'); // Reserve budget for group nudge if (selected_group && groupNudgeMessage) { chatCompletion.freeBudget(groupNudgeMessage); chatCompletion.insertAtEnd(groupNudgeMessage, 'chatHistory'); } // Insert and free continue nudge if (type === 'continue' && continueMessageCollection) { chatCompletion.freeBudget(continueMessageCollection); chatCompletion.add(continueMessageCollection, -1); } } /** * This function populates the dialogue examples in the conversation. * * @param {import('./PromptManager').PromptCollection} prompts - Map object containing all prompts where the key is the prompt identifier and the value is the prompt object. * @param {ChatCompletion} chatCompletion - An instance of ChatCompletion class that will be populated with the prompts. * @param {Object[]} messageExamples - Array containing all message examples. */ async function populateDialogueExamples(prompts, chatCompletion, messageExamples) { if (!prompts.has('dialogueExamples')) { return; } chatCompletion.add(new MessageCollection('dialogueExamples'), prompts.index('dialogueExamples')); if (Array.isArray(messageExamples) && messageExamples.length) { const newExampleChat = await Message.createAsync('system', substituteParams(oai_settings.new_example_chat_prompt), 'newChat'); for (const dialogue of [...messageExamples]) { const dialogueIndex = messageExamples.indexOf(dialogue); const chatMessages = []; for (let promptIndex = 0; promptIndex < dialogue.length; promptIndex++) { const prompt = dialogue[promptIndex]; const role = 'system'; const content = prompt.content || ''; const identifier = `dialogueExamples ${dialogueIndex}-${promptIndex}`; const chatMessage = await Message.createAsync(role, content, identifier); await chatMessage.setName(prompt.name); chatMessages.push(chatMessage); } if (!chatCompletion.canAffordAll([newExampleChat, ...chatMessages])) { break; } chatCompletion.insert(newExampleChat, 'dialogueExamples'); for (const chatMessage of chatMessages) { chatCompletion.insert(chatMessage, 'dialogueExamples'); } } } } /** * @param {number} position - Prompt position in the extensions object. * @returns {string|false} - The prompt position for prompt collection. */ export function getPromptPosition(position) { if (position == extension_prompt_types.BEFORE_PROMPT) { return 'start'; } if (position == extension_prompt_types.IN_PROMPT) { return 'end'; } return false; } /** * Gets a Chat Completion role based on the prompt role. * @param {number} role Role of the prompt. * @returns {string} Mapped role. */ export function getPromptRole(role) { switch (role) { case extension_prompt_roles.SYSTEM: return 'system'; case extension_prompt_roles.USER: return 'user'; case extension_prompt_roles.ASSISTANT: return 'assistant'; default: return 'system'; } } /** * Populate a chat conversation by adding prompts to the conversation and managing system and user prompts. * * @param {import('./PromptManager.js').PromptCollection} prompts - PromptCollection containing all prompts where the key is the prompt identifier and the value is the prompt object. * @param {ChatCompletion} chatCompletion - An instance of ChatCompletion class that will be populated with the prompts. * @param {Object} options - An object with optional settings. * @param {string} options.bias - A bias to be added in the conversation. * @param {string} options.quietPrompt - Instruction prompt for extras * @param {string} options.quietImage - Image prompt for extras * @param {string} options.type - The type of the chat, can be 'impersonate'. * @param {string} options.cyclePrompt - The last prompt in the conversation. * @param {object[]} options.messages - Array containing all messages. * @param {object[]} options.messageExamples - Array containing all message examples. * @returns {Promise} */ async function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt, messages, messageExamples }) { // Helper function for preparing a prompt, that already exists within the prompt collection, for completion const addToChatCompletion = async (source, target = null) => { // We need the prompts array to determine a position for the source. if (false === prompts.has(source)) return; if (promptManager.isPromptDisabledForActiveCharacter(source) && source !== 'main') { promptManager.log(`Skipping prompt ${source} because it is disabled`); return; } const prompt = prompts.get(source); if (prompt.injection_position === INJECTION_POSITION.ABSOLUTE) { promptManager.log(`Skipping prompt ${source} because it is an absolute prompt`); return; } const index = target ? prompts.index(target) : prompts.index(source); const collection = new MessageCollection(source); const message = await Message.fromPromptAsync(prompt); collection.add(message); chatCompletion.add(collection, index); }; chatCompletion.reserveBudget(3); // every reply is primed with <|start|>assistant<|message|> // Character and world information await addToChatCompletion('worldInfoBefore'); await addToChatCompletion('main'); await addToChatCompletion('worldInfoAfter'); await addToChatCompletion('charDescription'); await addToChatCompletion('charPersonality'); await addToChatCompletion('scenario'); await addToChatCompletion('personaDescription'); // Collection of control prompts that will always be positioned last chatCompletion.setOverriddenPrompts(prompts.overriddenPrompts); const controlPrompts = new MessageCollection('controlPrompts'); const impersonateMessage = await Message.fromPromptAsync(prompts.get('impersonate')) ?? null; if (type === 'impersonate') controlPrompts.add(impersonateMessage); // Add quiet prompt to control prompts // This should always be last, even in control prompts. Add all further control prompts BEFORE this prompt const quietPromptMessage = await Message.fromPromptAsync(prompts.get('quietPrompt')) ?? null; if (quietPromptMessage && quietPromptMessage.content) { if (isImageInliningSupported() && quietImage) { await quietPromptMessage.addImage(quietImage); } controlPrompts.add(quietPromptMessage); } chatCompletion.reserveBudget(controlPrompts); // Add ordered system and user prompts const systemPrompts = ['nsfw', 'jailbreak']; const userRelativePrompts = prompts.collection .filter((prompt) => false === prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE) .reduce((acc, prompt) => { acc.push(prompt.identifier); return acc; }, []); const absolutePrompts = prompts.collection .filter((prompt) => prompt.injection_position === INJECTION_POSITION.ABSOLUTE) .reduce((acc, prompt) => { acc.push(prompt); return acc; }, []); for (const identifier of [...systemPrompts, ...userRelativePrompts]) { await addToChatCompletion(identifier); } // Add enhance definition instruction if (prompts.has('enhanceDefinitions')) await addToChatCompletion('enhanceDefinitions'); // Bias if (bias && bias.trim().length) await addToChatCompletion('bias'); const injectToMain = async (/** @type {Prompt} */ prompt, /** @type {string|number} */ position) => { if (chatCompletion.has('main')) { const message = await Message.fromPromptAsync(prompt); chatCompletion.insert(message, 'main', position); } else { // Convert the relative prompt to an injection and place it relative to main prompt // Keeping prompts in the same order bucket will squash them together during in-chat injection const indexOfMain = absolutePrompts.findIndex(p => p.identifier === 'main'); if (indexOfMain >= 0) { const main = absolutePrompts[indexOfMain]; const promptCopy = new Prompt(prompt); promptCopy.role = main.role; promptCopy.injection_position = main.injection_position; promptCopy.injection_depth = main.injection_depth; promptCopy.injection_order = main.injection_order; const newIndex = position === 'end' ? indexOfMain + 1 : indexOfMain; absolutePrompts.splice(newIndex, 0, promptCopy); } } }; const knownPrompts = [ 'summary', 'authorsNote', 'vectorsMemory', 'vectorsDataBank', 'smartContext', ]; // Known relative extension prompts for (const key of knownPrompts) { if (prompts.has(key)) { const prompt = prompts.get(key); if (prompt.position) { await injectToMain(prompt, prompt.position); } } } // Other relative extension prompts for (const prompt of prompts.collection.filter(p => p.extension && p.position)) { await injectToMain(prompt, prompt.position); } // Pre-allocation of tokens for tool data if (ToolManager.canPerformToolCalls(type)) { const toolData = {}; await ToolManager.registerFunctionToolsOpenAI(toolData); const toolMessage = [{ role: 'user', content: JSON.stringify(toolData) }]; const toolTokens = await tokenHandler.countAsync(toolMessage); chatCompletion.reserveBudget(toolTokens); } // Displace the message to be continued from its original position before performing in-chat injections // In case if it is an assistant message, we want to prepend the users assistant prefill on the message if (type === 'continue' && oai_settings.continue_prefill && messages.length) { const chatMessage = messages.shift(); const isAssistantRole = chatMessage.role === 'assistant'; const supportsAssistantPrefill = oai_settings.chat_completion_source === chat_completion_sources.CLAUDE; const namesInCompletion = oai_settings.names_behavior === character_names_behavior.COMPLETION; const assistantPrefill = isAssistantRole && supportsAssistantPrefill ? substituteParams(oai_settings.assistant_prefill) : ''; const messageContent = [assistantPrefill, chatMessage.content].filter(x => x).join('\n\n'); const continueMessage = await Message.createAsync(chatMessage.role, messageContent, 'continuePrefill'); chatMessage.name && namesInCompletion && await continueMessage.setName(promptManager.sanitizeName(chatMessage.name)); controlPrompts.add(continueMessage); chatCompletion.reserveBudget(continueMessage); } // Add in-chat injections messages = await populationInjectionPrompts(absolutePrompts, messages); // Decide whether dialogue examples should always be added if (power_user.pin_examples) { await populateDialogueExamples(prompts, chatCompletion, messageExamples); await populateChatHistory(messages, prompts, chatCompletion, type, cyclePrompt); } else { await populateChatHistory(messages, prompts, chatCompletion, type, cyclePrompt); await populateDialogueExamples(prompts, chatCompletion, messageExamples); } chatCompletion.freeBudget(controlPrompts); if (controlPrompts.collection.length) chatCompletion.add(controlPrompts); } /** * Combines system prompts with prompt manager prompts * * @param {Object} options - An object with optional settings. * @param {string} options.scenario - The scenario or context of the dialogue. * @param {string} options.charPersonality - Description of the character's personality. * @param {string} options.name2 - The second name to be used in the messages. * @param {string} options.worldInfoBefore - The world info to be added before the main conversation. * @param {string} options.worldInfoAfter - The world info to be added after the main conversation. * @param {string} options.charDescription - Description of the character. * @param {string} options.quietPrompt - The quiet prompt to be used in the conversation. * @param {string} options.bias - The bias to be added in the conversation. * @param {Object} options.extensionPrompts - An object containing additional prompts. * @param {string} options.systemPromptOverride - Character card override of the main prompt * @param {string} options.jailbreakPromptOverride - Character card override of the PHI * @param {string} options.type - The type of generation that triggered the prompt * @returns {Promise} prompts - The prepared and merged system and user-defined prompts. */ async function preparePromptsForChatCompletion({ scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts, systemPromptOverride, jailbreakPromptOverride, type }) { const scenarioText = scenario && oai_settings.scenario_format ? substituteParams(oai_settings.scenario_format) : (scenario || ''); const charPersonalityText = charPersonality && oai_settings.personality_format ? substituteParams(oai_settings.personality_format) : (charPersonality || ''); const groupNudge = substituteParams(oai_settings.group_nudge_prompt); const impersonationPrompt = oai_settings.impersonation_prompt ? substituteParams(oai_settings.impersonation_prompt) : ''; // Create entries for system prompts const systemPrompts = [ // Ordered prompts for which a marker should exist { role: 'system', content: formatWorldInfo(worldInfoBefore), identifier: 'worldInfoBefore' }, { role: 'system', content: formatWorldInfo(worldInfoAfter), identifier: 'worldInfoAfter' }, { role: 'system', content: charDescription, identifier: 'charDescription' }, { role: 'system', content: charPersonalityText, identifier: 'charPersonality' }, { role: 'system', content: scenarioText, identifier: 'scenario' }, // Unordered prompts without marker { role: 'system', content: impersonationPrompt, identifier: 'impersonate' }, { role: 'system', content: quietPrompt, identifier: 'quietPrompt' }, { role: 'system', content: groupNudge, identifier: 'groupNudge' }, { role: 'assistant', content: bias, identifier: 'bias' }, ]; // Tavern Extras - Summary const summary = extensionPrompts['1_memory']; if (summary && summary.value) systemPrompts.push({ role: getPromptRole(summary.role), content: summary.value, identifier: 'summary', position: getPromptPosition(summary.position), }); // Authors Note const authorsNote = extensionPrompts['2_floating_prompt']; if (authorsNote && authorsNote.value) systemPrompts.push({ role: getPromptRole(authorsNote.role), content: authorsNote.value, identifier: 'authorsNote', position: getPromptPosition(authorsNote.position), }); // Vectors Memory const vectorsMemory = extensionPrompts['3_vectors']; if (vectorsMemory && vectorsMemory.value) systemPrompts.push({ role: 'system', content: vectorsMemory.value, identifier: 'vectorsMemory', position: getPromptPosition(vectorsMemory.position), }); const vectorsDataBank = extensionPrompts['4_vectors_data_bank']; if (vectorsDataBank && vectorsDataBank.value) systemPrompts.push({ role: getPromptRole(vectorsDataBank.role), content: vectorsDataBank.value, identifier: 'vectorsDataBank', position: getPromptPosition(vectorsDataBank.position), }); // Smart Context (ChromaDB) const smartContext = extensionPrompts.chromadb; if (smartContext && smartContext.value) systemPrompts.push({ role: 'system', content: smartContext.value, identifier: 'smartContext', position: getPromptPosition(smartContext.position), }); // Persona Description if (power_user.persona_description && power_user.persona_description_position === persona_description_positions.IN_PROMPT) { systemPrompts.push({ role: 'system', content: power_user.persona_description, identifier: 'personaDescription' }); } const knownExtensionPrompts = [ '1_memory', '2_floating_prompt', '3_vectors', '4_vectors_data_bank', 'chromadb', 'PERSONA_DESCRIPTION', 'QUIET_PROMPT', 'DEPTH_PROMPT', ]; // Anything that is not a known extension prompt for (const key in extensionPrompts) { if (Object.hasOwn(extensionPrompts, key)) { const prompt = extensionPrompts[key]; if (knownExtensionPrompts.includes(key)) continue; if (!extensionPrompts[key].value) continue; if (![extension_prompt_types.BEFORE_PROMPT, extension_prompt_types.IN_PROMPT].includes(prompt.position)) continue; const hasFilter = typeof prompt.filter === 'function'; if (hasFilter && !await prompt.filter()) continue; systemPrompts.push({ identifier: key.replace(/\W/g, '_'), position: getPromptPosition(prompt.position), role: getPromptRole(prompt.role), content: prompt.value, extension: true, }); } } // This is the prompt order defined by the user const prompts = promptManager.getPromptCollection(type); // Merge system prompts with prompt manager prompts systemPrompts.forEach(prompt => { const collectionPrompt = prompts.get(prompt.identifier); // Apply system prompt role/depth overrides if they set in the prompt manager if (collectionPrompt) { // In-Chat / Relative prompt.injection_position = collectionPrompt.injection_position ?? prompt.injection_position; // Depth for In-Chat prompt.injection_depth = collectionPrompt.injection_depth ?? prompt.injection_depth; // Priority for In-Chat prompt.injection_order = collectionPrompt.injection_order ?? prompt.injection_order; // Role (system, user, assistant) prompt.role = collectionPrompt.role ?? prompt.role; } const newPrompt = promptManager.preparePrompt(prompt); const markerIndex = prompts.index(prompt.identifier); if (-1 !== markerIndex) prompts.collection[markerIndex] = newPrompt; else prompts.add(newPrompt); }); // Apply character-specific main prompt const systemPrompt = prompts.get('main') ?? null; const isSystemPromptDisabled = promptManager.isPromptDisabledForActiveCharacter('main'); if (systemPromptOverride && systemPrompt && systemPrompt.forbid_overrides !== true && !isSystemPromptDisabled) { const mainOriginalContent = systemPrompt.content; systemPrompt.content = systemPromptOverride; const mainReplacement = promptManager.preparePrompt(systemPrompt, mainOriginalContent); prompts.override(mainReplacement, prompts.index('main')); } // Apply character-specific jailbreak const jailbreakPrompt = prompts.get('jailbreak') ?? null; const isJailbreakPromptDisabled = promptManager.isPromptDisabledForActiveCharacter('jailbreak'); if (jailbreakPromptOverride && jailbreakPrompt && jailbreakPrompt.forbid_overrides !== true && !isJailbreakPromptDisabled) { const jbOriginalContent = jailbreakPrompt.content; jailbreakPrompt.content = jailbreakPromptOverride; const jbReplacement = promptManager.preparePrompt(jailbreakPrompt, jbOriginalContent); prompts.override(jbReplacement, prompts.index('jailbreak')); } return prompts; } /** * Take a configuration object and prepares messages for a chat with OpenAI's chat completion API. * Handles prompts, prepares chat history, manages token budget, and processes various user settings. * * @param {Object} content - System prompts provided by SillyTavern * @param {string} content.name2 - The second name to be used in the messages. * @param {string} content.charDescription - Description of the character. * @param {string} content.charPersonality - Description of the character's personality. * @param {string} content.scenario - The scenario or context of the dialogue. * @param {string} content.worldInfoBefore - The world info to be added before the main conversation. * @param {string} content.worldInfoAfter - The world info to be added after the main conversation. * @param {string} content.bias - The bias to be added in the conversation. * @param {string} content.type - The type of the chat, can be 'impersonate'. * @param {string} content.quietPrompt - The quiet prompt to be used in the conversation. * @param {string} content.quietImage - Image prompt for extras * @param {string} content.cyclePrompt - The last prompt used for chat message continuation. * @param {string} content.systemPromptOverride - The system prompt override. * @param {string} content.jailbreakPromptOverride - The jailbreak prompt override. * @param {object} content.extensionPrompts - An array of additional prompts. * @param {object[]} content.messages - An array of messages to be used as chat history. * @param {string[]} content.messageExamples - An array of messages to be used as dialogue examples. * @param dryRun - Whether this is a live call or not. * @returns {Promise<(any[]|boolean)[]>} An array where the first element is the prepared chat and the second element is a boolean flag. */ export async function prepareOpenAIMessages({ name2, charDescription, charPersonality, scenario, worldInfoBefore, worldInfoAfter, bias, type, quietPrompt, quietImage, extensionPrompts, cyclePrompt, systemPromptOverride, jailbreakPromptOverride, messages, messageExamples, }, dryRun) { // Without a character selected, there is no way to accurately calculate tokens if (!promptManager.activeCharacter && dryRun) return [null, false]; const chatCompletion = new ChatCompletion(); if (power_user.console_log_prompts) chatCompletion.enableLogging(); const userSettings = promptManager.serviceSettings; chatCompletion.setTokenBudget(userSettings.openai_max_context, userSettings.openai_max_tokens); try { // Merge markers and ordered user prompts with system prompts const prompts = await preparePromptsForChatCompletion({ scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts, systemPromptOverride, jailbreakPromptOverride, type, }); // Fill the chat completion with as much context as the budget allows await populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt, messages, messageExamples }); } catch (error) { if (error instanceof TokenBudgetExceededError) { toastr.error(t`Mandatory prompts exceed the context size.`); chatCompletion.log('Mandatory prompts exceed the context size.'); promptManager.error = t`Not enough free tokens for mandatory prompts. Raise your token limit or disable custom prompts.`; } else if (error instanceof InvalidCharacterNameError) { toastr.warning(t`An error occurred while counting tokens: Invalid character name`); chatCompletion.log('Invalid character name'); promptManager.error = t`The name of at least one character contained whitespaces or special characters. Please check your user and character name.`; } else { toastr.error(t`An unknown error occurred while counting tokens. Further information may be available in console.`); chatCompletion.log('----- Unexpected error while preparing prompts -----'); chatCompletion.log(error); chatCompletion.log(error.stack); chatCompletion.log('----------------------------------------------------'); } } finally { // Pass chat completion to prompt manager for inspection promptManager.setChatCompletion(chatCompletion); if (oai_settings.squash_system_messages && dryRun == false) { await chatCompletion.squashSystemMessages(); } // All information is up-to-date, render. if (false === dryRun) promptManager.render(false); } const chat = chatCompletion.getChat(); const eventData = { chat, dryRun }; await eventSource.emit(event_types.CHAT_COMPLETION_PROMPT_READY, eventData); openai_messages_count = chat.filter(x => !x?.tool_calls && ['user', 'assistant', 'tool'].includes(x?.role)).length || 0; return [chat, promptManager.tokenHandler.counts]; } /** * Handles errors during streaming requests. * @param {Response} response * @param {string} decoded - response text or decoded stream data * @param {object} [options] * @param {boolean?} [options.quiet=false] Suppress toast messages */ export function tryParseStreamingError(response, decoded, { quiet = false } = {}) { try { const data = JSON.parse(decoded); if (!data) { return; } checkQuotaError(data, { quiet }); checkModerationError(data, { quiet }); // these do not throw correctly (equiv to Error("[object Object]")) // if trying to fix "[object Object]" displayed to users, start here if (data.error) { !quiet && toastr.error(data.error.message || response.statusText, 'Chat Completion API'); throw new Error(data); } if (data.message) { !quiet && toastr.error(data.message, 'Chat Completion API'); throw new Error(data); } if (data.detail) { !quiet && toastr.error(data.detail?.error?.message || response.statusText, 'Chat Completion API'); throw new Error(data); } } catch { // No JSON. Do nothing. } } /** * Checks if the response contains a quota error and displays a popup if it does. * @param data * @param {object} [options] * @param {boolean?} [options.quiet=false] Suppress toast messages * @returns {void} * @throws {object} - response JSON */ function checkQuotaError(data, { quiet = false } = {}) { if (!data) { return; } if (data.quota_error) { !quiet && renderTemplateAsync('quotaError').then((html) => Popup.show.text('Quota Error', html)); // this does not throw correctly (equiv to Error("[object Object]")) // if trying to fix "[object Object]" displayed to users, start here throw new Error(data); } } /** * @param {any} data * @param {object} [options] * @param {boolean?} [options.quiet=false] Suppress toast messages */ function checkModerationError(data, { quiet = false } = {}) { const moderationError = data?.error?.message?.includes('requires moderation'); if (moderationError && !quiet) { const moderationReason = `Reasons: ${data?.error?.metadata?.reasons?.join(', ') ?? '(N/A)'}`; const flaggedText = data?.error?.metadata?.flagged_input ?? '(N/A)'; toastr.info(flaggedText, moderationReason, { timeOut: 10000 }); } } /** * Gets the API model for the selected chat completion source. * @param {ChatCompletionSettings} settings Chat completion settings * @returns {string} API model */ export function getChatCompletionModel(settings = null) { settings = settings ?? oai_settings; const source = settings.chat_completion_source; switch (source) { case chat_completion_sources.CLAUDE: return settings.claude_model; case chat_completion_sources.OPENAI: return settings.openai_model; case chat_completion_sources.MAKERSUITE: return settings.google_model; case chat_completion_sources.VERTEXAI: return settings.vertexai_model; case chat_completion_sources.OPENROUTER: return settings.openrouter_model !== openrouter_website_model ? settings.openrouter_model : null; case chat_completion_sources.AI21: return settings.ai21_model; case chat_completion_sources.MISTRALAI: return settings.mistralai_model; case chat_completion_sources.CUSTOM: return settings.custom_model; case chat_completion_sources.COHERE: return settings.cohere_model; case chat_completion_sources.PERPLEXITY: return settings.perplexity_model; case chat_completion_sources.GROQ: return settings.groq_model; case chat_completion_sources.SILICONFLOW: return settings.siliconflow_model; case chat_completion_sources.MINIMAX: return settings.minimax_model; case chat_completion_sources.ELECTRONHUB: return settings.electronhub_model; case chat_completion_sources.CHUTES: return settings.chutes_model; case chat_completion_sources.NANOGPT: return settings.nanogpt_model; case chat_completion_sources.DEEPSEEK: return settings.deepseek_model; case chat_completion_sources.AIMLAPI: return settings.aimlapi_model; case chat_completion_sources.XAI: return settings.xai_model; case chat_completion_sources.POLLINATIONS: return settings.pollinations_model; case chat_completion_sources.COMETAPI: return settings.cometapi_model; case chat_completion_sources.MOONSHOT: return settings.moonshot_model; case chat_completion_sources.FIREWORKS: return settings.fireworks_model; case chat_completion_sources.AZURE_OPENAI: return settings.azure_openai_model; case chat_completion_sources.ZAI: return settings.zai_model; case chat_completion_sources.WORKERS_AI: return settings.workers_ai_model; default: console.error(`Unknown chat completion source: ${source}`); return ''; } } function getOpenRouterModelTemplate(option) { const model = model_list.find(x => x.id === option?.element?.value); if (!option.id || !model) { return option.text; } let tokens_dollar = Number(1 / (1000 * model.pricing?.prompt)); let tokens_rounded = (Math.round(tokens_dollar * 1000) / 1000).toFixed(0); const price = 0 === Number(model.pricing?.prompt) ? 'Free' : `${tokens_rounded}k t/$ `; return $((`
${DOMPurify.sanitize(model.name)} | ${model.context_length} ctx | ${price}
`)); } function calculateOpenRouterCost() { if (oai_settings.chat_completion_source !== chat_completion_sources.OPENROUTER) { return; } let cost = 'Unknown'; const model = model_list.find(x => x.id === oai_settings.openrouter_model); if (model?.pricing) { const completionCost = Number(model.pricing.completion); const promptCost = Number(model.pricing.prompt); const completionTokens = oai_settings.openai_max_tokens; const promptTokens = (oai_settings.openai_max_context - completionTokens); const totalCost = (completionCost * completionTokens) + (promptCost * promptTokens); if (!isNaN(totalCost)) { cost = '$' + totalCost.toFixed(3); } } if (oai_settings.enable_web_search) { const webSearchCost = (0.02).toFixed(2); cost = t`${cost} + $${webSearchCost}`; } $('#openrouter_max_prompt_cost').text(cost); } function getElectronHubModelTemplate(option) { const model = model_list.find(x => x.id === option?.element?.value); if (!option.id || !model) { return option.text; } const inputPrice = model.pricing?.input; const outputPrice = model.pricing?.output; const price = inputPrice && outputPrice ? `$${inputPrice}/$${outputPrice} in/out Mtoken` : 'Unknown'; const visionIcon = model.metadata?.vision ? '' : ''; const reasoningIcon = model.metadata?.reasoning ? '' : ''; const toolCallsIcon = model.metadata?.function_call ? '' : ''; const premiumIcon = model?.premium_model ? '' : ''; const iconsContainer = document.createElement('span'); iconsContainer.insertAdjacentHTML('beforeend', visionIcon); iconsContainer.insertAdjacentHTML('beforeend', reasoningIcon); iconsContainer.insertAdjacentHTML('beforeend', toolCallsIcon); iconsContainer.insertAdjacentHTML('beforeend', premiumIcon); const capabilities = (iconsContainer.children.length) ? ` | ${iconsContainer.innerHTML}` : ''; return $((`
${DOMPurify.sanitize(model.name)} | ${model.tokens} ctx | ${price}${capabilities}
`)); } function calculateElectronHubCost() { if (oai_settings.chat_completion_source !== chat_completion_sources.ELECTRONHUB) { return; } let cost = 'Unknown'; const model = model_list.find(x => x.id === oai_settings.electronhub_model); if (model?.pricing) { const outputCost = Number(model.pricing.output / 1000000); const inputCost = Number(model.pricing.input / 1000000); const outputTokens = oai_settings.openai_max_tokens; const inputTokens = (oai_settings.openai_max_context - outputTokens); const totalCost = (outputCost * outputTokens) + (inputCost * inputTokens); if (!isNaN(totalCost)) { cost = '$' + totalCost.toFixed(4); } } $('#electronhub_max_prompt_cost').text(cost); } function getChutesModelTemplate(option) { const model = model_list.find(x => x.id === option?.element?.value); if (!option.id || !model) { return option.text; } const inputPrice = model.pricing?.input; const outputPrice = model.pricing?.output; let price = 'Unknown'; if (inputPrice !== undefined && outputPrice !== undefined) { // Check if both prices are 0 (free model) if (inputPrice === 0 && outputPrice === 0) { price = 'Free'; } else { price = `$${inputPrice}/$${outputPrice} in/out Mtoken`; } } const contextLength = model.context_length || model.max_model_len || 'Unknown'; const visionIcon = model.input_modalities?.includes('image') ? '' : ''; const reasoningIcon = model.supported_features?.includes('reasoning') ? '' : ''; const toolCallsIcon = model.supported_features?.includes('structured_outputs') ? '' : ''; const iconsContainer = document.createElement('span'); iconsContainer.insertAdjacentHTML('beforeend', visionIcon); iconsContainer.insertAdjacentHTML('beforeend', reasoningIcon); iconsContainer.insertAdjacentHTML('beforeend', toolCallsIcon); const capabilities = (iconsContainer.children.length) ? ` | ${iconsContainer.innerHTML}` : ''; return $((`
${DOMPurify.sanitize(model.id)} | ${contextLength} ctx | ${price}${capabilities}
`)); } function calculateChutesCost() { if (oai_settings.chat_completion_source !== chat_completion_sources.CHUTES) { return; } let cost = 'Unknown'; const model = model_list.find(x => x.id === oai_settings.chutes_model); if (model?.pricing) { const outputPrice = model.pricing?.output; const inputPrice = model.pricing?.input; if (outputPrice !== undefined && inputPrice !== undefined) { const outputCost = Number(outputPrice / 1000000); const inputCost = Number(inputPrice / 1000000); const outputTokens = oai_settings.openai_max_tokens; const inputTokens = (oai_settings.openai_max_context - outputTokens); const totalCost = (outputCost * outputTokens) + (inputCost * inputTokens); if (!isNaN(totalCost)) { cost = '$' + totalCost.toFixed(4); } } } $('#chutes_max_prompt_cost').text(cost); } function getNanoGptModelTemplate(option) { const model = model_list.find(x => x.id === option?.element?.value); if (!option.id || !model) { return option.text; } const inputPrice = model.pricing?.prompt; const outputPrice = model.pricing?.completion; let price = 'Unknown'; if (inputPrice !== undefined && outputPrice !== undefined) { if (inputPrice === 0 && outputPrice === 0) { price = 'Free'; } else { price = `$${Math.round(inputPrice * 100) / 100}/$${Math.round(outputPrice * 100) / 100} in/out Mtoken`; } } const visionIcon = model.capabilities?.vision ? '' : ''; const reasoningIcon = model.capabilities?.reasoning ? '' : ''; const toolCallsIcon = model.capabilities?.tool_calling ? '' : ''; let subHtml = ''; const sub = model.subscription; if (sub) { if (sub.included) { let titleText = 'Included in subscription'; let multiplierText = ''; if (sub.inputTokenMultiplier && sub.inputTokenMultiplier !== 1) { multiplierText = ` (${sub.inputTokenMultiplier}x)`; titleText += ` - Input Multiplier: ${sub.inputTokenMultiplier}x`; } subHtml = ` Sub${multiplierText}`; } else if (sub.note) { const safeNote = DOMPurify.sanitize(sub.note); subHtml = ` Not in Sub`; } } const iconsContainer = document.createElement('span'); iconsContainer.insertAdjacentHTML('beforeend', visionIcon); iconsContainer.insertAdjacentHTML('beforeend', reasoningIcon); iconsContainer.insertAdjacentHTML('beforeend', toolCallsIcon); iconsContainer.insertAdjacentHTML('beforeend', subHtml); const capabilities = (iconsContainer.children.length) ? ` | ${iconsContainer.innerHTML}` : ''; const contextLength = model.context_length || 'Unknown'; const modelName = model.name || model.id; return $((`
${DOMPurify.sanitize(modelName)} | ${contextLength} ctx | ${price}${capabilities}
`)); } function getAimlapiModelTemplate(option) { const model = model_list.find(x => x.id === option?.element?.value); if (!option.id || !model) { return option.text; } const vendor = model.id.split('/')[0]; return $((`
${DOMPurify.sanitize(model.info?.name || model.name || model.id)} | ${vendor}
`)); } function saveModelList(data) { model_list = data.map((model) => ({ ...model })); model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id)); if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) { model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.OPENROUTER); $('#model_openrouter_select').empty(); $('#model_openrouter_select').append($('').attr('label', vendor); models.forEach((model) => { optgroup.append($(''); model_list.forEach((model) => { $('.model_custom_select').append( $('').attr('label', vendor); models.forEach((model) => { optgroup.append($('').attr('label', vendor); models.forEach((model) => { optgroup.append($('').attr('label', vendor); models.forEach((model) => { optgroup.append($('').attr('label', vendor); models.forEach((model) => { optgroup.append($('