Files

6842 lines
269 KiB
JavaScript

/*
* 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 {
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';
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 claude_max = 9000; // We have a proper tokenizer, so theoretically could be larger (up to 9k)
const claude_100k_max = 99000;
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',
};
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 ZAI_ENDPOINT = {
COMMON: 'common',
CODING: 'coding',
};
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',
];
/**
* preset_name -> [selector, setting_name, is_checkbox, is_connection]
* @type {Record<string, [string, string, boolean, boolean]>}
*/
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],
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_group_models: ['#openrouter_group_models', 'openrouter_group_models', false, true],
openrouter_sort_models: ['#openrouter_sort_models', 'openrouter_sort_models', false, true],
openrouter_providers: ['#openrouter_providers_chat', 'openrouter_providers', false, true],
openrouter_allow_fallbacks: ['#openrouter_allow_fallbacks', 'openrouter_allow_fallbacks', true, true],
openrouter_middleout: ['#openrouter_middleout', 'openrouter_middleout', false, true],
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],
chutes_sort_models: ['#chutes_sort_models', 'chutes_sort_models', false, true],
siliconflow_model: ['#model_siliconflow_select', 'siliconflow_model', false, true],
electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true],
electronhub_sort_models: ['#electronhub_sort_models', 'electronhub_sort_models', false, true],
electronhub_group_models: ['#electronhub_group_models', 'electronhub_group_models', false, true],
nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, 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],
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],
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,
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',
chutes_sort_models: 'alphabetically',
siliconflow_model: 'deepseek-ai/DeepSeek-V3',
electronhub_model: 'gpt-4o-mini',
electronhub_sort_models: 'alphabetically',
electronhub_group_models: false,
nanogpt_model: 'gpt-4o-mini',
deepseek_model: 'deepseek-chat',
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,
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_group_models: false,
openrouter_sort_models: 'alphabetically',
openrouter_providers: [],
openrouter_allow_fallbacks: true,
openrouter_middleout: openrouter_middleout_types.ON,
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,
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} */
// Stub to prevent null reference errors when extensions access promptManager at module evaluation time
export let promptManager = { render: () => { }, renderDebounced: () => { } };
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;
const signature = isSameModel ? chat[j]?.extra?.reasoning_signature : null;
// Remove signatures from invocations if the API/model don't match
if (Array.isArray(invocations) && invocations.length > 0) {
invocations.forEach((invocation, index) => {
if (invocation.signature && !isSameModel) {
const cloneInvocation = structuredClone(invocation);
delete cloneInvocation.signature;
invocations[index] = cloneInvocation;
}
});
}
messages[i] = { 'role': role, 'content': content, name: name, 'media': media, 'mediaDisplay': mediaDisplay, 'mediaIndex': mediaIndex, 'invocations': invocations, 'signature': signature };
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 <START> {Example Dialogue:} and replace \r\n with just \n
let replaced = item.replace(/<START>/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 instanceof 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<Object[]>} - 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();
// 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)) {
/** @type {import('./tool-calling.js').ToolInvocation[]} */
const invocations = chatPrompt.invocations;
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);
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<void>}
*/
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<Object>} 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.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;
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 $((`
<div class="flex-container flexFlowColumn" title="${DOMPurify.sanitize(model.id)}">
<div><strong>${DOMPurify.sanitize(model.name)}</strong> | ${model.context_length} ctx | <small>${price}</small></div>
</div>
`));
}
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 ? '<i class="fa-solid fa-eye fa-sm" title="This model supports vision"></i>' : '';
const reasoningIcon = model.metadata?.reasoning ? '<i class="fa-solid fa-brain fa-sm" title="This model supports reasoning"></i>' : '';
const toolCallsIcon = model.metadata?.function_call ? '<i class="fa-solid fa-wrench fa-sm" title="This model supports function tools"></i>' : '';
const premiumIcon = model?.premium_model ? '<i class="fa-solid fa-crown fa-sm" title="This model requires a subscription"></i>' : '';
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 $((`
<div class="flex-container alignItemsBaseline" title="${DOMPurify.sanitize(model.id)}">
<strong>${DOMPurify.sanitize(model.name)}</strong> | ${model.tokens} ctx | <small>${price}</small>${capabilities}
</div>
`));
}
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') ? '<i class="fa-solid fa-eye fa-sm" title="This model supports vision"></i>' : '';
const reasoningIcon = model.supported_features?.includes('reasoning') ? '<i class="fa-solid fa-brain fa-sm" title="This model supports reasoning"></i>' : '';
const toolCallsIcon = model.supported_features?.includes('structured_outputs') ? '<i class="fa-solid fa-wrench fa-sm" title="This model supports function tools"></i>' : '';
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 $((`
<div class="flex-container alignItemsBaseline" title="${DOMPurify.sanitize(model.id)}">
<strong>${DOMPurify.sanitize(model.id)}</strong> | ${contextLength} ctx | <small>${price}</small>${capabilities}
</div>
`));
}
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) {
// Check if both prices are 0 (free model)
if (inputPrice === 0 && outputPrice === 0) {
price = 'Free';
} else {
price = `$${Math.round(inputPrice * 100) / 100}/$${Math.round(outputPrice * 100) / 100} in/out Mtoken`;
}
}
const contextLength = model.context_length || 'Unknown';
return $((`
<div class="flex-container alignItemsBaseline" title="${DOMPurify.sanitize(model.id)}">
<strong>${DOMPurify.sanitize(model.id)}</strong> | ${contextLength} ctx | <small>${price}</small>
</div>
`));
}
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 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 = openRouterSortBy(model_list, oai_settings.openrouter_sort_models);
$('#model_openrouter_select').empty();
if (true === oai_settings.openrouter_group_models) {
appendOpenRouterOptions(openRouterGroupByVendor(model_list), oai_settings.openrouter_group_models);
} else {
appendOpenRouterOptions(model_list);
}
$('#model_openrouter_select').val(oai_settings.openrouter_model).trigger('change');
}
if (oai_settings.chat_completion_source == chat_completion_sources.OPENAI) {
$('#openai_external_category').empty();
model_list.forEach((model) => {
$('#openai_external_category').append(
$('<option>', {
value: model.id,
text: model.id,
}));
});
// If the selected model is not in the list, revert to default
if (oai_settings.show_external_models) {
const model = model_list.findIndex((model) => model.id == oai_settings.openai_model) !== -1 ? oai_settings.openai_model : default_settings.openai_model;
$('#model_openai_select').val(model).trigger('change');
}
}
if (oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) {
$('.model_custom_select').empty();
$('.model_custom_select').append('<option value="">None</option>');
model_list.forEach((model) => {
$('.model_custom_select').append(
$('<option>', {
value: model.id,
text: model.id,
selected: model.id == oai_settings.custom_model,
}));
});
if (!oai_settings.custom_model && model_list.length > 0) {
$('#model_custom_select').val(model_list[0].id).trigger('change');
}
}
if (oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI) {
$('#model_aimlapi_select').empty();
const chatModels = model_list.filter(m => m.type === 'chat-completion');
appendAimlapiOptions(aimlapiGroupByVendor(chatModels));
if (!oai_settings.aimlapi_model && chatModels.length > 0) {
oai_settings.aimlapi_model = chatModels[0].id;
}
$('#model_aimlapi_select').val(oai_settings.aimlapi_model).trigger('change');
}
if (oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) {
$('#model_mistralai_select').empty();
for (const model of model_list.filter(model => model?.capabilities?.completion_chat)) {
$('#model_mistralai_select').append(new Option(model.id, model.id));
}
const selectedModel = model_list.find(model => model.id === oai_settings.mistralai_model);
if (!selectedModel) {
oai_settings.mistralai_model = model_list.find(model => model?.capabilities?.completion_chat)?.id;
}
$('#model_mistralai_select').val(oai_settings.mistralai_model).trigger('change');
}
if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
model_list = model_list.filter(model => model?.endpoints?.includes('/v1/chat/completions'));
model_list = electronHubSortBy(model_list, oai_settings.electronhub_sort_models);
$('#model_electronhub_select').empty();
const groupedList = oai_settings.electronhub_group_models ? electronHubGroupByVendor(model_list) : model_list;
appendElectronHubOptions(groupedList, oai_settings.electronhub_group_models);
const selectedModel = model_list.find(model => model.id === oai_settings.electronhub_model);
if (model_list.length > 0 && (!selectedModel || !oai_settings.electronhub_model)) {
oai_settings.electronhub_model = model_list[0].id;
}
$('#model_electronhub_select').val(oai_settings.electronhub_model).trigger('change');
}
if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES) {
model_list = model_list.filter(model => typeof model.id === 'string' && !model.id.toLowerCase().includes('affine'));
model_list = chutesSortBy(model_list, oai_settings.chutes_sort_models);
$('#model_chutes_select').empty();
for (const model of model_list) {
const option = $('<option>').val(model.id).text(model.id);
option.attr('data-model', JSON.stringify(model));
$('#model_chutes_select').append(option);
}
const selectedModel = model_list.find(model => model.id === oai_settings.chutes_model);
if (model_list.length > 0 && (!selectedModel || !oai_settings.chutes_model)) {
oai_settings.chutes_model = model_list[0].id;
}
$('#model_chutes_select').val(oai_settings.chutes_model).trigger('change');
}
if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) {
$('#model_nanogpt_select').empty();
model_list.forEach((model) => {
$('#model_nanogpt_select').append(
$('<option>', {
value: model.id,
text: model.id,
}));
});
const selectedModel = model_list.find(model => model.id === oai_settings.nanogpt_model);
if (model_list.length > 0 && (!selectedModel || !oai_settings.nanogpt_model)) {
oai_settings.nanogpt_model = model_list[0].id;
}
$('#model_nanogpt_select').val(oai_settings.nanogpt_model).trigger('change');
}
if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {
$('#model_deepseek_select').empty();
model_list.forEach((model) => {
$('#model_deepseek_select').append(
$('<option>', {
value: model.id,
text: model.id,
}));
});
const selectedModel = model_list.find(model => model.id === oai_settings.deepseek_model);
if (model_list.length > 0 && (!selectedModel || !oai_settings.deepseek_model)) {
oai_settings.deepseek_model = model_list[0].id;
}
$('#model_deepseek_select').val(oai_settings.deepseek_model).trigger('change');
}
if (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS) {
$('#model_pollinations_select').empty();
model_list.forEach((model) => {
$('#model_pollinations_select').append(
$('<option>', {
value: model.id,
text: model.id,
}));
});
const selectedModel = model_list.find(model => model.id === oai_settings.pollinations_model);
if (model_list.length > 0 && (!selectedModel || !oai_settings.pollinations_model)) {
oai_settings.pollinations_model = model_list[0].id;
}
$('#model_pollinations_select').val(oai_settings.pollinations_model).trigger('change');
}
if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {
// Clear only the "Other" optgroup for dynamic models
$('#google_other_models').empty();
// Get static model options that are already in the HTML
const staticModels = [];
$('#model_google_select option').each(function () {
staticModels.push($(this).val());
});
// Add dynamic models to the "Other" group
model_list.forEach((model) => {
// Only add if not already in static list
if (!staticModels.includes(model.id)) {
$('#google_other_models').append(
$('<option>', {
value: model.id,
text: model.id,
}));
}
});
// Merge static models into model_list
staticModels.forEach(modelId => {
if (!model_list.some(model => model.id === modelId)) {
model_list.push({ id: modelId });
}
});
const selectedModel = model_list.find(model => model.id === oai_settings.google_model);
if (model_list.length > 0 && (!selectedModel || !oai_settings.google_model)) {
oai_settings.google_model = model_list[0].id;
}
$('#model_google_select').val(oai_settings.google_model).trigger('change');
}
if (oai_settings.chat_completion_source === chat_completion_sources.GROQ) {
$('#model_groq_select').empty();
model_list.forEach((model) => {
$('#model_groq_select').append(
$('<option>', {
value: model.id,
text: model.id,
}));
});
const selectedModel = model_list.find(model => model.id === oai_settings.groq_model);
if (model_list.length > 0 && (!selectedModel || !oai_settings.groq_model)) {
oai_settings.groq_model = model_list[0].id;
}
$('#model_groq_select').val(oai_settings.groq_model).trigger('change');
}
if (oai_settings.chat_completion_source === chat_completion_sources.SILICONFLOW) {
$('#model_siliconflow_select').empty();
model_list.forEach((model) => {
$('#model_siliconflow_select').append(
$('<option>', {
value: model.id,
text: model.id,
}));
});
const selectedModel = model_list.find(model => model.id === oai_settings.siliconflow_model);
if (model_list.length > 0 && (!selectedModel || !oai_settings.siliconflow_model)) {
oai_settings.siliconflow_model = model_list[0].id;
}
$('#model_siliconflow_select').val(oai_settings.siliconflow_model).trigger('change');
}
if (oai_settings.chat_completion_source === chat_completion_sources.FIREWORKS) {
$('#model_fireworks_select').empty();
model_list.forEach((model) => {
if (!model?.supports_chat) {
return;
}
$('#model_fireworks_select').append(
$('<option>', {
value: model.id,
text: model.id,
}));
});
const selectedModel = model_list.find(model => model.id === oai_settings.fireworks_model);
if (model_list.length > 0 && (!selectedModel || !oai_settings.fireworks_model)) {
oai_settings.fireworks_model = model_list[0].id;
}
$('#model_fireworks_select').val(oai_settings.fireworks_model).trigger('change');
}
if (oai_settings.chat_completion_source === chat_completion_sources.COMETAPI) {
$('#model_cometapi_select').empty();
model_list.forEach((model) => {
const modelId = model.id.toLowerCase();
const isIgnoredModel = COMETAPI_IGNORE_PATTERNS.some(pattern => modelId.includes(pattern));
if (isIgnoredModel) {
return;
}
$('#model_cometapi_select').append(new Option(model.id, model.id));
});
const selectedModel = model_list.find(model => model.id === oai_settings.cometapi_model);
if (model_list.length > 0 && (!selectedModel || !oai_settings.cometapi_model)) {
oai_settings.cometapi_model = model_list[0].id;
saveSettingsDebounced();
}
$('#model_cometapi_select').val(oai_settings.cometapi_model).trigger('change');
}
if (oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI) {
const modelId = model_list?.[0]?.id || '';
oai_settings.azure_openai_model = modelId;
$('#azure_openai_model')
.empty()
.append(new Option(modelId || 'None', modelId || '', true, true))
.trigger('change');
}
if (oai_settings.chat_completion_source == chat_completion_sources.XAI) {
$('#model_xai_select').empty();
model_list.forEach((model) => {
$('#model_xai_select').append(
$('<option>', {
value: model.id,
text: model.id,
}));
});
const selectedModel = model_list.find(model => model.id === oai_settings.xai_model);
if (model_list.length > 0 && (!selectedModel || !oai_settings.xai_model)) {
oai_settings.xai_model = model_list[0].id;
}
$('#model_xai_select').val(oai_settings.xai_model).trigger('change');
}
if (oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT) {
$('#model_moonshot_select').empty();
model_list.forEach((model) => {
$('#model_moonshot_select').append(new Option(model.id, model.id));
});
const selectedModel = model_list.find(model => model.id === oai_settings.moonshot_model);
if (model_list.length > 0 && (!selectedModel || !oai_settings.moonshot_model)) {
oai_settings.moonshot_model = model_list[0].id;
}
$('#model_moonshot_select').val(oai_settings.moonshot_model).trigger('change');
}
}
function appendOpenRouterOptions(model_list, groupModels = false, sort = false) {
$('#model_openrouter_select').append($('<option>', { value: openrouter_website_model, text: t`Use OpenRouter website setting` }));
const appendOption = (model, parent = null) => {
(parent || $('#model_openrouter_select')).append(
$('<option>', {
value: model.id,
text: model.name,
}));
};
if (groupModels) {
model_list.forEach((models, vendor) => {
const optgroup = $(`<optgroup label="${vendor}">`);
models.forEach((model) => {
appendOption(model, optgroup);
});
$('#model_openrouter_select').append(optgroup);
});
} else {
model_list.forEach((model) => {
appendOption(model);
});
}
}
const openRouterSortBy = (data, property = 'alphabetically') => {
return data.sort((a, b) => {
if (property === 'context_length') {
return b.context_length - a.context_length;
} else if (property === 'pricing.prompt') {
return parseFloat(a.pricing.prompt) - parseFloat(b.pricing.prompt);
} else {
// Alphabetically
return a?.name && b?.name && a.name.localeCompare(b.name);
}
});
};
function openRouterGroupByVendor(array) {
return array.reduce((acc, curr) => {
const vendor = curr.id.split('/')[0];
if (!acc.has(vendor)) {
acc.set(vendor, []);
}
acc.get(vendor).push(curr);
return acc;
}, new Map());
}
function chutesSortBy(data, property = 'alphabetically') {
return data.sort((a, b) => {
if (property === 'context_length') {
return b.context_length - a.context_length;
} else if (property === 'pricing.input') {
const aPrice = parseFloat(a.pricing?.input || 0);
const bPrice = parseFloat(b.pricing?.input || 0);
return aPrice - bPrice;
} else if (property === 'pricing.output') {
const aPrice = parseFloat(a.pricing?.output || 0);
const bPrice = parseFloat(b.pricing?.output || 0);
return aPrice - bPrice;
} else {
return a?.id && b?.id && a.id.localeCompare(b.id);
}
});
}
function appendElectronHubOptions(model_list, groupModels = false) {
const appendOption = (model, parent = null) => {
(parent || $('#model_electronhub_select')).append(
$('<option>', {
value: model.id,
text: model.name,
}));
};
if (groupModels) {
model_list.forEach((models, vendor) => {
const optgroup = $('<optgroup>').attr('label', vendor);
models.forEach((model) => {
appendOption(model, optgroup);
});
$('#model_electronhub_select').append(optgroup);
});
} else {
model_list.forEach((model) => {
appendOption(model);
});
}
}
function electronHubSortBy(data, property = 'alphabetically') {
return data.sort((a, b) => {
if (property === 'context_length') {
return b.tokens - a.tokens;
} else if (property === 'pricing.input') {
return parseFloat(a.pricing.input) - parseFloat(b.pricing.input);
} else if (property === 'pricing.output') {
return parseFloat(a.pricing.output) - parseFloat(b.pricing.output);
} else {
return a?.name && b?.name && a.name.localeCompare(b.name);
}
});
}
function electronHubGroupByVendor(array) {
return array.reduce((acc, curr) => {
const vendor = String(curr?.name || curr?.id || 'Other').split(':')[0].trim() || 'Other';
if (!acc.has(vendor)) {
acc.set(vendor, []);
}
acc.get(vendor).push(curr);
return acc;
}, new Map());
}
function aimlapiGroupByVendor(array) {
return array.reduce((acc, curr) => {
const vendor = curr.info.developer;
if (!acc.has(vendor)) {
acc.set(vendor, []);
}
acc.get(vendor).push(curr);
return acc;
}, new Map());
}
function appendAimlapiOptions(model_list) {
const appendOption = (model, parent = null) => {
(parent || $('#model_aimlapi_select')).append(
$('<option>', {
value: model.id,
text: model.info?.name || model.name || model.id,
}));
};
model_list.forEach((models, vendor) => {
const optgroup = $(`<optgroup label="${vendor}">`);
models.forEach((model) => {
appendOption(model, optgroup);
});
$('#model_aimlapi_select').append(optgroup);
});
}
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 $((`
<div class="flex-container flexFlowColumn" title="${DOMPurify.sanitize(model.id)}">
<div><strong>${DOMPurify.sanitize(model.info?.name || model.name || model.id)}</strong> | ${vendor}</div>
</div>
`));
}
/**
* Get the reasoning effort from chat completion settings
* @param {ChatCompletionSettings} settings Chat completion settings
* @param {string} model Model name (optional, used for ElectronHub)
* @returns {string} Reasoning effort, if present
*/
function getReasoningEffort(settings = null, model = null) {
settings = settings ?? oai_settings;
model = model ?? getChatCompletionModel(settings);
// These sources expect the effort as string.
const reasoningEffortSources = [
chat_completion_sources.OPENAI,
chat_completion_sources.AZURE_OPENAI,
chat_completion_sources.CUSTOM,
chat_completion_sources.XAI,
chat_completion_sources.AIMLAPI,
chat_completion_sources.OPENROUTER,
chat_completion_sources.POLLINATIONS,
chat_completion_sources.PERPLEXITY,
chat_completion_sources.COMETAPI,
chat_completion_sources.ELECTRONHUB,
chat_completion_sources.CHUTES,
];
if (!reasoningEffortSources.includes(settings.chat_completion_source)) {
return settings.reasoning_effort;
}
function resolveReasoningEffort() {
switch (settings.reasoning_effort) {
case reasoning_effort_types.auto:
return undefined;
case reasoning_effort_types.min:
return [chat_completion_sources.OPENAI, chat_completion_sources.AZURE_OPENAI].includes(settings.chat_completion_source) && /^gpt-5/.test(model)
? reasoning_effort_types.min
: reasoning_effort_types.low;
case reasoning_effort_types.max:
return reasoning_effort_types.high;
default:
return settings.reasoning_effort;
}
}
const reasoningEffort = resolveReasoningEffort();
// Check if the resolved effort supported by the model
if (settings.chat_completion_source === chat_completion_sources.ELECTRONHUB) {
if (Array.isArray(model_list) && reasoningEffort) {
const currentModel = model_list.find(m => m.id === model);
const supportedEfforts = currentModel?.metadata?.supported_reasoning_efforts;
if (Array.isArray(supportedEfforts) && supportedEfforts.includes(reasoningEffort)) {
return reasoningEffort;
}
return undefined;
}
}
return reasoningEffort;
}
/**
* Get the verbosity from chat completion settings
* @param {ChatCompletionSettings} settings Chat completion settings
* @returns {string} Verbosity level, if present
*/
function getVerbosity(settings = null) {
settings = settings ?? oai_settings;
if (settings.verbosity === verbosity_levels.auto) {
return undefined;
}
// TODO: Adjust verbosity based on model capabilities
return settings.verbosity;
}
/**
* Build the generation parameter object for an OAI request.
* @param {ChatCompletionSettings} settings Initial chat completion settings
* @param {string} model Model name
* @param {string} type Request type (impersonate, quiet, continue, etc)
* @param {ChatCompletionMessage[]} messages Array of chat completion messages
* @param {import('../script.js').AdditionalRequestOptions} options Additional request options
* @returns {Promise<object>} Final generation parameters object appropriate for the chat completion source
*/
export async function createGenerationParameters(settings, model, type, messages, { jsonSchema = null } = {}) {
// HACK: Filter out null and non-object messages
if (!Array.isArray(messages)) {
throw new Error('messages must be an array');
}
messages = messages.filter(msg => msg && typeof msg === 'object');
// "OpenAI-like" sources
const gptSources = [
chat_completion_sources.OPENAI,
chat_completion_sources.AZURE_OPENAI,
chat_completion_sources.OPENROUTER,
];
// Sources that support the "seed" parameter
const seedSupportedSources = [
chat_completion_sources.OPENAI,
chat_completion_sources.AZURE_OPENAI,
chat_completion_sources.OPENROUTER,
chat_completion_sources.MISTRALAI,
chat_completion_sources.CUSTOM,
chat_completion_sources.COHERE,
chat_completion_sources.GROQ,
chat_completion_sources.ELECTRONHUB,
chat_completion_sources.NANOGPT,
chat_completion_sources.XAI,
chat_completion_sources.POLLINATIONS,
chat_completion_sources.AIMLAPI,
chat_completion_sources.VERTEXAI,
chat_completion_sources.MAKERSUITE,
chat_completion_sources.CHUTES,
];
// Sources that support proxying
const proxySupportedSources = [
chat_completion_sources.CLAUDE,
chat_completion_sources.OPENAI,
chat_completion_sources.MISTRALAI,
chat_completion_sources.MAKERSUITE,
chat_completion_sources.VERTEXAI,
chat_completion_sources.DEEPSEEK,
chat_completion_sources.XAI,
];
// Sources that support logprobs
const logprobsSupportedSources = [
chat_completion_sources.OPENAI,
chat_completion_sources.AZURE_OPENAI,
chat_completion_sources.CUSTOM,
chat_completion_sources.DEEPSEEK,
chat_completion_sources.XAI,
chat_completion_sources.AIMLAPI,
chat_completion_sources.CHUTES,
];
// Sources that support logit bias
const logitBiasSources = [
chat_completion_sources.OPENAI,
chat_completion_sources.AZURE_OPENAI,
chat_completion_sources.OPENROUTER,
chat_completion_sources.ELECTRONHUB,
chat_completion_sources.CHUTES,
chat_completion_sources.CUSTOM,
];
// Sources that support "n" parameter for multi-swipe
const multiswipeSources = [
chat_completion_sources.OPENAI,
chat_completion_sources.AZURE_OPENAI,
chat_completion_sources.CUSTOM,
chat_completion_sources.XAI,
chat_completion_sources.AIMLAPI,
chat_completion_sources.MOONSHOT,
];
const isO1 = gptSources.includes(settings.chat_completion_source) && ['o1-2024-12-17', 'o1'].includes(model);
const stream = settings.stream_openai && type !== 'quiet' && !isO1;
const noMultiSwipeTypes = ['quiet', 'impersonate', 'continue'];
const canMultiSwipe = settings.n > 1 && !noMultiSwipeTypes.includes(type) && multiswipeSources.includes(settings.chat_completion_source);
let logit_bias = {};
if (settings.bias_preset_selected
&& logitBiasSources.includes(settings.chat_completion_source)
&& Array.isArray(settings.bias_presets[settings.bias_preset_selected])
&& settings.bias_presets[settings.bias_preset_selected].length) {
logit_bias = biasCache || await calculateLogitBias();
biasCache = logit_bias;
}
if (Object.keys(logit_bias).length === 0) {
logit_bias = undefined;
}
const generate_data = {
'type': type,
'messages': messages,
'model': model,
'temperature': Number(settings.temp_openai),
'frequency_penalty': Number(settings.freq_pen_openai),
'presence_penalty': Number(settings.pres_pen_openai),
'top_p': Number(settings.top_p_openai),
'max_tokens': settings.openai_max_tokens,
'stream': stream,
'logit_bias': logit_bias,
'stop': getCustomStoppingStrings(openai_max_stop_strings),
'chat_completion_source': settings.chat_completion_source,
'n': canMultiSwipe ? settings.n : undefined,
'user_name': name1,
'char_name': name2,
'group_names': getGroupNames(),
'include_reasoning': Boolean(settings.show_thoughts),
'reasoning_effort': getReasoningEffort(settings, model),
'enable_web_search': Boolean(settings.enable_web_search),
'request_images': Boolean(settings.request_images),
'request_image_resolution': String(settings.request_image_resolution),
'request_image_aspect_ratio': String(settings.request_image_aspect_ratio),
'custom_prompt_post_processing': settings.custom_prompt_post_processing,
'verbosity': getVerbosity(settings),
};
if (settings.chat_completion_source === chat_completion_sources.AZURE_OPENAI) {
generate_data.azure_base_url = settings.azure_base_url;
generate_data.azure_deployment_name = settings.azure_deployment_name;
generate_data.azure_api_version = settings.azure_api_version;
// Reasoning effort is not supported on some Azure models (e.g. GPT-3.x, GPT-4.x)
if (/^gpt-[34]/.test(model)) {
delete generate_data.reasoning_effort;
}
}
if (!canMultiSwipe && ToolManager.canPerformToolCalls(type, settings, model)) {
await ToolManager.registerFunctionToolsOpenAI(generate_data);
}
// Empty array will produce a validation error
if (!Array.isArray(generate_data.stop) || !generate_data.stop.length) {
delete generate_data.stop;
}
if (settings.reverse_proxy && proxySupportedSources.includes(settings.chat_completion_source)) {
await validateReverseProxy();
generate_data['reverse_proxy'] = settings.reverse_proxy;
generate_data['proxy_password'] = settings.proxy_password;
}
// Add logprobs request (max 5 per OpenAI docs)
const useLogprobs = !!power_user.request_token_probabilities;
if (useLogprobs && logprobsSupportedSources.includes(settings.chat_completion_source)) {
generate_data['logprobs'] = 5;
}
// Remove logit bias/logprobs/stop-strings if not supported by the model
const isVision = (m) => ['gpt', 'vision'].every(x => typeof m === 'string' && m.includes(x));
if (gptSources.includes(settings.chat_completion_source) && isVision(model)) {
delete generate_data.logit_bias;
delete generate_data.stop;
delete generate_data.logprobs;
}
if (gptSources.includes(settings.chat_completion_source) && /gpt-4.5/.test(model)) {
delete generate_data.logprobs;
}
if (settings.chat_completion_source === chat_completion_sources.CLAUDE) {
generate_data['top_k'] = Number(settings.top_k_openai);
generate_data['use_sysprompt'] = settings.use_sysprompt;
generate_data['stop'] = getCustomStoppingStrings(); // Claude shouldn't have limits on stop strings.
// Don't add a prefill on quiet gens (summarization) and when using continue prefill.
if (type !== 'quiet' && !(type === 'continue' && settings.continue_prefill)) {
generate_data['assistant_prefill'] = type === 'impersonate'
? substituteParams(settings.assistant_impersonation)
: substituteParams(settings.assistant_prefill);
}
}
if (settings.chat_completion_source === chat_completion_sources.OPENROUTER) {
generate_data['top_k'] = Number(settings.top_k_openai);
generate_data['min_p'] = Number(settings.min_p_openai);
generate_data['repetition_penalty'] = Number(settings.repetition_penalty_openai);
generate_data['top_a'] = Number(settings.top_a_openai);
generate_data['use_fallback'] = settings.openrouter_use_fallback;
generate_data['provider'] = settings.openrouter_providers;
generate_data['allow_fallbacks'] = settings.openrouter_allow_fallbacks;
generate_data['middleout'] = settings.openrouter_middleout;
}
if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(settings.chat_completion_source)) {
const stopStringsLimit = 5;
generate_data['top_k'] = Number(settings.top_k_openai);
generate_data['stop'] = getCustomStoppingStrings(stopStringsLimit).slice(0, stopStringsLimit).filter(x => x.length >= 1 && x.length <= 16);
generate_data['use_sysprompt'] = settings.use_sysprompt;
if (settings.chat_completion_source === chat_completion_sources.VERTEXAI) {
generate_data['vertexai_auth_mode'] = settings.vertexai_auth_mode;
generate_data['vertexai_region'] = settings.vertexai_region;
generate_data['vertexai_express_project_id'] = settings.vertexai_express_project_id;
}
}
if (settings.chat_completion_source === chat_completion_sources.MISTRALAI) {
generate_data['safe_prompt'] = false; // already defaults to false, but just incase they change that in the future.
generate_data['stop'] = getCustomStoppingStrings(); // Mistral shouldn't have limits on stop strings.
}
if (settings.chat_completion_source === chat_completion_sources.CUSTOM) {
generate_data['custom_url'] = settings.custom_url;
generate_data['custom_include_body'] = settings.custom_include_body;
generate_data['custom_exclude_body'] = settings.custom_exclude_body;
generate_data['custom_include_headers'] = settings.custom_include_headers;
}
if (settings.chat_completion_source === chat_completion_sources.COHERE) {
// Clamp to 0.01 -> 0.99
generate_data['top_p'] = Math.min(Math.max(Number(settings.top_p_openai), 0.01), 0.99);
generate_data['top_k'] = Number(settings.top_k_openai);
// Clamp to 0 -> 1
generate_data['frequency_penalty'] = Math.min(Math.max(Number(settings.freq_pen_openai), 0), 1);
generate_data['presence_penalty'] = Math.min(Math.max(Number(settings.pres_pen_openai), 0), 1);
generate_data['stop'] = getCustomStoppingStrings(5);
}
if (settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
generate_data['top_k'] = Number(settings.top_k_openai);
generate_data['frequency_penalty'] = Number(settings.freq_pen_openai);
generate_data['presence_penalty'] = Number(settings.pres_pen_openai);
delete generate_data['stop'];
}
// https://console.groq.com/docs/openai
if (settings.chat_completion_source === chat_completion_sources.GROQ) {
delete generate_data.logprobs;
delete generate_data.logit_bias;
delete generate_data.top_logprobs;
delete generate_data.n;
}
// https://api-docs.deepseek.com/api/create-chat-completion
if (settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
generate_data.top_p = generate_data.top_p || Number.EPSILON;
}
if (settings.chat_completion_source === chat_completion_sources.XAI) {
if (model.includes('grok-3-mini')) {
delete generate_data.presence_penalty;
delete generate_data.frequency_penalty;
delete generate_data.stop;
} else {
// As of 2025/09/21, only grok-3-mini accepts reasoning_effort
delete generate_data.reasoning_effort;
}
if (model.includes('grok-4') || model.includes('grok-code')) {
delete generate_data.presence_penalty;
delete generate_data.frequency_penalty;
// grok-4-fast-non-reasoning accepts stop
if (!model.includes('grok-4-fast-non-reasoning')) {
delete generate_data.stop;
}
}
}
if (settings.chat_completion_source === chat_completion_sources.POLLINATIONS) {
delete generate_data.max_tokens;
}
// https://docs.electronhub.ai/api-reference/chat/completions
if (settings.chat_completion_source === chat_completion_sources.ELECTRONHUB) {
generate_data['top_k'] = Number(settings.top_k_openai);
}
if (settings.chat_completion_source === chat_completion_sources.CHUTES) {
generate_data['min_p'] = Number(settings.min_p_openai);
generate_data['top_k'] = settings.top_k_openai > 0 ? Number(settings.top_k_openai) : undefined;
generate_data['repetition_penalty'] = Number(settings.repetition_penalty_openai);
generate_data['stop'] = getCustomStoppingStrings();
}
// https://docs.z.ai/api-reference/llm/chat-completion
if (settings.chat_completion_source === chat_completion_sources.ZAI) {
generate_data['top_p'] = generate_data.top_p || 0.01;
generate_data['stop'] = getCustomStoppingStrings(1);
generate_data['zai_endpoint'] = settings.zai_endpoint || ZAI_ENDPOINT.COMMON;
delete generate_data.presence_penalty;
delete generate_data.frequency_penalty;
}
// https://docs.nano-gpt.com/api-reference/endpoint/chat-completion#temperature-&-nucleus
if (settings.chat_completion_source === chat_completion_sources.NANOGPT) {
generate_data['top_k'] = Number(settings.top_k_openai);
generate_data['min_p'] = Number(settings.min_p_openai);
generate_data['repetition_penalty'] = Number(settings.repetition_penalty_openai);
generate_data['top_a'] = Number(settings.top_a_openai);
}
if (seedSupportedSources.includes(settings.chat_completion_source) && settings.seed >= 0) {
generate_data['seed'] = settings.seed;
}
if ([chat_completion_sources.OPENAI, chat_completion_sources.AZURE_OPENAI].includes(settings.chat_completion_source) && /^(o1|o3|o4)/.test(model) ||
(chat_completion_sources.OPENROUTER === settings.chat_completion_source && /^openai\/(o1|o3|o4)/.test(model))) {
generate_data.max_completion_tokens = generate_data.max_tokens;
delete generate_data.max_tokens;
delete generate_data.logprobs;
delete generate_data.top_logprobs;
delete generate_data.stop;
delete generate_data.logit_bias;
delete generate_data.temperature;
delete generate_data.top_p;
delete generate_data.frequency_penalty;
delete generate_data.presence_penalty;
if (/^(openai\/)?(o1)/.test(model)) {
generate_data.messages.forEach((msg) => {
if (msg.role === 'system') {
msg.role = 'user';
}
});
delete generate_data.n;
delete generate_data.tools;
delete generate_data.tool_choice;
}
}
if (gptSources.includes(settings.chat_completion_source) && /gpt-5/.test(model)) {
generate_data.max_completion_tokens = generate_data.max_tokens;
delete generate_data.max_tokens;
delete generate_data.logprobs;
delete generate_data.top_logprobs;
if (/gpt-5-chat-latest/.test(model)) {
delete generate_data.tools;
delete generate_data.tool_choice;
} else if (/gpt-5.(1|2)/.test(model) && !/chat-latest/.test(model)) {
delete generate_data.frequency_penalty;
delete generate_data.presence_penalty;
delete generate_data.logit_bias;
delete generate_data.stop;
} else {
delete generate_data.temperature;
delete generate_data.top_p;
delete generate_data.frequency_penalty;
delete generate_data.presence_penalty;
delete generate_data.logit_bias;
delete generate_data.stop;
}
}
if (jsonSchema) {
generate_data.json_schema = jsonSchema;
}
return { generate_data, stream, canMultiSwipe };
}
/**
* Send a chat completion request to backend
* @param {string} type Request type (impersonate, quiet, continue, etc)
* @param {ChatCompletionMessage[]} messages Array of chat completion messages
* @param {AbortSignal?} signal Abort signal for request cancellation
* @param {import('../script.js').AdditionalRequestOptions} options Additional request options
* @returns {Promise<unknown>}
* @throws {Error}
*/
async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } = {}) {
// Provide default abort signal
if (!signal) {
signal = new AbortController().signal;
}
const model = getChatCompletionModel(oai_settings);
const { generate_data, stream, canMultiSwipe } = await createGenerationParameters(oai_settings, model, type, messages, { jsonSchema });
await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
const generate_url = '/api/backends/chat-completions/generate';
const response = await fetch(generate_url, {
method: 'POST',
body: JSON.stringify(generate_data),
headers: getRequestHeaders(),
signal: signal,
});
if (!response.ok) {
tryParseStreamingError(response, await response.text());
throw new Error(`Got response status ${response.status}`);
}
if (stream) {
const eventStream = getEventSourceStream();
response.body.pipeThrough(eventStream);
const reader = eventStream.readable.getReader();
return async function* streamData() {
let text = '';
const swipes = [];
const toolCalls = [];
const state = { reasoning: '', images: [], signature: '', toolSignatures: {} };
while (true) {
const { done, value } = await reader.read();
if (done) return;
const rawData = value.data;
if (rawData === '[DONE]') return;
tryParseStreamingError(response, rawData);
const parsed = JSON.parse(rawData);
if (canMultiSwipe && Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) {
const swipeIndex = parsed.choices[0].index - 1;
// FIXME: state.reasoning should be an array to support multi-swipe
swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed, state, { overrideShowThoughts: false });
} else {
text += getStreamingReply(parsed, state);
}
ToolManager.parseToolCalls(toolCalls, parsed, state.toolSignatures);
yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls, state: state };
}
};
}
else {
const data = await response.json();
checkQuotaError(data);
checkModerationError(data);
if (data.error) {
const message = data.error.message || response.statusText || t`Unknown error`;
toastr.error(message, t`API returned an error`);
throw new Error(message);
}
if (type !== 'quiet') {
const logprobs = parseChatCompletionLogprobs(data);
// Delay is required to allow the active message to be updated to
// the one we are generating (happens right after sendOpenAIRequest)
delay(1).then(() => saveLogprobsForActiveMessage(logprobs, null));
}
return data;
}
}
/**
* Extracts the reply from the response data from a chat completions-like source
* @param {object} data Response data from the chat completions-like source
* @param {object} state Additional state to keep track of
* @param {object} [options] Additional options
* @param {string?} [options.chatCompletionSource] Chat completion source
* @param {boolean?} [options.overrideShowThoughts] Override show thoughts
* @returns {string} The reply extracted from the response data
*/
export function getStreamingReply(data, state, { chatCompletionSource = null, overrideShowThoughts = null } = {}) {
const chat_completion_source = chatCompletionSource ?? oai_settings.chat_completion_source;
const show_thoughts = overrideShowThoughts ?? oai_settings.show_thoughts;
if (chat_completion_source === chat_completion_sources.CLAUDE) {
if (show_thoughts) {
state.reasoning += data?.delta?.thinking || '';
}
return data?.delta?.text || '';
} else if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(chat_completion_source)) {
const inlineData = data?.candidates?.[0]?.content?.parts?.filter(x => x.inlineData && !x.thought)?.map(x => x.inlineData) || [];
if (Array.isArray(inlineData) && inlineData.length > 0) {
state.images.push(...inlineData.map(x => `data:${x.mimeType};base64,${x.data}`).filter(isDataURL));
}
if (show_thoughts) {
state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || '');
}
// Extract thought signatures from streaming chunks (typically in final chunk)
const parts = data?.candidates?.[0]?.content?.parts || [];
parts.forEach((part) => {
if (part.thoughtSignature && typeof part.text === 'string') {
state.signature = part.thoughtSignature;
}
});
return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || '';
} else if (chat_completion_source === chat_completion_sources.COHERE) {
return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';
} else if (chat_completion_source === chat_completion_sources.DEEPSEEK) {
if (show_thoughts) {
state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
}
return data.choices?.[0]?.delta?.content || '';
} else if (chat_completion_source === chat_completion_sources.XAI) {
if (show_thoughts) {
state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
}
return data.choices?.[0]?.delta?.content || '';
} else if (chat_completion_source === chat_completion_sources.OPENROUTER) {
const imageUrls = data?.choices?.[0]?.delta?.images?.filter(x => x.type === 'image_url')?.map(x => x?.image_url?.url) || [];
if (Array.isArray(imageUrls) && imageUrls.length > 0) {
state.images.push(...imageUrls.filter(isDataURL));
}
if (show_thoughts) {
state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
}
// Extract thought signatures from OpenRouter streaming (reasoning_details in delta)
const reasoningDetails = data?.choices?.[0]?.delta?.reasoning_details || [];
reasoningDetails.forEach((detail) => {
if (detail.type === 'reasoning.encrypted' && detail.data) {
if (/^tool_/.test(detail.id)) {
state.toolSignatures[detail.id] = detail.data;
} else {
state.signature = detail.data;
}
}
});
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
} else if ([chat_completion_sources.CUSTOM, chat_completion_sources.POLLINATIONS, chat_completion_sources.AIMLAPI, chat_completion_sources.MOONSHOT, chat_completion_sources.COMETAPI, chat_completion_sources.ELECTRONHUB, chat_completion_sources.NANOGPT, chat_completion_sources.ZAI, chat_completion_sources.SILICONFLOW, chat_completion_sources.CHUTES].includes(chat_completion_source)) {
if (show_thoughts) {
state.reasoning +=
data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning ??
'';
}
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
} else if (chat_completion_source === chat_completion_sources.MISTRALAI) {
if (show_thoughts) {
state.reasoning += (data.choices?.filter(x => x?.delta?.content?.[0]?.thinking)?.[0]?.delta?.content?.[0]?.thinking?.[0]?.text || '');
}
const content = data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
return Array.isArray(content) ? content.map(x => x.text).filter(x => x).join('') : content;
} else {
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
}
}
/**
* parseChatCompletionLogprobs converts the response data returned from a chat
* completions-like source into an array of TokenLogprobs found in the response.
* @param {Object} data - response data from a chat completions-like source
* @returns {import('./logprobs.js').TokenLogprobs[] | null} converted logprobs
*/
function parseChatCompletionLogprobs(data) {
if (!data) {
return null;
}
switch (oai_settings.chat_completion_source) {
case chat_completion_sources.AIMLAPI:
return Object.keys(data?.choices?.[0]?.logprobs ?? {}).includes('content')
? parseOpenAIChatLogprobs(data.choices[0]?.logprobs)
: parseOpenAITextLogprobs(data.choices[0]?.logprobs);
case chat_completion_sources.OPENAI:
case chat_completion_sources.AZURE_OPENAI:
case chat_completion_sources.DEEPSEEK:
case chat_completion_sources.XAI:
case chat_completion_sources.CUSTOM:
case chat_completion_sources.CHUTES:
if (!data.choices?.length) {
return null;
}
// OpenAI Text Completion API is treated as a chat completion source
// by SillyTavern, hence its presence in this function.
return textCompletionModels.includes(getChatCompletionModel())
? parseOpenAITextLogprobs(data.choices[0]?.logprobs)
: parseOpenAIChatLogprobs(data.choices[0]?.logprobs);
default:
// implement other chat completion sources here
}
return null;
}
/**
* parseOpenAIChatLogprobs receives a `logprobs` response from OpenAI's chat
* completion API and converts into the structure used by the Token Probabilities
* view.
* @param {{content: { token: string, logprob: number, top_logprobs: { token: string, logprob: number }[] }[]}} logprobs
* @returns {import('./logprobs.js').TokenLogprobs[] | null} converted logprobs
*/
function parseOpenAIChatLogprobs(logprobs) {
const { content } = logprobs ?? {};
if (!Array.isArray(content)) {
return null;
}
/** @type {(x: { token: string, logprob: number }) => [string, number]} */
const toTuple = (x) => [x.token, x.logprob];
return content.map(({ token, logprob, top_logprobs = [] }) => {
// Add the chosen token to top_logprobs if it's not already there, then
// convert to a list of [token, logprob] pairs
const chosenTopToken = top_logprobs.some((top) => token === top.token);
/** @type {import('./logprobs.js').Candidate[]} */
const topLogprobs = chosenTopToken
? top_logprobs.map(toTuple)
: [...top_logprobs.map(toTuple), [token, logprob]];
return { token, topLogprobs };
});
}
/**
* parseOpenAITextLogprobs receives a `logprobs` response from OpenAI's text
* completion API and converts into the structure used by the Token Probabilities
* view.
* @param {{tokens: string[], token_logprobs: number[], top_logprobs: { token: string, logprob: number }[][]}} logprobs
* @returns {import('./logprobs.js').TokenLogprobs[] | null} converted logprobs
*/
function parseOpenAITextLogprobs(logprobs) {
const { tokens, token_logprobs, top_logprobs } = logprobs ?? {};
if (!Array.isArray(tokens)) {
return null;
}
return tokens.map((token, i) => {
// Add the chosen token to top_logprobs if it's not already there, then
// convert to a list of [token, logprob] pairs
/** @type {any[]} */
const topLogprobs = top_logprobs[i] ? Object.entries(top_logprobs[i]) : [];
const chosenTopToken = topLogprobs.some(([topToken]) => token === topToken);
if (!chosenTopToken) {
topLogprobs.push([token, token_logprobs[i]]);
}
return { token, topLogprobs };
});
}
async function calculateLogitBias() {
const body = JSON.stringify(oai_settings.bias_presets[oai_settings.bias_preset_selected]);
let result = {};
try {
const reply = await fetch(`/api/backends/chat-completions/bias?model=${getTokenizerModel()}`, {
method: 'POST',
headers: getRequestHeaders(),
body,
});
result = await reply.json();
}
catch (err) {
result = {};
console.error(err);
}
return result;
}
class TokenHandler {
/**
* @param {(messages: object[] | object, full?: boolean) => Promise<number>} countTokenAsyncFn Function to count tokens
*/
constructor(countTokenAsyncFn) {
this.countTokenAsyncFn = countTokenAsyncFn;
this.counts = {
'start_chat': 0,
'prompt': 0,
'bias': 0,
'nudge': 0,
'jailbreak': 0,
'impersonate': 0,
'examples': 0,
'conversation': 0,
};
}
getCounts() {
return this.counts;
}
resetCounts() {
Object.keys(this.counts).forEach((key) => this.counts[key] = 0);
}
setCounts(counts) {
this.counts = counts;
}
uncount(value, type) {
this.counts[type] -= value;
}
/**
* Count tokens for a message or messages.
* @param {object|any[]} messages Messages to count tokens for
* @param {boolean} [full] Count full tokens
* @param {string} [type] Identifier for the token count
* @returns {Promise<number>} The token count
*/
async countAsync(messages, full, type) {
const token_count = await this.countTokenAsyncFn(messages, full);
this.counts[type] += token_count;
return token_count;
}
getTokensForIdentifier(identifier) {
return this.counts[identifier] ?? 0;
}
getTotal() {
return Object.values(this.counts).reduce((a, b) => a + (isNaN(b) ? 0 : b), 0);
}
log() {
console.table({ ...this.counts, 'total': this.getTotal() });
}
}
const tokenHandler = new TokenHandler(countTokensOpenAIAsync);
// Thrown by ChatCompletion when a requested prompt couldn't be found.
class IdentifierNotFoundError extends Error {
constructor(identifier) {
super(`Identifier ${identifier} not found.`);
this.name = 'IdentifierNotFoundError';
}
}
// Thrown by ChatCompletion when the token budget is unexpectedly exceeded
class TokenBudgetExceededError extends Error {
constructor(identifier = '') {
super(`Token budged exceeded. Message: ${identifier}`);
this.name = 'TokenBudgetExceeded';
}
}
// Thrown when a character name is invalid
class InvalidCharacterNameError extends Error {
constructor(identifier = '') {
super(`Invalid character name. Message: ${identifier}`);
this.name = 'InvalidCharacterName';
}
}
/**
* Used for creating, managing, and interacting with a specific message object.
*/
class Message {
static tokensPerImage = 85;
/** @type {number} */
tokens;
/** @type {string} */
identifier;
/** @type {string} */
role;
/** @type {string|any[]} */
content;
/** @type {string} */
name;
/** @type {object} */
tool_call = null;
/** @type {string?} */
signature = null;
/**
* @constructor
* @param {string} role - The role of the entity creating the message.
* @param {string} content - The actual content of the message.
* @param {string} identifier - A unique identifier for the message.
* @private Don't use this constructor directly. Use createAsync instead.
*/
constructor(role, content, identifier) {
this.identifier = identifier;
this.role = role;
this.content = content;
if (!this.role) {
console.log(`Message role not set, defaulting to 'system' for identifier '${this.identifier}'`);
this.role = 'system';
}
this.tokens = 0;
}
/**
* Create a new Message instance.
* @param {string} role
* @param {string} content
* @param {string} identifier
* @returns {Promise<Message>} Message instance
*/
static async createAsync(role, content, identifier) {
const message = new Message(role, content, identifier);
if (typeof message.content === 'string' && message.content.length > 0) {
message.tokens = await tokenHandler.countAsync({ role: message.role, content: message.content });
}
return message;
}
/**
* Reconstruct the message from a tool invocation.
* @param {import('./tool-calling.js').ToolInvocation[]} invocations - The tool invocations to reconstruct the message from.
* @param {boolean} includeSignature Whether to include the signature in the tool calls.
* @returns {Promise<void>}
*/
async setToolCalls(invocations, includeSignature) {
this.tool_calls = invocations.map(i => ({
id: i.id,
type: 'function',
function: {
arguments: i.parameters,
name: i.name,
},
...(includeSignature && i.signature ? { signature: i.signature } : {}),
}));
this.tokens = await tokenHandler.countAsync({ role: this.role, tool_calls: JSON.stringify(this.tool_calls) });
}
/**
* Add a name to the message.
* @param {string} name Name to set for the message.
* @returns {Promise<void>}
*/
async setName(name) {
this.name = name;
this.tokens = await tokenHandler.countAsync({ role: this.role, content: this.content, name: this.name });
}
/**
* Ensures the content is an array. If it's a string, converts it to an array with a single text object.
* @returns {any[]} Content as an array
*/
ensureContentIsArray() {
const textContent = this.content;
if (!Array.isArray(this.content)) {
this.content = [];
if (typeof textContent === 'string') {
this.content.push({ type: 'text', text: textContent });
}
}
return this.content;
}
/**
* Adds an image to the message.
* @param {string} image Image URL or Data URL.
* @returns {Promise<void>}
*/
async addImage(image) {
this.content = this.ensureContentIsArray();
const isDataUrl = isDataURL(image);
if (!isDataUrl) {
try {
const response = await fetch(image, { method: 'GET', cache: 'force-cache' });
if (!response.ok) throw new Error('Failed to fetch image');
const blob = await response.blob();
image = await getBase64Async(blob);
} catch (error) {
console.error('Image adding skipped', error);
return;
}
}
image = await this.compressImage(image);
const quality = oai_settings.inline_image_quality || default_settings.inline_image_quality;
this.content.push({ type: 'image_url', image_url: { 'url': image, 'detail': quality } });
try {
const tokens = await this.getImageTokenCost(image, quality);
this.tokens += tokens;
} catch (error) {
this.tokens += Message.tokensPerImage;
console.error('Failed to get image token cost', error);
}
}
/**
* Adds a video to the message.
* @param {string} video Video URL or Data URL.
* @returns {Promise<void>}
*/
async addVideo(video) {
this.content = this.ensureContentIsArray();
const isDataUrl = isDataURL(video);
if (!isDataUrl) {
try {
const response = await fetch(video, { method: 'GET', cache: 'force-cache' });
if (!response.ok) throw new Error('Failed to fetch video');
const blob = await response.blob();
video = await getBase64Async(blob);
} catch (error) {
console.error('Video adding skipped', error);
return;
}
}
// Note: No compression for videos (unlike images)
const quality = oai_settings.inline_image_quality || default_settings.inline_image_quality;
this.content.push({ type: 'video_url', video_url: { 'url': video, 'detail': quality } });
try {
// Using Gemini calculation (263 tokens per second)
const duration = await getVideoDurationFromDataURL(video);
this.tokens += 263 * Math.ceil(duration);
} catch (error) {
// Convservative estimate for video token cost without knowing duration
this.tokens += 263 * 40; // ~40 second video (60 seconds max)
console.error('Failed to get video token cost', error);
}
}
/**
* Adds a audio to the message.
* @param {string} audio Audio URL or Data URL.
* @returns {Promise<void>}
*/
async addAudio(audio) {
this.content = this.ensureContentIsArray();
const isDataUrl = isDataURL(audio);
if (!isDataUrl) {
try {
const response = await fetch(audio, { method: 'GET', cache: 'force-cache' });
if (!response.ok) throw new Error('Failed to fetch audio');
const blob = await response.blob();
audio = await getBase64Async(blob);
} catch (error) {
console.error('Audio adding skipped', error);
return;
}
}
this.content.push({ type: 'audio_url', audio_url: { 'url': audio } });
try {
// Using Gemini calculation (32 tokens per second)
const duration = await getAudioDurationFromDataURL(audio);
this.tokens += 32 * Math.ceil(duration);
} catch (error) {
// Estimate for audio token cost without knowing duration
const tokens = 32 * 300; // ~5 minute audio
this.tokens += tokens;
console.error('Failed to get audio token cost', error);
}
}
/**
* Compress an image if it exceeds the size threshold for the current chat completion source.
* @param {string} image Data URL of the image.
* @returns {Promise<string>} Compressed image as a Data URL.
*/
async compressImage(image) {
const compressImageSources = [
chat_completion_sources.OPENROUTER,
chat_completion_sources.MAKERSUITE,
chat_completion_sources.MISTRALAI,
chat_completion_sources.VERTEXAI,
];
const sizeThreshold = 2 * 1024 * 1024;
const dataSize = image.length * 0.75;
const safeMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
const mimeType = image?.split(';')?.[0]?.split(':')?.[1];
if (compressImageSources.includes(oai_settings.chat_completion_source) && dataSize > sizeThreshold) {
const maxSide = 2048;
image = await createThumbnail(image, maxSide, maxSide);
} else if (!safeMimeTypes.includes(mimeType)) {
image = await createThumbnail(image, null, null);
}
return image;
}
/**
* Get the token cost of an image.
* @param {string} dataUrl Data URL of the image.
* @param {string} quality String representing the quality of the image. Can be 'low', 'auto', or 'high'.
* @returns {Promise<number>} The token cost of the image.
*/
async getImageTokenCost(dataUrl, quality) {
if (quality === 'low') {
return Message.tokensPerImage;
}
const size = await getImageSizeFromDataURL(dataUrl);
// If the image is small enough, we can use the low quality token cost
if (quality === 'auto' && size.width <= 512 && size.height <= 512) {
return Message.tokensPerImage;
}
/*
* Images are first scaled to fit within a 2048 x 2048 square, maintaining their aspect ratio.
* Then, they are scaled such that the shortest side of the image is 768px long.
* Finally, we count how many 512px squares the image consists of.
* Each of those squares costs 170 tokens. Another 85 tokens are always added to the final total.
* https://platform.openai.com/docs/guides/vision/calculating-costs
*/
const scale = 2048 / Math.min(size.width, size.height);
const scaledWidth = Math.round(size.width * scale);
const scaledHeight = Math.round(size.height * scale);
const finalScale = 768 / Math.min(scaledWidth, scaledHeight);
const finalWidth = Math.round(scaledWidth * finalScale);
const finalHeight = Math.round(scaledHeight * finalScale);
const squares = Math.ceil(finalWidth / 512) * Math.ceil(finalHeight / 512);
const tokens = squares * 170 + 85;
return tokens;
}
/**
* Create a new Message instance from a prompt asynchronously.
* @static
* @param {Object} prompt - The prompt object.
* @returns {Promise<Message>} A new instance of Message.
*/
static fromPromptAsync(prompt) {
return Message.createAsync(prompt.role, prompt.content, prompt.identifier);
}
/**
* Returns the number of tokens in the message.
* @returns {number} Number of tokens in the message.
*/
getTokens() { return this.tokens; }
}
/**
* Used for creating, managing, and interacting with a collection of Message instances.
*
* @class MessageCollection
*/
class MessageCollection {
collection = [];
identifier;
/**
* @constructor
* @param {string} identifier - A unique identifier for the MessageCollection.
* @param {...Object} items - An array of Message or MessageCollection instances to be added to the collection.
*/
constructor(identifier, ...items) {
for (let item of items) {
if (!(item instanceof Message || item instanceof MessageCollection)) {
throw new Error('Only Message and MessageCollection instances can be added to MessageCollection');
}
}
this.collection.push(...items);
this.identifier = identifier;
}
/**
* Get chat in the format of {role, name, content, tool_calls}.
* @returns {Array} Array of objects with role, name, and content properties.
*/
getChat() {
return this.collection.reduce((acc, message) => {
if (message.content || message.tool_calls) {
acc.push({
role: message.role,
content: message.content,
...(message.name && { name: message.name }),
...(message.tool_calls && { tool_calls: message.tool_calls }),
...(message.role === 'tool' && { tool_call_id: message.identifier }),
...(message.signature && { signature: message.signature }),
});
}
return acc;
}, []);
}
/**
* Method to get the collection of messages.
* @returns {Array} The collection of Message instances.
*/
getCollection() {
return this.collection;
}
/**
* Add a new item to the collection.
* @param {Object} item - The Message or MessageCollection instance to be added.
*/
add(item) {
this.collection.push(item);
}
/**
* Get an item from the collection by its identifier.
* @param {string} identifier - The identifier of the item to be found.
* @returns {Object} The found item, or undefined if no item was found.
*/
getItemByIdentifier(identifier) {
return this.collection.find(item => item?.identifier === identifier);
}
/**
* Check if an item with the given identifier exists in the collection.
* @param {string} identifier - The identifier to check.
* @returns {boolean} True if an item with the given identifier exists, false otherwise.
*/
hasItemWithIdentifier(identifier) {
return this.collection.some(message => message.identifier === identifier);
}
/**
* Get the total number of tokens in the collection.
* @returns {number} The total number of tokens.
*/
getTokens() {
return this.collection.reduce((tokens, message) => tokens + message.getTokens(), 0);
}
/**
* Combines message collections into a single collection.
* @returns {Message[]} The collection of messages flattened into a single array.
*/
flatten() {
return this.collection.reduce((acc, message) => {
if (message instanceof MessageCollection) {
acc.push(...message.flatten());
} else {
acc.push(message);
}
return acc;
}, []);
}
}
/**
* OpenAI API chat completion representation
* const map = [{identifier: 'example', message: {role: 'system', content: 'exampleContent'}}, ...];
*
* This class creates a chat context that can be sent to Open AI's api
* Includes message management and token budgeting.
*
* @see https://platform.openai.com/docs/guides/gpt/chat-completions-api
*
*/
export class ChatCompletion {
/**
* Combines consecutive system messages into one if they have no name attached.
* @returns {Promise<void>}
*/
async squashSystemMessages() {
const excludeList = ['newMainChat', 'newChat', 'groupNudge'];
this.messages.collection = this.messages.flatten();
let lastMessage = null;
let squashedMessages = [];
for (let message of this.messages.collection) {
// Force exclude empty messages
if (message.role === 'system' && !message.content) {
continue;
}
const shouldSquash = (message) => {
return !excludeList.includes(message.identifier) && message.role === 'system' && !message.name;
};
if (shouldSquash(message)) {
if (lastMessage && shouldSquash(lastMessage)) {
lastMessage.content += '\n' + message.content;
lastMessage.tokens = await tokenHandler.countAsync({ role: lastMessage.role, content: lastMessage.content });
}
else {
squashedMessages.push(message);
lastMessage = message;
}
}
else {
squashedMessages.push(message);
lastMessage = message;
}
}
this.messages.collection = squashedMessages;
}
/**
* Initializes a new instance of ChatCompletion.
* Sets up the initial token budget and a new message collection.
*/
constructor() {
this.tokenBudget = 0;
this.messages = new MessageCollection('root');
this.loggingEnabled = false;
this.overriddenPrompts = [];
}
/**
* Retrieves all messages.
*
* @returns {MessageCollection} The MessageCollection instance holding all messages.
*/
getMessages() {
return this.messages;
}
/**
* Calculates and sets the token budget based on context and response.
*
* @param {number} context - Number of tokens in the context.
* @param {number} response - Number of tokens in the response.
*/
setTokenBudget(context, response) {
this.log(`Prompt tokens: ${context}`);
this.log(`Completion tokens: ${response}`);
this.tokenBudget = context - response;
this.log(`Token budget: ${this.tokenBudget}`);
}
/**
* Adds a message or message collection to the collection.
*
* @param {Message|MessageCollection} collection - The message or message collection to add.
* @param {number|null} position - The position at which to add the collection.
* @returns {ChatCompletion} The current instance for chaining.
*/
add(collection, position = null) {
this.validateMessageCollection(collection);
this.checkTokenBudget(collection, collection.identifier);
if (null !== position && -1 !== position) {
this.messages.collection[position] = collection;
} else {
this.messages.collection.push(collection);
}
this.decreaseTokenBudgetBy(collection.getTokens());
this.log(`Added ${collection.identifier}. Remaining tokens: ${this.tokenBudget}`);
return this;
}
/**
* Inserts a message at the start of the specified collection.
*
* @param {Message} message - The message to insert.
* @param {string} identifier - The identifier of the collection where to insert the message.
*/
insertAtStart(message, identifier) {
this.insert(message, identifier, 'start');
}
/**
* Inserts a message at the end of the specified collection.
*
* @param {Message} message - The message to insert.
* @param {string} identifier - The identifier of the collection where to insert the message.
*/
insertAtEnd(message, identifier) {
this.insert(message, identifier, 'end');
}
/**
* Inserts a message at the specified position in the specified collection.
*
* @param {Message} message - The message to insert.
* @param {string} identifier - The identifier of the collection where to insert the message.
* @param {string|number} position - The position at which to insert the message ('start' or 'end').
*/
insert(message, identifier, position = 'end') {
this.validateMessage(message);
this.checkTokenBudget(message, message.identifier);
const index = this.findMessageIndex(identifier);
if (message.content || message.tool_calls) {
if ('start' === position) this.messages.collection[index].collection.unshift(message);
else if ('end' === position) this.messages.collection[index].collection.push(message);
else if (typeof position === 'number') this.messages.collection[index].collection.splice(position, 0, message);
this.decreaseTokenBudgetBy(message.getTokens());
this.log(`Inserted ${message.identifier} into ${identifier}. Remaining tokens: ${this.tokenBudget}`);
}
}
/**
* Remove the last item of the collection
*
* @param identifier
*/
removeLastFrom(identifier) {
const index = this.findMessageIndex(identifier);
const message = this.messages.collection[index].collection.pop();
if (!message) {
this.log(`No message to remove from ${identifier}`);
return;
}
this.increaseTokenBudgetBy(message.getTokens());
this.log(`Removed ${message.identifier} from ${identifier}. Remaining tokens: ${this.tokenBudget}`);
}
/**
* Checks if the token budget can afford the tokens of the specified message.
*
* @param {Message|MessageCollection} message - The message to check for affordability.
* @returns {boolean} True if the budget can afford the message, false otherwise.
*/
canAfford(message) {
return 0 <= this.tokenBudget - message.getTokens();
}
/**
* Checks if the token budget can afford the tokens of all the specified messages.
* @param {Message[]} messages - The messages to check for affordability.
* @returns {boolean} True if the budget can afford all the messages, false otherwise.
*/
canAffordAll(messages) {
return 0 <= this.tokenBudget - messages.reduce((total, message) => total + message.getTokens(), 0);
}
/**
* Checks if a message with the specified identifier exists in the collection.
*
* @param {string} identifier - The identifier to check for existence.
* @returns {boolean} True if a message with the specified identifier exists, false otherwise.
*/
has(identifier) {
return this.messages.hasItemWithIdentifier(identifier);
}
/**
* Retrieves the total number of tokens in the collection.
*
* @returns {number} The total number of tokens.
*/
getTotalTokenCount() {
return this.messages.getTokens();
}
/**
* Retrieves the chat as a flattened array of messages.
*
* @returns {Array} The chat messages.
*/
getChat() {
const chat = [];
for (let item of this.messages.collection) {
if (item instanceof MessageCollection) {
chat.push(...item.getChat());
} else if (item instanceof Message && (item.content || item.tool_calls)) {
const message = {
role: item.role,
content: item.content,
...(item.name ? { name: item.name } : {}),
...(item.tool_calls ? { tool_calls: item.tool_calls } : {}),
...(item.role === 'tool' ? { tool_call_id: item.identifier } : {}),
...(item.signature ? { signature: item.signature } : {}),
};
chat.push(message);
} else {
this.log(`Skipping invalid or empty message in collection: ${JSON.stringify(item)}`);
}
}
return chat;
}
/**
* Logs an output message to the console if logging is enabled.
*
* @param {string} output - The output message to log.
*/
log(output) {
if (this.loggingEnabled) console.log('[ChatCompletion] ' + output);
}
/**
* Enables logging of output messages to the console.
*/
enableLogging() {
this.loggingEnabled = true;
}
/**
* Disables logging of output messages to the console.
*/
disableLogging() {
this.loggingEnabled = false;
}
/**
* Validates if the given argument is an instance of MessageCollection.
* Throws an error if the validation fails.
*
* @param {MessageCollection|Message} collection - The collection to validate.
*/
validateMessageCollection(collection) {
if (!(collection instanceof MessageCollection)) {
console.log(collection);
throw new Error('Argument must be an instance of MessageCollection');
}
}
/**
* Validates if the given argument is an instance of Message.
* Throws an error if the validation fails.
*
* @param {Message} message - The message to validate.
*/
validateMessage(message) {
if (!(message instanceof Message)) {
console.log(message);
throw new Error('Argument must be an instance of Message');
}
}
/**
* Checks if the token budget can afford the tokens of the given message.
* Throws an error if the budget can't afford the message.
*
* @param {Message|MessageCollection} message - The message to check.
* @param {string} identifier - The identifier of the message.
*/
checkTokenBudget(message, identifier) {
if (!this.canAfford(message)) {
throw new TokenBudgetExceededError(identifier);
}
}
/**
* Reserves the tokens required by the given message from the token budget.
*
* @param {Message|MessageCollection|number} message - The message whose tokens to reserve.
*/
reserveBudget(message) {
const tokens = typeof message === 'number' ? message : message.getTokens();
this.decreaseTokenBudgetBy(tokens);
}
/**
* Frees up the tokens used by the given message from the token budget.
*
* @param {Message|MessageCollection} message - The message whose tokens to free.
*/
freeBudget(message) { this.increaseTokenBudgetBy(message.getTokens()); }
/**
* Increases the token budget by the given number of tokens.
* This function should be used sparingly, per design the completion should be able to work with its initial budget.
*
* @param {number} tokens - The number of tokens to increase the budget by.
*/
increaseTokenBudgetBy(tokens) {
this.tokenBudget += tokens;
}
/**
* Decreases the token budget by the given number of tokens.
* This function should be used sparingly, per design the completion should be able to work with its initial budget.
*
* @param {number} tokens - The number of tokens to decrease the budget by.
*/
decreaseTokenBudgetBy(tokens) {
this.tokenBudget -= tokens;
}
/**
* Finds the index of a message in the collection by its identifier.
* Throws an error if a message with the given identifier is not found.
*
* @param {string} identifier - The identifier of the message to find.
* @returns {number} The index of the message in the collection.
*/
findMessageIndex(identifier) {
const index = this.messages.collection.findIndex(item => item?.identifier === identifier);
if (index < 0) {
throw new IdentifierNotFoundError(identifier);
}
return index;
}
/**
* Sets the list of overridden prompts.
* @param {string[]} list A list of prompts that were overridden.
*/
setOverriddenPrompts(list) {
this.overriddenPrompts = list;
}
getOverriddenPrompts() {
return this.overriddenPrompts ?? [];
}
}
/**
* Migrate old Chat Completion settings to new format.
* @param {ChatCompletionSettings} settings Settings to migrate
*/
function migrateChatCompletionSettings(settings) {
const migrateMap = [
{ oldKey: 'names_in_completion', oldValue: true, newKey: 'names_behavior', newValue: character_names_behavior.COMPLETION },
{ oldKey: 'chat_completion_source', oldValue: 'palm', newKey: 'chat_completion_source', newValue: chat_completion_sources.MAKERSUITE },
{ oldKey: 'custom_prompt_post_processing', oldValue: custom_prompt_post_processing_types.CLAUDE, newKey: 'custom_prompt_post_processing', newValue: custom_prompt_post_processing_types.MERGE },
{ oldKey: 'ai21_model', oldValue: /^j2-/, newKey: 'ai21_model', newValue: 'jamba-large' },
{ oldKey: 'image_inlining', oldValue: false, newKey: 'media_inlining', newValue: false },
{ oldKey: 'image_inlining', oldValue: true, newKey: 'media_inlining', newValue: true },
{ oldKey: 'video_inlining', oldValue: true, newKey: 'media_inlining', newValue: true },
{ oldKey: 'audio_inlining', oldValue: true, newKey: 'media_inlining', newValue: true },
{ oldKey: 'claude_use_sysprompt', oldValue: true, newKey: 'use_sysprompt', newValue: true },
{ oldKey: 'use_makersuite_sysprompt', oldValue: true, newKey: 'use_sysprompt', newValue: true },
{ oldKey: 'mistralai_model', oldValue: /^(mistral-medium|mistral-small)$/, newKey: 'mistralai_model', newValue: (settings.mistralai_model + '-latest') },
];
for (const migration of migrateMap) {
if (Object.hasOwn(settings, migration.oldKey)) {
const shouldMigrate = migration.oldValue instanceof RegExp
? migration.oldValue.test(settings[migration.oldKey])
: settings[migration.oldKey] === migration.oldValue;
if (shouldMigrate) {
settings[migration.newKey] = migration.newValue;
}
if (migration.oldKey !== migration.newKey) {
delete settings[migration.oldKey];
}
}
}
}
/**
* Load OpenAI settings from backend data
* @param {any} data Settings data from backend
* @param {ChatCompletionSettings} settings Saved settings from backend
*/
function loadOpenAISettings(data, settings) {
openai_setting_names = data.openai_setting_names;
openai_settings = data.openai_settings;
openai_settings.forEach(function (item, i) {
openai_settings[i] = JSON.parse(item);
});
$('#settings_preset_openai').empty();
const settingNames = {};
openai_setting_names.forEach(function (item, i) {
settingNames[item] = i;
const option = document.createElement('option');
option.value = i;
option.text = item;
$('#settings_preset_openai').append(option);
});
openai_setting_names = settingNames;
migrateChatCompletionSettings(settings);
for (const key of Object.keys(default_settings)) {
oai_settings[key] = settings[key] ?? default_settings[key];
const settingToUpdate = Object.values(settingsToUpdate).find(([_, k]) => k === key);
if (settingToUpdate) {
const [selector] = settingToUpdate;
const $element = $(selector);
if ($element.length === 0) {
continue;
}
if ($element.is('input[type="checkbox"]')) {
$element.prop('checked', oai_settings[key]);
} else if ($element.is('select')) {
$element.val(oai_settings[key]);
$element.find(`option[value="${CSS.escape(oai_settings[key])}"]`).prop('selected', true);
} else {
$element.val(oai_settings[key]);
if ($element.is('input[type="range"]')) {
const id = $element.attr('id');
const $counter = $(`input[type="number"][data-for="${id}"]`);
if ($counter.length > 0) {
$counter.val(Number(oai_settings[key]));
}
}
}
}
}
$(`#settings_preset_openai option[value="${openai_setting_names[oai_settings.preset_settings_openai]}"]`).prop('selected', true);
$('#bind_preset_to_connection').prop('checked', oai_settings.bind_preset_to_connection);
$('#openai_external_category').toggle(oai_settings.show_external_models);
$('.reverse_proxy_warning').toggle(oai_settings.reverse_proxy !== '');
// Don't display Service Account JSON in textarea - it's stored in backend secrets
$('#vertexai_service_account_json').val('');
updateVertexAIServiceAccountStatus();
$('#openai_logit_bias_preset').empty();
for (const preset of Object.keys(oai_settings.bias_presets)) {
// Backfill missing IDs
if (Array.isArray(oai_settings.bias_presets[preset])) {
oai_settings.bias_presets[preset].forEach((bias) => {
if (bias && !bias.id) {
bias.id = uuidv4();
}
});
}
const option = document.createElement('option');
option.innerText = preset;
option.value = preset;
option.selected = preset === oai_settings.bias_preset_selected;
$('#openai_logit_bias_preset').append(option);
}
$('#openai_logit_bias_preset').trigger('change');
setNamesBehaviorControls();
setContinuePostfixControls();
$('#openrouter_providers_chat').trigger('change');
$('#chat_completion_source').trigger('change');
}
function setNamesBehaviorControls() {
switch (oai_settings.names_behavior) {
case character_names_behavior.NONE:
$('#character_names_none').prop('checked', true);
break;
case character_names_behavior.DEFAULT:
$('#character_names_default').prop('checked', true);
break;
case character_names_behavior.COMPLETION:
$('#character_names_completion').prop('checked', true);
break;
case character_names_behavior.CONTENT:
$('#character_names_content').prop('checked', true);
break;
}
const checkedItemText = $('input[name="character_names"]:checked ~ span').text().trim();
$('#character_names_display').text(checkedItemText);
}
function setContinuePostfixControls() {
switch (oai_settings.continue_postfix) {
case continue_postfix_types.NONE:
$('#continue_postfix_none').prop('checked', true);
break;
case continue_postfix_types.SPACE:
$('#continue_postfix_space').prop('checked', true);
break;
case continue_postfix_types.NEWLINE:
$('#continue_postfix_newline').prop('checked', true);
break;
case continue_postfix_types.DOUBLE_NEWLINE:
$('#continue_postfix_double_newline').prop('checked', true);
break;
default:
// Prevent preset value abuse
oai_settings.continue_postfix = continue_postfix_types.SPACE;
$('#continue_postfix_space').prop('checked', true);
break;
}
$('#continue_postfix').val(oai_settings.continue_postfix);
const checkedItemText = $('input[name="continue_postfix"]:checked ~ span').text().trim();
$('#continue_postfix_display').text(checkedItemText);
}
async function getStatusOpen() {
const noValidateSources = [
chat_completion_sources.CLAUDE,
chat_completion_sources.AI21,
chat_completion_sources.VERTEXAI,
chat_completion_sources.PERPLEXITY,
chat_completion_sources.ZAI,
];
if (noValidateSources.includes(oai_settings.chat_completion_source)) {
let status = t`Key saved; press \"Test Message\" to verify.`;
setOnlineStatus(status);
updateFeatureSupportFlags();
return resultCheckStatus();
}
if (oai_settings.chat_completion_source === chat_completion_sources.CUSTOM && !isValidUrl(oai_settings.custom_url)) {
console.debug('Invalid endpoint URL of Custom OpenAI API:', oai_settings.custom_url);
setOnlineStatus(t`Invalid endpoint URL. Requests may fail.`);
return resultCheckStatus();
}
if (oai_settings.chat_completion_source === chat_completion_sources.AZURE_OPENAI && !isValidUrl(oai_settings.azure_base_url)) {
console.debug('Invalid endpoint URL of Azure OpenAI API:', oai_settings.azure_base_url);
setOnlineStatus(t`Invalid Azure endpoint URL. Requests may fail.`);
return resultCheckStatus();
}
let data = {
reverse_proxy: oai_settings.reverse_proxy,
proxy_password: oai_settings.proxy_password,
chat_completion_source: oai_settings.chat_completion_source,
};
const validateProxySources = [
chat_completion_sources.CLAUDE,
chat_completion_sources.OPENAI,
chat_completion_sources.MISTRALAI,
chat_completion_sources.MAKERSUITE,
chat_completion_sources.VERTEXAI,
chat_completion_sources.DEEPSEEK,
chat_completion_sources.XAI,
];
if (oai_settings.reverse_proxy && validateProxySources.includes(oai_settings.chat_completion_source)) {
await validateReverseProxy();
}
if (oai_settings.chat_completion_source === chat_completion_sources.CUSTOM) {
$('.model_custom_select').empty();
data.custom_url = oai_settings.custom_url;
data.custom_include_headers = oai_settings.custom_include_headers;
}
if (oai_settings.chat_completion_source === chat_completion_sources.AZURE_OPENAI) {
data.azure_base_url = oai_settings.azure_base_url;
data.azure_deployment_name = oai_settings.azure_deployment_name;
data.azure_api_version = oai_settings.azure_api_version;
}
const canBypass = (oai_settings.chat_completion_source === chat_completion_sources.OPENAI && oai_settings.bypass_status_check) || oai_settings.chat_completion_source === chat_completion_sources.CUSTOM;
if (canBypass) {
setOnlineStatus(t`Status check bypassed`);
}
try {
const response = await fetch('/api/backends/chat-completions/status', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(data),
signal: abortStatusCheck.signal,
cache: 'no-cache',
});
if (!response.ok) {
throw new Error(response.statusText);
}
const responseData = await response.json();
if ('data' in responseData && Array.isArray(responseData.data)) {
saveModelList(responseData.data);
}
if (!('error' in responseData)) {
setOnlineStatus(t`Valid`);
}
if (responseData.bypass) {
setOnlineStatus(t`Status check bypassed`);
}
} catch (error) {
console.error(error);
if (!canBypass) {
setOnlineStatus('no_connection');
}
}
updateFeatureSupportFlags();
return resultCheckStatus();
}
/**
* Get OpenAI preset body from settings
* @param {ChatCompletionSettings} settings The settings object
* @returns {Object} The preset body object
*/
export function getChatCompletionPreset(settings = oai_settings) {
const presetBody = {};
for (const [presetKey, [, settingsKey]] of Object.entries(settingsToUpdate)) {
presetBody[presetKey] = settings[settingsKey];
}
return structuredClone(presetBody);
}
/**
* Persist a settings preset with the given name
*
* @param {string} name - Name of the preset
* @param {ChatCompletionSettings} settings The settings object
* @param {boolean} triggerUi Whether the change event of preset UI element should be emitted
* @returns {Promise<void>}
*/
async function saveOpenAIPreset(name, settings, triggerUi = true) {
const presetBody = getChatCompletionPreset(settings);
const savePresetSettings = await fetch('/api/presets/save', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
apiId: 'openai',
name: name,
preset: presetBody,
}),
});
if (savePresetSettings.ok) {
const data = await savePresetSettings.json();
if (Object.keys(openai_setting_names).includes(data.name)) {
oai_settings.preset_settings_openai = data.name;
const value = openai_setting_names[data.name];
Object.assign(openai_settings[value], presetBody);
$(`#settings_preset_openai option[value="${value}"]`).prop('selected', true);
if (triggerUi) $('#settings_preset_openai').trigger('change');
}
else {
openai_settings.push(presetBody);
openai_setting_names[data.name] = openai_settings.length - 1;
const option = document.createElement('option');
option.selected = true;
option.value = String(openai_settings.length - 1);
option.innerText = data.name;
if (triggerUi) $('#settings_preset_openai').append(option).trigger('change');
}
} else {
toastr.error(t`Failed to save preset`);
throw new Error('Failed to save preset');
}
}
function onLogitBiasPresetChange() {
const value = String($('#openai_logit_bias_preset').find(':selected').val());
const preset = oai_settings.bias_presets[value];
if (!Array.isArray(preset)) {
console.error('Preset not found');
return;
}
oai_settings.bias_preset_selected = value;
const list = $('.openai_logit_bias_list');
list.empty();
for (const entry of preset) {
if (entry) {
createLogitBiasListItem(entry);
}
}
// Check if a sortable instance exists
if (list.sortable('instance') !== undefined) {
// Destroy the instance
list.sortable('destroy');
}
// Make the list sortable
list.sortable({
delay: getSortableDelay(),
handle: '.drag-handle',
stop: function () {
const order = [];
list.children().each(function () {
order.unshift($(this).data('id'));
});
preset.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
console.log('Logit bias reordered:', preset);
saveSettingsDebounced();
},
});
biasCache = undefined;
saveSettingsDebounced();
}
function createNewLogitBiasEntry() {
const entry = { id: uuidv4(), text: '', value: 0 };
oai_settings.bias_presets[oai_settings.bias_preset_selected].push(entry);
biasCache = undefined;
createLogitBiasListItem(entry);
saveSettingsDebounced();
}
function createLogitBiasListItem(entry) {
if (!entry.id) {
entry.id = uuidv4();
}
const id = entry.id;
const template = $('#openai_logit_bias_template .openai_logit_bias_form').clone();
template.data('id', id);
template.find('.openai_logit_bias_text').val(entry.text).on('input', function () {
entry.text = String($(this).val());
biasCache = undefined;
saveSettingsDebounced();
});
template.find('.openai_logit_bias_value').val(entry.value).on('input', function () {
const min = Number($(this).attr('min'));
const max = Number($(this).attr('max'));
let value = Number($(this).val());
if (value < min) {
$(this).val(min);
value = min;
}
if (value > max) {
$(this).val(max);
value = max;
}
entry.value = value;
biasCache = undefined;
saveSettingsDebounced();
});
template.find('.openai_logit_bias_remove').on('click', function () {
$(this).closest('.openai_logit_bias_form').remove();
const preset = oai_settings.bias_presets[oai_settings.bias_preset_selected];
const index = preset.findIndex(item => item.id === id);
if (index >= 0) {
preset.splice(index, 1);
}
onLogitBiasPresetChange();
});
$('.openai_logit_bias_list').prepend(template);
}
async function createNewLogitBiasPreset() {
const name = await Popup.show.input(t`Preset name:`, null);
if (!name) {
return;
}
if (name in oai_settings.bias_presets) {
toastr.error(t`Preset name should be unique.`);
return;
}
oai_settings.bias_preset_selected = name;
oai_settings.bias_presets[name] = [];
addLogitBiasPresetOption(name);
saveSettingsDebounced();
}
function addLogitBiasPresetOption(name) {
const option = document.createElement('option');
option.innerText = name;
option.value = name;
option.selected = true;
$('#openai_logit_bias_preset').append(option);
$('#openai_logit_bias_preset').trigger('change');
}
function onImportPresetClick() {
$('#openai_preset_import_file').trigger('click');
}
function onLogitBiasPresetImportClick() {
$('#openai_logit_bias_import_file').trigger('click');
}
async function onPresetImportFileChange(e) {
const file = e.target.files[0];
if (!file) {
return;
}
const name = file.name.replace(/\.[^/.]+$/, '');
const importedFile = await getFileText(file);
let presetBody;
e.target.value = '';
try {
presetBody = JSON.parse(importedFile);
} catch (err) {
toastr.error(t`Invalid file`);
return;
}
const fields = sensitiveFields.filter(field => presetBody[field]).map(field => `<b>${field}</b>`);
const shouldConfirm = fields.length > 0;
if (shouldConfirm) {
const textHeader = 'The imported preset contains proxy and/or custom endpoint settings.';
const textMessage = fields.join('<br>');
const cancelButton = { text: 'Cancel import', result: POPUP_RESULT.CANCELLED, appendAtEnd: true };
const popupOptions = { customButtons: [cancelButton], okButton: 'Remove them', cancelButton: 'Import as-is' };
const popupResult = await Popup.show.confirm(textHeader, textMessage, popupOptions);
if (popupResult === POPUP_RESULT.CANCELLED) {
console.log('Import cancelled by user');
return;
}
if (popupResult === POPUP_RESULT.AFFIRMATIVE) {
sensitiveFields.forEach(field => delete presetBody[field]);
}
}
if (name in openai_setting_names) {
const confirm = await callGenericPopup('Preset name already exists. Overwrite?', POPUP_TYPE.CONFIRM);
if (!confirm) {
return;
}
}
await eventSource.emit(event_types.OAI_PRESET_IMPORT_READY, { data: presetBody, presetName: name });
const savePresetSettings = await fetch('/api/presets/save', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
apiId: 'openai',
name: name,
preset: presetBody,
}),
});
if (!savePresetSettings.ok) {
toastr.error(t`Failed to save preset`);
return;
}
const data = await savePresetSettings.json();
if (Object.keys(openai_setting_names).includes(data.name)) {
oai_settings.preset_settings_openai = data.name;
const value = openai_setting_names[data.name];
Object.assign(openai_settings[value], presetBody);
$(`#settings_preset_openai option[value="${value}"]`).prop('selected', true);
$('#settings_preset_openai').trigger('change');
} else {
openai_settings.push(presetBody);
openai_setting_names[data.name] = openai_settings.length - 1;
const option = document.createElement('option');
option.selected = true;
option.value = String(openai_settings.length - 1);
option.innerText = data.name;
$('#settings_preset_openai').append(option).trigger('change');
}
}
async function onExportPresetClick() {
if (!oai_settings.preset_settings_openai) {
toastr.error(t`No preset selected`);
return;
}
const preset = structuredClone(openai_settings[openai_setting_names[oai_settings.preset_settings_openai]]);
const fieldValues = sensitiveFields.filter(field => preset[field]).map(field => `<b>${field}</b>: <code>${preset[field]}</code>`);
if (fieldValues.length > 0) {
const textHeader = t`Your preset contains proxy and/or custom endpoint settings.`;
const textMessage = '<div>' + t`Do you want to remove these fields before exporting?` + `</div><br>${DOMPurify.sanitize(fieldValues.join('<br>'))}`;
const cancelButton = { text: 'Cancel', result: POPUP_RESULT.CANCELLED, appendAtEnd: true };
const popupOptions = { customButtons: [cancelButton] };
const popupResult = await Popup.show.confirm(textHeader, textMessage, popupOptions);
if (popupResult === POPUP_RESULT.CANCELLED) {
console.log('Export cancelled by user');
return;
}
if (popupResult === POPUP_RESULT.AFFIRMATIVE) {
sensitiveFields.forEach(field => delete preset[field]);
}
}
const exportConnectionTemplate = $(await renderTemplateAsync('exportPreset'));
await new Popup(exportConnectionTemplate, POPUP_TYPE.TEXT).show();
const removeConnectionData = exportConnectionTemplate.find('input[name="export_connection_data"]:checked').val() === 'false';
if (removeConnectionData) {
for (const [, [, settingName, , isConnection]] of Object.entries(settingsToUpdate)) {
if (isConnection) {
delete preset[settingName];
}
}
}
await eventSource.emit(event_types.OAI_PRESET_EXPORT_READY, preset);
const presetJsonString = JSON.stringify(preset, null, 4);
const presetFileName = `${oai_settings.preset_settings_openai}.json`;
download(presetJsonString, presetFileName, 'application/json');
}
async function onLogitBiasPresetImportFileChange(e) {
const file = e.target.files[0];
if (!file || file.type !== 'application/json') {
return;
}
const name = file.name.replace(/\.[^/.]+$/, '');
const importedFile = await parseJsonFile(file);
e.target.value = '';
if (name in oai_settings.bias_presets) {
toastr.error(t`Preset name should be unique.`);
return;
}
if (!Array.isArray(importedFile)) {
toastr.error(t`Invalid logit bias preset file.`);
return;
}
const validEntries = [];
for (const entry of importedFile) {
if (typeof entry == 'object' && entry !== null) {
if (Object.hasOwn(entry, 'text') &&
Object.hasOwn(entry, 'value')) {
if (!entry.id) {
entry.id = uuidv4();
}
validEntries.push(entry);
}
}
}
oai_settings.bias_presets[name] = validEntries;
oai_settings.bias_preset_selected = name;
addLogitBiasPresetOption(name);
saveSettingsDebounced();
}
function onLogitBiasPresetExportClick() {
if (!oai_settings.bias_preset_selected || Object.keys(oai_settings.bias_presets).length === 0) {
return;
}
const presetJsonString = JSON.stringify(oai_settings.bias_presets[oai_settings.bias_preset_selected], null, 4);
const presetFileName = `${oai_settings.bias_preset_selected}.json`;
download(presetJsonString, presetFileName, 'application/json');
}
async function onDeletePresetClick() {
const confirm = await callGenericPopup(t`Delete the preset? This action is irreversible and your current settings will be overwritten.`, POPUP_TYPE.CONFIRM);
if (!confirm) {
return;
}
const nameToDelete = oai_settings.preset_settings_openai;
const value = openai_setting_names[oai_settings.preset_settings_openai];
$(`#settings_preset_openai option[value="${value}"]`).remove();
delete openai_setting_names[oai_settings.preset_settings_openai];
oai_settings.preset_settings_openai = null;
if (Object.keys(openai_setting_names).length) {
oai_settings.preset_settings_openai = Object.keys(openai_setting_names)[0];
const newValue = openai_setting_names[oai_settings.preset_settings_openai];
$(`#settings_preset_openai option[value="${newValue}"]`).prop('selected', true);
$('#settings_preset_openai').trigger('change');
}
const response = await fetch('/api/presets/delete', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ apiId: 'openai', name: nameToDelete }),
});
if (!response.ok) {
toastr.warning(t`Preset was not deleted from server`);
} else {
toastr.success(t`Preset deleted`);
await eventSource.emit(event_types.PRESET_DELETED, { apiId: 'openai', name: nameToDelete });
}
saveSettingsDebounced();
}
async function onLogitBiasPresetDeleteClick() {
const value = await callGenericPopup(t`Delete the preset?`, POPUP_TYPE.CONFIRM);
if (!value) {
return;
}
$(`#openai_logit_bias_preset option[value="${oai_settings.bias_preset_selected}"]`).remove();
delete oai_settings.bias_presets[oai_settings.bias_preset_selected];
oai_settings.bias_preset_selected = null;
if (Object.keys(oai_settings.bias_presets).length) {
oai_settings.bias_preset_selected = Object.keys(oai_settings.bias_presets)[0];
$(`#openai_logit_bias_preset option[value="${oai_settings.bias_preset_selected}"]`).prop('selected', true);
$('#openai_logit_bias_preset').trigger('change');
}
biasCache = undefined;
saveSettingsDebounced();
}
// Load OpenAI preset settings
function onSettingsPresetChange() {
const presetNameBefore = oai_settings.preset_settings_openai;
const presetName = $('#settings_preset_openai').find(':selected').text();
oai_settings.preset_settings_openai = presetName;
const preset = structuredClone(openai_settings[openai_setting_names[oai_settings.preset_settings_openai]]);
migrateChatCompletionSettings(preset);
const updateInput = (selector, value) => $(selector).val(value).trigger('input', { source: 'preset' });
const updateCheckbox = (selector, value) => $(selector).prop('checked', value).trigger('input', { source: 'preset' });
// Allow subscribers to alter the preset before applying deltas
eventSource.emit(event_types.OAI_PRESET_CHANGED_BEFORE, {
preset: preset,
presetName: presetName,
settingsToUpdate: settingsToUpdate,
settings: oai_settings,
savePreset: saveOpenAIPreset,
presetNameBefore: presetNameBefore,
}).finally(async () => {
if (oai_settings.bind_preset_to_connection) {
$('.model_custom_select').empty();
}
for (const [key, [selector, setting, isCheckbox, isConnection]] of Object.entries(settingsToUpdate)) {
if (isConnection && !oai_settings.bind_preset_to_connection) {
continue;
}
// Extensions don't need UI updates and shouldn't fallback to current settings
if (key === 'extensions') {
oai_settings.extensions = preset.extensions || {};
continue;
}
if (preset[key] !== undefined) {
if (isCheckbox) {
updateCheckbox(selector, preset[key]);
} else {
updateInput(selector, preset[key]);
}
oai_settings[setting] = preset[key];
}
}
// These cannot be changed via preset if unbound to connection
if (oai_settings.bind_preset_to_connection) {
$('#chat_completion_source').trigger('change');
$('#openrouter_providers_chat').trigger('change');
}
$('#openai_logit_bias_preset').trigger('change');
saveSettingsDebounced();
await eventSource.emit(event_types.OAI_PRESET_CHANGED_AFTER);
await eventSource.emit(event_types.PRESET_CHANGED, { apiId: 'openai', name: presetName });
});
}
function getMaxContextOpenAI(value) {
if (oai_settings.max_context_unlocked) {
return unlocked_max;
}
else if (value.startsWith('gpt-5')) {
return max_400k;
}
else if (value.includes('gpt-4.1')) {
return max_1mil;
}
else if (value.startsWith('o1')) {
return max_128k;
}
else if (value.startsWith('o4') || value.startsWith('o3')) {
return max_200k;
}
else if (value.includes('chatgpt-4o-latest') || value.includes('gpt-4-turbo') || value.includes('gpt-4o') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {
return max_128k;
}
else if (value.includes('gpt-3.5-turbo-1106')) {
return max_16k;
}
else if (['gpt-4', 'gpt-4-0314', 'gpt-4-0613'].includes(value)) {
return max_8k;
}
else if (['gpt-4-32k', 'gpt-4-32k-0314', 'gpt-4-32k-0613'].includes(value)) {
return max_32k;
}
else if (['gpt-3.5-turbo-16k', 'gpt-3.5-turbo-16k-0613'].includes(value)) {
return max_16k;
}
else if (value == 'code-davinci-002') {
return max_8k;
}
else if (['text-curie-001', 'text-babbage-001', 'text-ada-001'].includes(value)) {
return max_2k;
}
else {
// default to gpt-3 (4095 tokens)
return max_4k;
}
}
/**
* Get the maximum context size for the Mistral model
* @param {string} model Model identifier
* @param {boolean} isUnlocked Whether context limits are unlocked
* @returns {number} Maximum context size in tokens
*/
function getMistralMaxContext(model, isUnlocked) {
if (isUnlocked) {
return unlocked_max;
}
if (Array.isArray(model_list) && model_list.length > 0) {
const contextLength = model_list.find((record) => record.id === model)?.max_context_length;
if (contextLength) {
return contextLength;
}
}
// Return context size if model found, otherwise default to 32k
return max_32k;
}
/**
* Get the maximum context size for the Groq model
* @param {string} model Model identifier
* @param {boolean} isUnlocked Whether context limits are unlocked
* @returns {number} Maximum context size in tokens
*/
function getGroqMaxContext(model, isUnlocked) {
if (isUnlocked) {
return unlocked_max;
}
if (Array.isArray(model_list) && model_list.length > 0) {
const contextLength = model_list.find((record) => record.id === model)?.context_window;
if (contextLength) {
return contextLength;
}
}
const contextMap = {
'gemma2-9b-it': max_8k,
'llama-3.3-70b-versatile': max_128k,
'llama-3.1-8b-instant': max_128k,
'llama3-70b-8192': max_8k,
'llama3-8b-8192': max_8k,
'llama-guard-3-8b': max_8k,
'mixtral-8x7b-32768': max_32k,
'deepseek-r1-distill-llama-70b': max_128k,
'llama-3.3-70b-specdec': max_8k,
'llama-3.2-1b-preview': max_128k,
'llama-3.2-3b-preview': max_128k,
'llama-3.2-11b-vision-preview': max_128k,
'llama-3.2-90b-vision-preview': max_128k,
'qwen-2.5-32b': max_128k,
'deepseek-r1-distill-qwen-32b': max_128k,
'deepseek-r1-distill-llama-70b-specdec': max_128k,
'mistral-saba-24b': max_32k,
'meta-llama/llama-4-scout-17b-16e-instruct': max_128k,
'meta-llama/llama-4-maverick-17b-128e-instruct': max_128k,
'compound-beta': max_128k,
'compound-beta-mini': max_128k,
'qwen/qwen3-32b': max_128k,
};
// Return context size if model found, otherwise default to 128k
return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_128k;
}
/**
* Get the maximum context size for the Z.AI model
* @param {string} model Model identifier
* @param {boolean} isUnlocked If context limits are unlocked
* @returns {number} Maximum context size in tokens
*/
function getZaiMaxContext(model, isUnlocked) {
if (isUnlocked) {
return unlocked_max;
}
const contextMap = {
'glm-4.7': max_200k,
'glm-4.6v': max_128k,
'glm-4.6v-flash': max_128k,
'glm-4.6v-flashx': max_128k,
'glm-4.6': max_200k,
'glm-4.5': max_128k,
'glm-4-32b-0414-128k': max_128k,
'glm-4.5-air': max_128k,
'glm-4.5v': max_64k,
'autoglm-phone-multilingual': max_64k,
};
// Return context size if model found, otherwise default to 128k
return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_128k;
}
/**
* Get the maximum context size for the SiliconFlow model
* @param {string} model Model identifier
* @param {boolean} isUnlocked Whether context limits are unlocked
* @returns {number} Maximum context size in tokens
*/
function getSiliconflowMaxContext(model, isUnlocked) {
if (isUnlocked) {
return unlocked_max;
}
const contextMap = {
'baidu/ERNIE-4.5-300B-A47B': max_128k,
'ByteDance-Seed/Seed-OSS-36B-Instruct': max_256k,
'deepseek-ai/DeepSeek-R1': max_128k,
'deepseek-ai/DeepSeek-V3': max_128k,
'deepseek-ai/DeepSeek-V3.1': max_128k,
'deepseek-ai/DeepSeek-V3.1-Terminus': max_128k,
'deepseek-ai/DeepSeek-V3.2-Exp': max_128k,
'deepseek-ai/deepseek-vl2': max_4k,
'inclusionAI/Ling-1T': max_128k,
'inclusionAI/Ling-flash-2.0': max_128k,
'inclusionAI/Ling-mini-2.0': max_128k,
'inclusionAI/Ring-1T': max_128k,
'inclusionAI/Ring-flash-2.0': max_128k,
'meta-llama/Llama-3.3-70B-Instruct': max_32k,
'meta-llama/Meta-Llama-3.1-8B-Instruct': max_32k,
'MiniMaxAI/MiniMax-M1-80k': max_128k,
'MiniMaxAI/MiniMax-M2': max_128k,
'moonshotai/Kimi-K2-Instruct': max_128k,
'moonshotai/Kimi-K2-Instruct-0905': max_256k,
'moonshotai/Kimi-K2-Thinking': max_256k,
'openai/gpt-oss-120b': max_128k,
'openai/gpt-oss-20b': max_128k,
'Qwen/Qwen3-235B-A22B-Instruct-2507': max_256k,
'Qwen/Qwen3-235B-A22B-Thinking-2507': max_256k,
'Qwen/Qwen3-30B-A3B-Instruct-2507': max_256k,
'Qwen/Qwen3-30B-A3B-Thinking-2507': max_256k,
'Qwen/Qwen3-VL-235B-A22B-Instruct': max_256k,
'Qwen/Qwen3-VL-235B-A22B-Thinking': max_256k,
'Qwen/Qwen3-VL-30B-A3B-Instruct': max_256k,
'Qwen/Qwen3-VL-30B-A3B-Thinking': max_256k,
'Qwen/Qwen3-VL-32B-Instruct': max_256k,
'Qwen/Qwen3-VL-32B-Thinking': max_256k,
'Qwen/Qwen3-VL-8B-Instruct': max_256k,
'Qwen/Qwen3-VL-8B-Thinking': max_256k,
'stepfun-ai/step3': max_64k,
'tencent/Hunyuan-A13B-Instruct': max_128k,
'zai-org/GLM-4.5': max_128k,
'zai-org/GLM-4.5-Air': max_128k,
'zai-org/GLM-4.5V': max_64k,
'zai-org/GLM-4.6': max_200k,
};
// Return context size if model found, otherwise default to 32k
return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_32k;
}
/**
* Get the maximum context size for the Moonshot model
* @param {string} model Model identifier
* @param {boolean} isUnlocked If context limits are unlocked
* @returns {number} Maximum context size in tokens
*/
function getMoonshotMaxContext(model, isUnlocked) {
if (isUnlocked) {
return unlocked_max;
}
if (Array.isArray(model_list) && model_list.length > 0) {
const modelInfo = model_list.find((record) => record.id === model);
if (modelInfo?.context_length) {
return modelInfo.context_length;
}
}
const contextMap = {
'moonshot-v1-8k': max_8k,
'moonshot-v1-32k': max_32k,
'moonshot-v1-128k': max_128k,
'moonshot-v1-auto': max_128k,
'moonshot-v1-8k-vision-preview': max_8k,
'moonshot-v1-32k-vision-preview': max_32k,
'moonshot-v1-128k-vision-preview': max_128k,
'kimi-k2-0711-preview': max_32k,
'kimi-latest': max_32k,
'kimi-thinking-preview': max_32k,
};
// Return context size if model found, otherwise default to 32k
return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_32k;
}
/**
* Get the maximum context size for the Fireworks model
* @param {string} model Model identifier
* @param {boolean} isUnlocked Whether context limits are unlocked
* @returns {number} Maximum context size in tokens
*/
function getFireworksMaxContext(model, isUnlocked) {
if (isUnlocked) {
return unlocked_max;
}
// First check if model info is available from model_list
if (Array.isArray(model_list) && model_list.length > 0) {
const modelInfo = model_list.find((record) => record.id === model);
if (modelInfo?.context_length) {
return modelInfo.context_length;
}
if (modelInfo?.context_window) {
return modelInfo.context_window;
}
}
return max_32k;
}
/**
* Get the maximum context size for the Chutes model
* @param {string} model Model identifier
* @param {boolean} isUnlocked Whether context limits are unlocked
* @returns {number} Maximum context size in tokens
*/
function getChutesMaxContext(model, isUnlocked) {
if (isUnlocked) {
return unlocked_max;
}
if (Array.isArray(model_list)) {
const modelInfo = model_list.find(m => m.id === model);
if (modelInfo?.context_length) {
return modelInfo.context_length;
}
}
return max_8k;
}
/**
* Get the maximum context size for the ElectronHub model
* @param {string} model Model identifier
* @param {boolean} isUnlocked Whether context limits are unlocked
* @returns {number} Maximum context size in tokens
*/
function getElectronHubMaxContext(model, isUnlocked) {
if (isUnlocked) {
return unlocked_max;
}
if (Array.isArray(model_list)) {
const modelInfo = model_list.find(m => m.id === model);
if (modelInfo?.tokens) {
return modelInfo.tokens;
}
}
return max_128k;
}
/**
* Get the maximum context size for the NanoGPT model
* @param {string} model Model identifier
* @param {boolean} isUnlocked Whether context limits are unlocked
* @returns {number} Maximum context size in tokens
*/
function getNanoGptMaxContext(model, isUnlocked) {
if (isUnlocked) {
return unlocked_max;
}
if (Array.isArray(model_list)) {
const modelInfo = model_list.find(m => m.id === model);
if (modelInfo?.context_length) {
return modelInfo.context_length;
}
}
return max_128k;
}
async function onModelChange() {
biasCache = undefined;
let value = String($(this).val() || '');
// Skip setting the context size for sources that get it from external APIs
const hasModelsLoaded = Array.isArray(model_list) && model_list.length > 0;
if ($(this).is('#model_claude_select')) {
if (value.includes('-v')) {
value = value.replace('-v', '-');
} else if (value === '' || value === 'claude-2') {
value = default_settings.claude_model;
}
console.log('Claude model changed to', value);
oai_settings.claude_model = value;
$('#model_claude_select').val(oai_settings.claude_model);
}
if ($(this).is('#model_openai_select')) {
console.log('OpenAI model changed to', value);
oai_settings.openai_model = value;
}
if ($(this).is('#model_openrouter_select')) {
if (!value || !hasModelsLoaded) {
console.debug('Null OR model selected. Ignoring.');
return;
}
console.log('OpenRouter model changed to', value);
oai_settings.openrouter_model = value;
}
if ($(this).is('#model_ai21_select')) {
if (value === '' || value.startsWith('j2-')) {
value = 'jamba-large';
$('#model_ai21_select').val(value);
}
console.log('AI21 model changed to', value);
oai_settings.ai21_model = value;
}
if ($(this).is('#model_google_select')) {
if (!value) {
console.debug('Null Google model selected. Ignoring.');
return;
}
console.log('Google model changed to', value);
oai_settings.google_model = value;
}
if ($(this).is('#model_vertexai_select')) {
console.log('Vertex AI model changed to', value);
oai_settings.vertexai_model = value;
}
if ($(this).is('#model_mistralai_select')) {
if (!value || !hasModelsLoaded) {
console.debug('Null MistralAI model selected. Ignoring.');
return;
}
console.log('MistralAI model changed to', value);
oai_settings.mistralai_model = value;
$('#model_mistralai_select').val(oai_settings.mistralai_model);
}
if ($(this).is('#model_cohere_select')) {
console.log('Cohere model changed to', value);
oai_settings.cohere_model = value;
}
if ($(this).is('#model_perplexity_select')) {
console.log('Perplexity model changed to', value);
oai_settings.perplexity_model = value;
}
if ($(this).is('#model_groq_select')) {
if (!value || !hasModelsLoaded) {
console.debug('Null Groq model selected. Ignoring.');
return;
}
console.log('Groq model changed to', value);
oai_settings.groq_model = value;
}
if ($(this).is('#model_siliconflow_select')) {
if (!value) {
console.debug('Null SiliconFlow model selected. Ignoring.');
return;
}
console.log('SiliconFlow model changed to', value);
oai_settings.siliconflow_model = value;
}
if ($(this).is('#model_electronhub_select')) {
if (!value || !hasModelsLoaded) {
console.debug('Null ElectronHub model selected. Ignoring.');
return;
}
console.log('ElectronHub model changed to', value);
oai_settings.electronhub_model = value;
}
if ($(this).is('#model_chutes_select')) {
if (!value || !hasModelsLoaded) {
console.debug('Null Chutes model selected. Ignoring.');
return;
}
console.log('Chutes model changed to', value);
oai_settings.chutes_model = value;
}
if ($(this).is('#model_nanogpt_select')) {
if (!value || !hasModelsLoaded) {
console.debug('Null NanoGPT model selected. Ignoring.');
return;
}
console.log('NanoGPT model changed to', value);
oai_settings.nanogpt_model = value;
}
if ($(this).is('#model_deepseek_select')) {
if (!value) {
console.debug('Null DeepSeek model selected. Ignoring.');
return;
}
console.log('DeepSeek model changed to', value);
oai_settings.deepseek_model = value;
}
if (value && $(this).is('#model_custom_select')) {
console.log('Custom model changed to', value);
oai_settings.custom_model = value;
$('#custom_model_id').val(value).trigger('input');
}
if (value && $(this).is('#model_pollinations_select')) {
console.log('Pollinations model changed to', value);
oai_settings.pollinations_model = value;
}
if ($(this).is('#model_aimlapi_select')) {
if (!value || !hasModelsLoaded) {
console.debug('Null AI/ML model selected. Ignoring.');
return;
}
console.log('AI/ML model changed to', value);
oai_settings.aimlapi_model = value;
}
if ($(this).is('#model_xai_select')) {
if (!value) {
console.debug('Null XAI model selected. Ignoring.');
return;
}
console.log('XAI model changed to', value);
oai_settings.xai_model = value;
}
if ($(this).is('#model_moonshot_select')) {
if (!value || !hasModelsLoaded) {
console.debug('Null Moonshot model selected. Ignoring.');
return;
}
console.log('Moonshot model changed to', value);
oai_settings.moonshot_model = value;
}
if ($(this).is('#model_fireworks_select')) {
if (!value || !hasModelsLoaded) {
console.debug('Null Fireworks model selected. Ignoring.');
return;
}
console.log('Fireworks model changed to', value);
oai_settings.fireworks_model = value;
}
if ($(this).is('#model_cometapi_select')) {
if (!value) {
console.debug('Null CometAPI model selected. Ignoring.');
return;
}
console.log('CometAPI model changed to', value);
oai_settings.cometapi_model = value;
}
if ($(this).is('#azure_openai_model')) {
if (!value) {
console.debug('Null Azure OpenAI model selected. Ignoring.');
return;
}
oai_settings.azure_openai_model = value;
}
if ($(this).is('#model_zai_select')) {
console.log('ZAI model changed to', value);
oai_settings.zai_model = value;
}
if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(oai_settings.chat_completion_source)) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', max_2mil);
} else if (value.includes('gemini-2.5-flash-image')) {
$('#openai_max_context').attr('max', max_32k);
} else if (value.includes('gemini-3-pro-image')) {
$('#openai_max_context').attr('max', max_64k);
} else if (/gemini-3-(pro|flash)/.test(value) || /gemini-2.5-(pro|flash)/.test(value) || /gemini-2.0-(pro|flash)/.test(value)) {
$('#openai_max_context').attr('max', max_1mil);
} else if (value.includes('gemini-exp') || value.includes('learnlm-2.0-flash') || value.includes('gemini-robotics')) {
$('#openai_max_context').attr('max', max_1mil);
} else if (value.includes('gemma-3-27b-it')) {
$('#openai_max_context').attr('max', max_128k);
} else if (value.includes('gemma-3n-e4b-it')) {
$('#openai_max_context').attr('max', max_8k);
} else if (value.includes('gemma-3')) {
$('#openai_max_context').attr('max', max_32k);
} else {
$('#openai_max_context').attr('max', max_32k);
}
let makersuite_max_temp = (value.includes('vision') || value.includes('ultra') || value.includes('gemma')) ? 1.0 : 2.0;
oai_settings.temp_openai = Math.min(makersuite_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', makersuite_max_temp).val(oai_settings.temp_openai).trigger('input');
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
}
if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
} else {
const model = model_list.find(m => m.id == oai_settings.openrouter_model);
if (model?.context_length) {
$('#openai_max_context').attr('max', model.context_length);
} else {
$('#openai_max_context').attr('max', max_128k);
}
}
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
if (value && (value.includes('claude') || value.includes('palm-2'))) {
oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
}
else {
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
calculateOpenRouterCost();
}
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
}
else if (value.startsWith('claude-sonnet-4-5')) {
$('#openai_max_context').attr('max', max_1mil);
}
else if (value == 'claude-2.1' || value.startsWith('claude-3') || value.startsWith('claude-opus') || value.startsWith('claude-haiku') || value.startsWith('claude-sonnet')) {
$('#openai_max_context').attr('max', max_200k);
}
else if (value.endsWith('100k') || value.startsWith('claude-2') || value === 'claude-instant-1.2') {
$('#openai_max_context').attr('max', claude_100k_max);
}
else {
$('#openai_max_context').attr('max', claude_max);
}
oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
$('#openai_reverse_proxy').attr('placeholder', 'https://api.anthropic.com/v1');
oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if ([chat_completion_sources.AZURE_OPENAI, chat_completion_sources.OPENAI].includes(oai_settings.chat_completion_source)) {
$('#openai_max_context').attr('max', getMaxContextOpenAI(value));
oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
$('#openai_reverse_proxy').attr('placeholder', 'https://api.openai.com/v1');
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.MISTRALAI) {
const maxContext = getMistralMaxContext(oai_settings.mistralai_model, oai_settings.max_context_unlocked);
$('#openai_max_context').attr('max', maxContext);
oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
oai_settings.temp_openai = Math.min(mistral_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', mistral_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
}
else if (['command-light-nightly', 'command-light', 'command'].includes(oai_settings.cohere_model)) {
$('#openai_max_context').attr('max', max_4k);
}
else if (oai_settings.cohere_model.includes('command-r') || ['c4ai-aya-23', 'c4ai-aya-expanse-32b', 'command-nightly', 'command-a-vision-07-2025'].includes(oai_settings.cohere_model)) {
$('#openai_max_context').attr('max', max_128k);
}
else if (['command-a-03-2025'].includes(oai_settings.cohere_model)) {
$('#openai_max_context').attr('max', max_256k);
}
else if (['c4ai-aya-23-8b', 'c4ai-aya-expanse-8b'].includes(oai_settings.cohere_model)) {
$('#openai_max_context').attr('max', max_8k);
}
else if (['c4ai-aya-vision-8b', 'c4ai-aya-vision-32b'].includes(oai_settings.cohere_model)) {
$('#openai_max_context').attr('max', max_16k);
}
else {
$('#openai_max_context').attr('max', max_4k);
}
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
$('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
}
else if (['sonar', 'sonar-reasoning', 'sonar-reasoning-pro', 'r1-1776'].includes(oai_settings.perplexity_model)) {
$('#openai_max_context').attr('max', 127000);
}
else if (['sonar-pro'].includes(oai_settings.perplexity_model)) {
$('#openai_max_context').attr('max', 200000);
}
else if (oai_settings.perplexity_model.includes('llama-3.1')) {
const isOnline = oai_settings.perplexity_model.includes('online');
const contextSize = isOnline ? 128 * 1024 - 4000 : 128 * 1024;
$('#openai_max_context').attr('max', contextSize);
}
else {
$('#openai_max_context').attr('max', max_128k);
}
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
const maxContext = getGroqMaxContext(oai_settings.groq_model, oai_settings.max_context_unlocked);
$('#openai_max_context').attr('max', maxContext);
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source == chat_completion_sources.AI21) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
} else if (oai_settings.ai21_model.startsWith('jamba-')) {
$('#openai_max_context').attr('max', max_256k);
}
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) {
$('#openai_max_context').attr('max', unlocked_max);
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES) {
const maxContext = getChutesMaxContext(oai_settings.chutes_model, oai_settings.max_context_unlocked);
$('#openai_max_context').attr('max', maxContext);
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
calculateChutesCost();
}
if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
const maxContext = getElectronHubMaxContext(oai_settings.electronhub_model, oai_settings.max_context_unlocked);
$('#openai_max_context').attr('max', maxContext);
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
calculateElectronHubCost();
}
if (oai_settings.chat_completion_source === chat_completion_sources.NANOGPT) {
const maxContext = getNanoGptMaxContext(oai_settings.nanogpt_model, oai_settings.max_context_unlocked);
$('#openai_max_context').attr('max', maxContext);
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
} else {
$('#openai_max_context').attr('max', max_128k);
}
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
} else if (['deepseek-reasoner', 'deepseek-chat'].includes(oai_settings.deepseek_model)) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.deepseek_model == 'deepseek-coder') {
$('#openai_max_context').attr('max', max_16k);
} else {
$('#openai_max_context').attr('max', max_64k);
}
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.COMETAPI) {
$('#openai_max_context').attr('max', oai_settings.max_context_unlocked ? unlocked_max : max_128k);
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.XAI) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
} else if (oai_settings.xai_model.includes('grok-2-vision')) {
$('#openai_max_context').attr('max', max_32k);
} else if (oai_settings.xai_model.includes('grok-4-fast')) {
$('#openai_max_context').attr('max', max_2mil);
} else if (oai_settings.xai_model.includes('grok-4')) {
$('#openai_max_context').attr('max', max_256k);
} else if (oai_settings.xai_model.includes('grok-code')) {
$('#openai_max_context').attr('max', max_256k);
} else {
// grok 2 and grok 3
$('#openai_max_context').attr('max', max_128k);
}
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.AIMLAPI) {
let maxContext;
if (oai_settings.max_context_unlocked) {
maxContext = unlocked_max;
} else {
const model = model_list.find(m => m.id === oai_settings.aimlapi_model);
maxContext = (model?.info?.contextLength ?? model?.context_length) || max_32k;
console.log('[AI/ML API] Model CTX:', model?.info?.contextLength);
}
$('#openai_max_context')
.prop('max', maxContext)
.val(Math.min(Number(oai_settings.openai_max_context), maxContext))
.trigger('input');
$('#temp_openai')
.prop('max', oai_max_temp)
.val(Number(oai_settings.temp_openai))
.trigger('input');
oai_settings.openai_max_context = Number($('#openai_max_context').val());
oai_settings.temp_openai = Number($('#temp_openai').val());
}
if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
oai_settings.pres_pen_openai = Math.min(Math.max(0, oai_settings.pres_pen_openai), 1);
$('#pres_pen_openai').attr('max', 1).attr('min', 0).val(oai_settings.pres_pen_openai).trigger('input');
oai_settings.freq_pen_openai = Math.min(Math.max(0, oai_settings.freq_pen_openai), 1);
$('#freq_pen_openai').attr('max', 1).attr('min', 0).val(oai_settings.freq_pen_openai).trigger('input');
} else {
$('#pres_pen_openai').attr('max', 2).attr('min', -2).val(oai_settings.pres_pen_openai).trigger('input');
$('#freq_pen_openai').attr('max', 2).attr('min', -2).val(oai_settings.freq_pen_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.MOONSHOT) {
const maxContext = getMoonshotMaxContext(oai_settings.moonshot_model, oai_settings.max_context_unlocked);
$('#openai_max_context').attr('max', maxContext);
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.FIREWORKS) {
const maxContext = getFireworksMaxContext(oai_settings.fireworks_model, oai_settings.max_context_unlocked);
$('#openai_max_context').attr('max', maxContext);
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.SILICONFLOW) {
const maxContext = getSiliconflowMaxContext(oai_settings.siliconflow_model, oai_settings.max_context_unlocked);
$('#openai_max_context').attr('max', maxContext);
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source == chat_completion_sources.ZAI) {
const maxContext = getZaiMaxContext(oai_settings.zai_model, oai_settings.max_context_unlocked);
$('#openai_max_context').attr('max', maxContext);
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
}
$('#openai_max_context_counter').attr('max', Number($('#openai_max_context').attr('max')));
saveSettingsDebounced();
updateFeatureSupportFlags();
eventSource.emit(event_types.CHATCOMPLETION_MODEL_CHANGED, value);
}
async function onOpenrouterModelSortChange() {
await getStatusOpen();
}
async function onChutesModelSortChange() {
await getStatusOpen();
}
async function onElectronHubModelSortChange() {
await getStatusOpen();
}
async function onNewPresetClick() {
const name = await Popup.show.input(t`Preset name:`, t`Hint: Use a character/group name to bind preset to a specific chat.`, oai_settings.preset_settings_openai);
if (!name) {
return;
}
await saveOpenAIPreset(name, oai_settings);
}
function onReverseProxyInput() {
oai_settings.reverse_proxy = String($(this).val());
$('.reverse_proxy_warning').toggle(oai_settings.reverse_proxy != '');
saveSettingsDebounced();
}
async function onConnectButtonClick(e) {
e.stopPropagation();
/** @type {Object.<string, {key: string, selector: string, proxy?: boolean, keyless?: boolean}>} */
const apiSourceConfig = {
[chat_completion_sources.OPENROUTER]: { key: SECRET_KEYS.OPENROUTER, selector: '#api_key_openrouter', proxy: false },
[chat_completion_sources.MAKERSUITE]: { key: SECRET_KEYS.MAKERSUITE, selector: '#api_key_makersuite', proxy: true },
[chat_completion_sources.CLAUDE]: { key: SECRET_KEYS.CLAUDE, selector: '#api_key_claude', proxy: true },
[chat_completion_sources.OPENAI]: { key: SECRET_KEYS.OPENAI, selector: '#api_key_openai', proxy: true },
[chat_completion_sources.AI21]: { key: SECRET_KEYS.AI21, selector: '#api_key_ai21', proxy: false },
[chat_completion_sources.MISTRALAI]: { key: SECRET_KEYS.MISTRALAI, selector: '#api_key_mistralai', proxy: true },
[chat_completion_sources.CUSTOM]: { key: SECRET_KEYS.CUSTOM, selector: '#api_key_custom', proxy: false, keyless: true },
[chat_completion_sources.COHERE]: { key: SECRET_KEYS.COHERE, selector: '#api_key_cohere', proxy: false },
[chat_completion_sources.PERPLEXITY]: { key: SECRET_KEYS.PERPLEXITY, selector: '#api_key_perplexity', proxy: false },
[chat_completion_sources.GROQ]: { key: SECRET_KEYS.GROQ, selector: '#api_key_groq', proxy: false },
[chat_completion_sources.SILICONFLOW]: { key: SECRET_KEYS.SILICONFLOW, selector: '#api_key_siliconflow', proxy: false },
[chat_completion_sources.ELECTRONHUB]: { key: SECRET_KEYS.ELECTRONHUB, selector: '#api_key_electronhub', proxy: false },
[chat_completion_sources.NANOGPT]: { key: SECRET_KEYS.NANOGPT, selector: '#api_key_nanogpt', proxy: false },
[chat_completion_sources.DEEPSEEK]: { key: SECRET_KEYS.DEEPSEEK, selector: '#api_key_deepseek', proxy: true },
[chat_completion_sources.XAI]: { key: SECRET_KEYS.XAI, selector: '#api_key_xai', proxy: true },
[chat_completion_sources.AIMLAPI]: { key: SECRET_KEYS.AIMLAPI, selector: '#api_key_aimlapi', proxy: false },
[chat_completion_sources.MOONSHOT]: { key: SECRET_KEYS.MOONSHOT, selector: '#api_key_moonshot', proxy: false },
[chat_completion_sources.FIREWORKS]: { key: SECRET_KEYS.FIREWORKS, selector: '#api_key_fireworks', proxy: false },
[chat_completion_sources.COMETAPI]: { key: SECRET_KEYS.COMETAPI, selector: '#api_key_cometapi', proxy: false },
[chat_completion_sources.AZURE_OPENAI]: { key: SECRET_KEYS.AZURE_OPENAI, selector: '#api_key_azure_openai', proxy: false },
[chat_completion_sources.ZAI]: { key: SECRET_KEYS.ZAI, selector: '#api_key_zai', proxy: false },
[chat_completion_sources.CHUTES]: { key: SECRET_KEYS.CHUTES, selector: '#api_key_chutes', proxy: false },
};
// Vertex AI Express version - use API key
if (oai_settings.vertexai_auth_mode === 'express') {
apiSourceConfig[chat_completion_sources.VERTEXAI] = { key: SECRET_KEYS.VERTEXAI, selector: '#api_key_vertexai', proxy: true };
}
// Vertex AI Full version - use service account
if (oai_settings.chat_completion_source === chat_completion_sources.VERTEXAI && oai_settings.vertexai_auth_mode === 'full') {
if (!secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) {
toastr.error(t`Service Account JSON is required for Vertex AI full version. Please validate and save your Service Account JSON.`);
return;
}
}
// Other generic configs
const config = apiSourceConfig[oai_settings.chat_completion_source];
if (config) {
const apiKey = String($(config.selector).val()).trim();
if (apiKey.length) {
await writeSecret(config.key, apiKey);
}
if (!secret_state[config.key] && (!config.proxy || !oai_settings.reverse_proxy) && !config.keyless) {
console.log(`No secret key saved for ${oai_settings.chat_completion_source}`);
return;
}
}
startStatusLoading();
saveSettingsDebounced();
await getStatusOpen();
}
function toggleChatCompletionForms() {
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
$('#model_claude_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.OPENAI) {
if (oai_settings.show_external_models && (!Array.isArray(model_list) || model_list.length == 0)) {
// Wait until the models list is loaded so that we could show a proper saved model
}
else {
$('#model_openai_select').trigger('change');
}
}
else if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
$('#model_google_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) {
$('#model_vertexai_select').trigger('change');
// Update UI based on authentication mode
onVertexAIAuthModeChange.call($('#vertexai_auth_mode')[0]);
}
else if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {
$('#model_openrouter_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.AI21) {
$('#model_ai21_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) {
$('#model_mistralai_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) {
$('#model_cohere_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY) {
$('#model_perplexity_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
$('#model_groq_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES) {
$('#model_chutes_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.SILICONFLOW) {
$('#model_siliconflow_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
$('#model_electronhub_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) {
$('#model_nanogpt_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) {
$('#model_custom_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {
$('#model_deepseek_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI) {
$('#model_aimlapi_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.XAI) {
$('#model_xai_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS) {
$('#model_pollinations_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT) {
$('#model_moonshot_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.FIREWORKS) {
$('#model_fireworks_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.COMETAPI) {
$('#model_cometapi_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI) {
$('#azure_openai_model').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.ZAI) {
$('#model_zai_select').trigger('change');
}
$('[data-source]').each(function () {
const mode = $(this).data('source-mode');
const validSources = $(this).data('source').split(',');
const matchesSource = validSources.includes(oai_settings.chat_completion_source);
$(this).toggle(mode !== 'except' ? matchesSource : !matchesSource);
});
}
async function testApiConnection() {
// Check if the previous request is still in progress
if (is_send_press) {
toastr.info(t`Please wait for the previous request to complete.`);
return;
}
try {
const reply = await sendOpenAIRequest('quiet', [{ 'role': 'user', 'content': 'Hi' }], new AbortController().signal);
console.log(reply);
toastr.success(t`API connection successful!`);
}
catch (err) {
toastr.error(t`Could not get a reply from API. Check your connection settings / API key and try again.`);
}
}
function reconnectOpenAi() {
if (main_api == 'openai') {
setOnlineStatus('no_connection');
resultCheckStatus();
$('#api_button_openai').trigger('click');
}
}
function onProxyPasswordShowClick() {
const $input = $('#openai_proxy_password');
const type = $input.attr('type') === 'password' ? 'text' : 'password';
$input.attr('type', type);
$(this).toggleClass('fa-eye-slash fa-eye');
}
async function onCustomizeParametersClick() {
const template = $(await renderTemplateAsync('customEndpointAdditionalParameters'));
template.find('#custom_include_body').val(oai_settings.custom_include_body).on('input', function () {
oai_settings.custom_include_body = String($(this).val());
saveSettingsDebounced();
});
template.find('#custom_exclude_body').val(oai_settings.custom_exclude_body).on('input', function () {
oai_settings.custom_exclude_body = String($(this).val());
saveSettingsDebounced();
});
template.find('#custom_include_headers').val(oai_settings.custom_include_headers).on('input', function () {
oai_settings.custom_include_headers = String($(this).val());
saveSettingsDebounced();
});
await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true });
}
/**
* Check if the model supports image inlining
* @returns {boolean} True if the model supports image inlining
*/
export function isImageInliningSupported() {
if (main_api !== 'openai') {
return false;
}
if (!oai_settings.media_inlining) {
return false;
}
// gultra just isn't being offered as multimodal, thanks google.
const visionSupportedModels = [
// OpenAI
'chatgpt-4o-latest',
'gpt-4-turbo',
'gpt-4-vision',
'gpt-4.1',
'gpt-4.5-preview',
'gpt-4o',
'gpt-5',
'o1',
'o3',
'o4-mini',
// Claude
'claude-3',
'claude-opus-4',
'claude-sonnet-4',
'claude-haiku-4',
// Cohere
'c4ai-aya-vision',
'command-a-vision',
// Google AI Studio
'gemini-2.0',
'gemini-2.5',
'gemini-3',
'gemini-exp-1206',
'learnlm',
'gemini-robotics',
// MistralAI
'mistral-small-2503',
'mistral-small-2506',
'mistral-small-latest',
'mistral-medium-latest',
'mistral-medium-2505',
'mistral-medium-2508',
'pixtral',
// xAI (Grok)
'grok-4',
'grok-2-vision',
// Moonshot
'moonshot-v1-8k-vision-preview',
'moonshot-v1-32k-vision-preview',
'moonshot-v1-128k-vision-preview',
// Z.AI (GLM)
'glm-4.5v',
'glm-4.6v',
'autoglm-phone',
// SiliconFlow
'Qwen/Qwen3-VL-32B-Instruct',
'Qwen/Qwen3-VL-8B-Instruct',
'Qwen/Qwen3-VL-235B-A22B-Instruct',
'Qwen/Qwen3-VL-30B-A3B-Instruct',
'zai-org/GLM-4.5V',
];
switch (oai_settings.chat_completion_source) {
case chat_completion_sources.OPENAI:
case chat_completion_sources.AZURE_OPENAI: {
const modelToCheck = oai_settings.chat_completion_source === chat_completion_sources.AZURE_OPENAI
? oai_settings.azure_openai_model
: oai_settings.openai_model;
return visionSupportedModels.some(model =>
modelToCheck.includes(model)
&& ['gpt-4-turbo-preview', 'o1-mini', 'o3-mini'].some(x => !modelToCheck.includes(x)),
);
}
case chat_completion_sources.MAKERSUITE:
return visionSupportedModels.some(model => oai_settings.google_model.includes(model));
case chat_completion_sources.VERTEXAI:
return visionSupportedModels.some(model => oai_settings.vertexai_model.includes(model));
case chat_completion_sources.CLAUDE:
return visionSupportedModels.some(model => oai_settings.claude_model.includes(model));
case chat_completion_sources.OPENROUTER:
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.openrouter_model)?.architecture?.input_modalities?.includes('image'));
case chat_completion_sources.CUSTOM:
return true;
case chat_completion_sources.MISTRALAI:
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.mistralai_model)?.capabilities?.vision);
case chat_completion_sources.COHERE:
return visionSupportedModels.some(model => oai_settings.cohere_model.includes(model));
case chat_completion_sources.XAI:
// TODO: xAI's /models endpoint doesn't return modality info
return visionSupportedModels.some(model => oai_settings.xai_model.includes(model));
case chat_completion_sources.AIMLAPI:
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.aimlapi_model)?.features?.includes('openai/chat-completion.vision'));
case chat_completion_sources.CHUTES:
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.chutes_model)?.input_modalities?.includes('image'));
case chat_completion_sources.ELECTRONHUB:
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.electronhub_model)?.metadata?.vision);
case chat_completion_sources.POLLINATIONS:
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.pollinations_model)?.vision);
case chat_completion_sources.COMETAPI:
return true;
case chat_completion_sources.MOONSHOT:
return visionSupportedModels.some(model => oai_settings.moonshot_model.includes(model));
case chat_completion_sources.NANOGPT:
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.nanogpt_model)?.capabilities?.vision);
case chat_completion_sources.ZAI:
return visionSupportedModels.some(model => oai_settings.zai_model.includes(model));
case chat_completion_sources.SILICONFLOW:
return visionSupportedModels.some(model => oai_settings.siliconflow_model.includes(model));
default:
return false;
}
}
/**
* Check if the model supports video inlining
* @returns {boolean} True if the model supports video inlining
*/
export function isVideoInliningSupported() {
if (main_api !== 'openai') {
return false;
}
if (!oai_settings.media_inlining) {
return false;
}
const videoSupportedModels = [
// Gemini
'gemini-2.0',
'gemini-2.5',
'gemini-exp-1206',
'gemini-3',
// Z.AI (GLM)
'glm-4.5v',
'glm-4.6v',
];
switch (oai_settings.chat_completion_source) {
case chat_completion_sources.MAKERSUITE:
return videoSupportedModels.some(model => oai_settings.google_model.includes(model));
case chat_completion_sources.VERTEXAI:
return videoSupportedModels.some(model => oai_settings.vertexai_model.includes(model));
case chat_completion_sources.OPENROUTER:
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.openrouter_model)?.architecture?.input_modalities?.includes('video'));
case chat_completion_sources.ZAI:
return videoSupportedModels.some(model => oai_settings.zai_model.includes(model));
default:
return false;
}
}
/**
* Check if the model supports video inlining
* @returns {boolean} True if the model supports audio inlining
*/
export function isAudioInliningSupported() {
if (main_api !== 'openai') {
return false;
}
if (!oai_settings.media_inlining) {
return false;
}
// Only Gemini models support audio for now
const audioSupportedModels = [
'gemini-2.0',
'gemini-2.5',
'gemini-3',
'gemini-exp-1206',
];
switch (oai_settings.chat_completion_source) {
case chat_completion_sources.MAKERSUITE:
return audioSupportedModels.some(model => oai_settings.google_model.includes(model));
case chat_completion_sources.VERTEXAI:
return audioSupportedModels.some(model => oai_settings.vertexai_model.includes(model));
case chat_completion_sources.OPENROUTER:
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.openrouter_model)?.architecture?.input_modalities?.includes('audio'));
default:
return false;
}
}
/**
* Check if the model supports encrypted reasoning signatures.
* @param {ChatCompletionSettings} settings Settings object to use
* @returns {boolean} True if reasoning signatures should be included in the request
*/
export function isReasoningSignatureSupported(settings = oai_settings) {
// If it's Vertex AI or Makersuite, that's OK - convertGooglePrompt() will handle it later
const isGoogle = [chat_completion_sources.VERTEXAI, chat_completion_sources.MAKERSUITE].includes(settings.chat_completion_source);
// Need a more crunchy check for OpenRouter: look for Gemini models
const isOpenRouterGemini = settings.chat_completion_source === chat_completion_sources.OPENROUTER && /google\/gemini/i.test(settings.openrouter_model);
return isGoogle || isOpenRouterGemini;
}
/**
* Proxy stuff
*/
export function loadProxyPresets(settings) {
let proxyPresets = settings.proxies;
selected_proxy = settings.selected_proxy || selected_proxy;
if (!Array.isArray(proxyPresets) || proxyPresets.length === 0) {
proxyPresets = proxies;
} else {
proxies = proxyPresets;
}
$('#openai_proxy_preset').empty();
for (const preset of proxyPresets) {
const option = document.createElement('option');
option.innerText = preset.name;
option.value = preset.name;
option.selected = preset.name === 'None';
$('#openai_proxy_preset').append(option);
}
$('#openai_proxy_preset').val(selected_proxy.name);
setProxyPreset(selected_proxy.name, selected_proxy.url, selected_proxy.password);
}
function setProxyPreset(name, url, password) {
const preset = proxies.find(p => p.name === name);
if (preset) {
preset.url = url;
preset.password = password;
selected_proxy = preset;
} else {
let new_proxy = { name, url, password };
proxies.push(new_proxy);
selected_proxy = new_proxy;
}
$('#openai_reverse_proxy_name').val(name);
oai_settings.reverse_proxy = url;
$('#openai_reverse_proxy').val(oai_settings.reverse_proxy);
oai_settings.proxy_password = password;
$('#openai_proxy_password').val(oai_settings.proxy_password);
reconnectOpenAi();
}
function onProxyPresetChange() {
const value = String($('#openai_proxy_preset').find(':selected').val());
const selectedPreset = proxies.find(preset => preset.name === value);
if (selectedPreset) {
setProxyPreset(selectedPreset.name, selectedPreset.url, selectedPreset.password);
} else {
console.error(t`Proxy preset '${value}' not found in proxies array.`);
}
saveSettingsDebounced();
}
$('#save_proxy').on('click', async function () {
const presetName = $('#openai_reverse_proxy_name').val();
const reverseProxy = $('#openai_reverse_proxy').val();
const proxyPassword = $('#openai_proxy_password').val();
setProxyPreset(presetName, reverseProxy, proxyPassword);
saveSettingsDebounced();
toastr.success(t`Proxy Saved`);
if ($('#openai_proxy_preset').val() !== presetName) {
const option = document.createElement('option');
option.text = String(presetName);
option.value = String(presetName);
$('#openai_proxy_preset').append(option);
}
$('#openai_proxy_preset').val(presetName);
});
$('#delete_proxy').on('click', async function () {
const presetName = $('#openai_reverse_proxy_name').val();
const index = proxies.findIndex(preset => preset.name === presetName);
if (index !== -1) {
proxies.splice(index, 1);
$('#openai_proxy_preset option[value="' + presetName + '"]').remove();
if (proxies.length > 0) {
const newIndex = Math.max(0, index - 1);
selected_proxy = proxies[newIndex];
} else {
selected_proxy = { name: 'None', url: '', password: '' };
}
$('#openai_reverse_proxy_name').val(selected_proxy.name);
oai_settings.reverse_proxy = selected_proxy.url;
$('#openai_reverse_proxy').val(selected_proxy.url);
oai_settings.proxy_password = selected_proxy.password;
$('#openai_proxy_password').val(selected_proxy.password);
saveSettingsDebounced();
$('#openai_proxy_preset').val(selected_proxy.name);
toastr.success(t`Proxy Deleted`);
} else {
toastr.error(t`Could not find proxy with name '${presetName}'`);
}
});
function runProxyCallback(_, value) {
if (!value) {
return selected_proxy?.name || '';
}
const proxyNames = proxies.map(preset => preset.name);
const fuse = new Fuse(proxyNames);
const result = fuse.search(value);
if (result.length === 0) {
toastr.warning(t`Proxy preset '${value}' not found`);
return '';
}
const foundName = result[0].item;
$('#openai_proxy_preset').val(foundName).trigger('change');
return foundName;
}
/**
* Handle Vertex AI authentication mode change
*/
function onVertexAIAuthModeChange() {
const authMode = String($(this).val());
oai_settings.vertexai_auth_mode = authMode;
$('#vertexai_form [data-mode]').each(function () {
const mode = $(this).data('mode');
$(this).toggle(mode === authMode);
$(this).find('option').toggle(mode === authMode);
});
saveSettingsDebounced();
}
/**
* Validate Vertex AI service account JSON
*/
async function onVertexAIValidateServiceAccount() {
const jsonContent = String($('#vertexai_service_account_json').val()).trim();
if (!jsonContent) {
toastr.error(t`Please enter Service Account JSON content`);
return;
}
try {
const serviceAccount = JSON.parse(jsonContent);
const requiredFields = ['type', 'project_id', 'private_key', 'client_email', 'client_id'];
const missingFields = requiredFields.filter(field => !serviceAccount[field]);
if (missingFields.length > 0) {
toastr.error(t`Missing required fields: ${missingFields.join(', ')}`);
updateVertexAIServiceAccountStatus(false, t`Missing fields: ${missingFields.join(', ')}`);
return;
}
if (serviceAccount.type !== 'service_account') {
toastr.error(t`Invalid service account type. Expected "service_account"`);
updateVertexAIServiceAccountStatus(false, t`Invalid service account type`);
return;
}
// Save to backend secret storage
const keyLabel = serviceAccount['client_email'] || '';
await writeSecret(SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, jsonContent, keyLabel);
// Show success status
updateVertexAIServiceAccountStatus(true, `Project: ${serviceAccount.project_id}, Email: ${serviceAccount.client_email}`);
toastr.success(t`Service Account JSON is valid and saved securely`);
saveSettingsDebounced();
} catch (error) {
console.error('JSON validation error:', error);
toastr.error(t`Invalid JSON format`);
updateVertexAIServiceAccountStatus(false, t`Invalid JSON format`);
}
}
/**
* Clear Vertex AI service account JSON
*/
async function onVertexAIClearServiceAccount() {
$('#vertexai_service_account_json').val('');
// Clear from backend secret storage
await writeSecret(SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, '');
updateVertexAIServiceAccountStatus(false);
toastr.info(t`Service Account JSON cleared`);
saveSettingsDebounced();
}
/**
* Handle Vertex AI service account JSON input change
*/
function onVertexAIServiceAccountJsonChange() {
const jsonContent = String($(this).val()).trim();
// Autocomplete has been triggered, don't validate if the input is a UUID
if (isUuid(jsonContent)) {
return;
}
if (jsonContent) {
// Auto-validate when content is pasted
try {
const serviceAccount = JSON.parse(jsonContent);
const requiredFields = ['type', 'project_id', 'private_key', 'client_email'];
const hasAllFields = requiredFields.every(field => serviceAccount[field]);
if (hasAllFields && serviceAccount.type === 'service_account') {
updateVertexAIServiceAccountStatus(false, t`JSON appears valid - click "Validate JSON" to save`);
} else {
updateVertexAIServiceAccountStatus(false, t`Incomplete or invalid JSON`);
}
} catch (error) {
updateVertexAIServiceAccountStatus(false, t`Invalid JSON format`);
}
} else {
updateVertexAIServiceAccountStatus(false);
}
// Don't save settings automatically
// saveSettingsDebounced();
}
/**
* Update the Vertex AI service account status display
* @param {boolean} isValid - Whether the service account is valid
* @param {string} message - Status message to display
*/
function updateVertexAIServiceAccountStatus(isValid = false, message = '') {
const statusDiv = $('#vertexai_service_account_status');
const infoSpan = $('#vertexai_service_account_info');
// If no explicit message provided, check if we have a saved service account
if (!message && secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) {
isValid = true;
message = t`Service Account JSON is saved and ready to use`;
}
if (isValid && message) {
infoSpan.html(`<i class="fa-solid fa-check-circle" style="color: green;"></i> ${message}`);
statusDiv.show();
} else if (!isValid && message) {
infoSpan.html(`<i class="fa-solid fa-exclamation-triangle" style="color: orange;"></i> ${message}`);
statusDiv.show();
} else {
statusDiv.hide();
}
}
function updateFeatureSupportFlags() {
const featureFlags = {
openai_function_calling_supported: ToolManager.isToolCallingSupported(),
openai_image_inlining_supported: isImageInliningSupported(),
openai_video_inlining_supported: isVideoInliningSupported(),
openai_audio_inlining_supported: isAudioInliningSupported(),
};
for (const [key, value] of Object.entries(featureFlags)) {
const element = document.getElementById(key);
if (element) {
element.dataset.ccToggle = String(value ?? false);
}
}
}
export function initOpenAI() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'proxy',
callback: runProxyCallback,
returns: 'current proxy',
namedArgumentList: [],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: () => proxies.map(preset => new SlashCommandEnumValue(preset.name, preset.url)),
}),
],
helpString: 'Sets a proxy preset by name.',
}));
$('#test_api_button').on('click', testApiConnection);
$('#temp_openai').on('input', function () {
oai_settings.temp_openai = Number($(this).val());
$('#temp_counter_openai').val(Number($(this).val()).toFixed(2));
saveSettingsDebounced();
});
$('#freq_pen_openai').on('input', function () {
oai_settings.freq_pen_openai = Number($(this).val());
$('#freq_pen_counter_openai').val(Number($(this).val()).toFixed(2));
saveSettingsDebounced();
});
$('#pres_pen_openai').on('input', function () {
oai_settings.pres_pen_openai = Number($(this).val());
$('#pres_pen_counter_openai').val(Number($(this).val()).toFixed(2));
saveSettingsDebounced();
});
$('#top_p_openai').on('input', function () {
oai_settings.top_p_openai = Number($(this).val());
$('#top_p_counter_openai').val(Number($(this).val()).toFixed(2));
saveSettingsDebounced();
});
$('#top_k_openai').on('input', function () {
oai_settings.top_k_openai = Number($(this).val());
$('#top_k_counter_openai').val(Number($(this).val()).toFixed(0));
saveSettingsDebounced();
});
$('#top_a_openai').on('input', function () {
oai_settings.top_a_openai = Number($(this).val());
$('#top_a_counter_openai').val(Number($(this).val()));
saveSettingsDebounced();
});
$('#min_p_openai').on('input', function () {
oai_settings.min_p_openai = Number($(this).val());
$('#min_p_counter_openai').val(Number($(this).val()));
saveSettingsDebounced();
});
$('#repetition_penalty_openai').on('input', function () {
oai_settings.repetition_penalty_openai = Number($(this).val());
$('#repetition_penalty_counter_openai').val(Number($(this).val()));
saveSettingsDebounced();
});
$('#openai_max_context').on('input', function () {
oai_settings.openai_max_context = Number($(this).val());
$('#openai_max_context_counter').val(`${$(this).val()}`);
calculateOpenRouterCost();
calculateElectronHubCost();
calculateChutesCost();
saveSettingsDebounced();
});
$('#openai_max_tokens').on('input', function () {
oai_settings.openai_max_tokens = Number($(this).val());
calculateOpenRouterCost();
calculateElectronHubCost();
calculateChutesCost();
saveSettingsDebounced();
});
$('#stream_toggle').on('change', function () {
oai_settings.stream_openai = !!$('#stream_toggle').prop('checked');
saveSettingsDebounced();
});
$('#use_sysprompt').on('change', function () {
oai_settings.use_sysprompt = !!$('#use_sysprompt').prop('checked');
saveSettingsDebounced();
});
$('#send_if_empty_textarea').on('input', function () {
oai_settings.send_if_empty = String($('#send_if_empty_textarea').val());
saveSettingsDebounced();
});
$('#impersonation_prompt_textarea').on('input', function () {
oai_settings.impersonation_prompt = String($('#impersonation_prompt_textarea').val());
saveSettingsDebounced();
});
$('#newchat_prompt_textarea').on('input', function () {
oai_settings.new_chat_prompt = String($('#newchat_prompt_textarea').val());
saveSettingsDebounced();
});
$('#newgroupchat_prompt_textarea').on('input', function () {
oai_settings.new_group_chat_prompt = String($('#newgroupchat_prompt_textarea').val());
saveSettingsDebounced();
});
$('#newexamplechat_prompt_textarea').on('input', function () {
oai_settings.new_example_chat_prompt = String($('#newexamplechat_prompt_textarea').val());
saveSettingsDebounced();
});
$('#continue_nudge_prompt_textarea').on('input', function () {
oai_settings.continue_nudge_prompt = String($('#continue_nudge_prompt_textarea').val());
saveSettingsDebounced();
});
$('#wi_format_textarea').on('input', function () {
oai_settings.wi_format = String($('#wi_format_textarea').val());
saveSettingsDebounced();
});
$('#scenario_format_textarea').on('input', function () {
oai_settings.scenario_format = String($('#scenario_format_textarea').val());
saveSettingsDebounced();
});
$('#personality_format_textarea').on('input', function () {
oai_settings.personality_format = String($('#personality_format_textarea').val());
saveSettingsDebounced();
});
$('#group_nudge_prompt_textarea').on('input', function () {
oai_settings.group_nudge_prompt = String($('#group_nudge_prompt_textarea').val());
saveSettingsDebounced();
});
$('#update_oai_preset').on('click', async function () {
const name = oai_settings.preset_settings_openai;
await saveOpenAIPreset(name, oai_settings, false);
toastr.success(t`Preset updated`);
});
$('#impersonation_prompt_restore').on('click', function () {
oai_settings.impersonation_prompt = default_impersonation_prompt;
$('#impersonation_prompt_textarea').val(oai_settings.impersonation_prompt);
saveSettingsDebounced();
});
$('#newchat_prompt_restore').on('click', function () {
oai_settings.new_chat_prompt = default_new_chat_prompt;
$('#newchat_prompt_textarea').val(oai_settings.new_chat_prompt);
saveSettingsDebounced();
});
$('#newgroupchat_prompt_restore').on('click', function () {
oai_settings.new_group_chat_prompt = default_new_group_chat_prompt;
$('#newgroupchat_prompt_textarea').val(oai_settings.new_group_chat_prompt);
saveSettingsDebounced();
});
$('#newexamplechat_prompt_restore').on('click', function () {
oai_settings.new_example_chat_prompt = default_new_example_chat_prompt;
$('#newexamplechat_prompt_textarea').val(oai_settings.new_example_chat_prompt);
saveSettingsDebounced();
});
$('#continue_nudge_prompt_restore').on('click', function () {
oai_settings.continue_nudge_prompt = default_continue_nudge_prompt;
$('#continue_nudge_prompt_textarea').val(oai_settings.continue_nudge_prompt);
saveSettingsDebounced();
});
$('#wi_format_restore').on('click', function () {
oai_settings.wi_format = default_wi_format;
$('#wi_format_textarea').val(oai_settings.wi_format);
saveSettingsDebounced();
});
$('#scenario_format_restore').on('click', function () {
oai_settings.scenario_format = default_scenario_format;
$('#scenario_format_textarea').val(oai_settings.scenario_format);
saveSettingsDebounced();
});
$('#personality_format_restore').on('click', function () {
oai_settings.personality_format = default_personality_format;
$('#personality_format_textarea').val(oai_settings.personality_format);
saveSettingsDebounced();
});
$('#group_nudge_prompt_restore').on('click', function () {
oai_settings.group_nudge_prompt = default_group_nudge_prompt;
$('#group_nudge_prompt_textarea').val(oai_settings.group_nudge_prompt);
saveSettingsDebounced();
});
$('#openai_bypass_status_check').on('input', function () {
oai_settings.bypass_status_check = !!$(this).prop('checked');
getStatusOpen();
saveSettingsDebounced();
});
$('#chat_completion_source').on('change', function () {
cancelStatusCheck('Chat Completion source changed');
model_list = [];
oai_settings.chat_completion_source = String($(this).find(':selected').val());
toggleChatCompletionForms();
saveSettingsDebounced();
reconnectOpenAi();
forceCharacterEditorTokenize();
updateFeatureSupportFlags();
eventSource.emit(event_types.CHATCOMPLETION_SOURCE_CHANGED, oai_settings.chat_completion_source);
});
$('#oai_max_context_unlocked').on('input', function (_e, data) {
oai_settings.max_context_unlocked = !!$(this).prop('checked');
if (data?.source !== 'preset') {
$('#chat_completion_source').trigger('change');
}
saveSettingsDebounced();
});
$('#openai_show_external_models').on('input', function () {
oai_settings.show_external_models = !!$(this).prop('checked');
$('#openai_external_category').toggle(oai_settings.show_external_models);
saveSettingsDebounced();
});
$('#openai_proxy_password').on('input', function () {
oai_settings.proxy_password = String($(this).val());
saveSettingsDebounced();
});
$('#claude_assistant_prefill').on('input', function () {
oai_settings.assistant_prefill = String($(this).val());
saveSettingsDebounced();
});
$('#claude_assistant_impersonation').on('input', function () {
oai_settings.assistant_impersonation = String($(this).val());
saveSettingsDebounced();
});
$('#openrouter_use_fallback').on('input', function () {
oai_settings.openrouter_use_fallback = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#openrouter_group_models').on('input', function () {
oai_settings.openrouter_group_models = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#openrouter_sort_models').on('input', function () {
oai_settings.openrouter_sort_models = String($(this).val());
saveSettingsDebounced();
});
$('#openrouter_allow_fallbacks').on('input', function () {
oai_settings.openrouter_allow_fallbacks = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#openrouter_middleout').on('input', function () {
oai_settings.openrouter_middleout = String($(this).val());
saveSettingsDebounced();
});
$('#electronhub_sort_models').on('input', function () {
oai_settings.electronhub_sort_models = String($(this).val());
saveSettingsDebounced();
});
$('#chutes_sort_models').on('input', function () {
oai_settings.chutes_sort_models = String($(this).val());
saveSettingsDebounced();
});
$('#electronhub_group_models').on('input', function () {
oai_settings.electronhub_group_models = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#squash_system_messages').on('input', function () {
oai_settings.squash_system_messages = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#openai_media_inlining').on('input', function () {
oai_settings.media_inlining = !!$(this).prop('checked');
updateFeatureSupportFlags();
saveSettingsDebounced();
});
$('#openai_inline_image_quality').on('input', function () {
oai_settings.inline_image_quality = String($(this).val());
saveSettingsDebounced();
});
$('#continue_prefill').on('input', function () {
oai_settings.continue_prefill = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#openai_function_calling').on('input', function () {
oai_settings.function_calling = !!$(this).prop('checked');
updateFeatureSupportFlags();
saveSettingsDebounced();
});
$('#seed_openai').on('input', function () {
oai_settings.seed = Number($(this).val());
saveSettingsDebounced();
});
$('#n_openai').on('input', function () {
oai_settings.n = Number($(this).val());
saveSettingsDebounced();
});
$('#custom_api_url_text').on('input', function () {
oai_settings.custom_url = String($(this).val());
saveSettingsDebounced();
});
$('#custom_model_id').on('input', function () {
oai_settings.custom_model = String($(this).val());
saveSettingsDebounced();
});
$('#custom_prompt_post_processing').on('change', function () {
oai_settings.custom_prompt_post_processing = String($(this).val());
updateFeatureSupportFlags();
saveSettingsDebounced();
});
$('#names_behavior').on('input', function () {
oai_settings.names_behavior = Number($(this).val());
setNamesBehaviorControls();
saveSettingsDebounced();
});
$('#azure_base_url').on('input', function () {
oai_settings.azure_base_url = String($(this).val());
saveSettingsDebounced();
});
$('#azure_deployment_name').on('input', function () {
oai_settings.azure_deployment_name = String($(this).val());
saveSettingsDebounced();
});
$('#azure_api_version').on('input change', function () {
oai_settings.azure_api_version = String($(this).val());
saveSettingsDebounced();
});
$('#character_names_none').on('input', function () {
oai_settings.names_behavior = character_names_behavior.NONE;
setNamesBehaviorControls();
saveSettingsDebounced();
});
$('#character_names_default').on('input', function () {
oai_settings.names_behavior = character_names_behavior.DEFAULT;
setNamesBehaviorControls();
saveSettingsDebounced();
});
$('#character_names_completion').on('input', function () {
oai_settings.names_behavior = character_names_behavior.COMPLETION;
setNamesBehaviorControls();
saveSettingsDebounced();
});
$('#character_names_content').on('input', function () {
oai_settings.names_behavior = character_names_behavior.CONTENT;
setNamesBehaviorControls();
saveSettingsDebounced();
});
$('#continue_postifx').on('input', function () {
oai_settings.continue_postfix = String($(this).val());
setContinuePostfixControls();
saveSettingsDebounced();
});
$('#continue_postfix_none').on('input', function () {
oai_settings.continue_postfix = continue_postfix_types.NONE;
setContinuePostfixControls();
saveSettingsDebounced();
});
$('#continue_postfix_space').on('input', function () {
oai_settings.continue_postfix = continue_postfix_types.SPACE;
setContinuePostfixControls();
saveSettingsDebounced();
});
$('#continue_postfix_newline').on('input', function () {
oai_settings.continue_postfix = continue_postfix_types.NEWLINE;
setContinuePostfixControls();
saveSettingsDebounced();
});
$('#continue_postfix_double_newline').on('input', function () {
oai_settings.continue_postfix = continue_postfix_types.DOUBLE_NEWLINE;
setContinuePostfixControls();
saveSettingsDebounced();
});
$('#openai_show_thoughts').on('input', function () {
oai_settings.show_thoughts = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#openai_reasoning_effort').on('input', function () {
oai_settings.reasoning_effort = String($(this).val());
saveSettingsDebounced();
});
$('#openai_verbosity').on('input', function () {
oai_settings.verbosity = String($(this).val());
saveSettingsDebounced();
});
$('#openai_enable_web_search').on('input', function () {
oai_settings.enable_web_search = !!$(this).prop('checked');
calculateOpenRouterCost();
saveSettingsDebounced();
});
$('#openai_request_images').on('input', function () {
oai_settings.request_images = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#request_image_resolution').on('input', function () {
oai_settings.request_image_resolution = String($(this).val());
saveSettingsDebounced();
});
$('#request_image_aspect_ratio').on('input', function () {
oai_settings.request_image_aspect_ratio = String($(this).val());
saveSettingsDebounced();
});
if (!CSS.supports('field-sizing', 'content')) {
$(document).on('input', '#openai_settings .autoSetHeight', function () {
resetScrollHeight($(this));
});
}
if (!isMobile()) {
$('#model_openrouter_select').select2({
placeholder: t`Select a model`,
searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getOpenRouterModelTemplate,
matcher: textValueMatcher,
});
$('#model_aimlapi_select').select2({
placeholder: t`Select a model`,
searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getAimlapiModelTemplate,
});
$('#model_electronhub_select').select2({
placeholder: t`Select a model`,
searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getElectronHubModelTemplate,
matcher: textValueMatcher,
});
$('#model_chutes_select').select2({
placeholder: t`Select a model`,
searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getChutesModelTemplate,
matcher: textValueMatcher,
});
$('#model_nanogpt_select').select2({
placeholder: t`Select a model`,
searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getNanoGptModelTemplate,
matcher: textValueMatcher,
});
$('#completion_prompt_manager_popup_entry_form_injection_trigger').select2({
placeholder: t`All types (default)`,
width: '100%',
closeOnSelect: false,
});
}
$('#openrouter_providers_chat').on('change', function () {
const selectedProviders = $(this).val();
// Not a multiple select?
if (!Array.isArray(selectedProviders)) {
return;
}
oai_settings.openrouter_providers = selectedProviders;
saveSettingsDebounced();
});
$('#bind_preset_to_connection').on('input', function () {
oai_settings.bind_preset_to_connection = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#api_button_openai').on('click', onConnectButtonClick);
$('#openai_reverse_proxy').on('input', onReverseProxyInput);
$('#model_openai_select').on('change', onModelChange);
$('#model_claude_select').on('change', onModelChange);
$('#model_google_select').on('change', onModelChange);
$('#model_vertexai_select').on('change', onModelChange);
$('#vertexai_auth_mode').on('change', onVertexAIAuthModeChange);
$('#vertexai_region').on('input', function () {
oai_settings.vertexai_region = String($(this).val());
saveSettingsDebounced();
});
$('#vertexai_express_project_id').on('input', function () {
oai_settings.vertexai_express_project_id = String($(this).val());
saveSettingsDebounced();
});
$('#zai_endpoint').on('input', function () {
oai_settings.zai_endpoint = String($(this).val());
saveSettingsDebounced();
});
$('#vertexai_service_account_json').on('input', onVertexAIServiceAccountJsonChange);
$('#vertexai_validate_service_account').on('click', onVertexAIValidateServiceAccount);
$('#vertexai_clear_service_account').on('click', onVertexAIClearServiceAccount);
$('#model_openrouter_select').on('change', onModelChange);
$('#openrouter_group_models').on('change', onOpenrouterModelSortChange);
$('#openrouter_sort_models').on('change', onOpenrouterModelSortChange);
$('#chutes_sort_models').on('change', onChutesModelSortChange);
$('#electronhub_group_models').on('change', onElectronHubModelSortChange);
$('#electronhub_sort_models').on('change', onElectronHubModelSortChange);
$('#model_ai21_select').on('change', onModelChange);
$('#model_mistralai_select').on('change', onModelChange);
$('#model_cohere_select').on('change', onModelChange);
$('#model_perplexity_select').on('change', onModelChange);
$('#model_groq_select').on('change', onModelChange);
$('#model_chutes_select').on('change', onModelChange);
$('#model_siliconflow_select').on('change', onModelChange);
$('#model_electronhub_select').on('change', onModelChange);
$('#model_nanogpt_select').on('change', onModelChange);
$('#model_deepseek_select').on('change', onModelChange);
$('#model_aimlapi_select').on('change', onModelChange);
$('#model_custom_select').on('change', onModelChange);
$('#model_xai_select').on('change', onModelChange);
$('#model_pollinations_select').on('change', onModelChange);
$('#model_cometapi_select').on('change', onModelChange);
$('#model_moonshot_select').on('change', onModelChange);
$('#model_fireworks_select').on('change', onModelChange);
$('#azure_openai_model').on('change', onModelChange);
$('#model_zai_select').on('change', onModelChange);
$('#settings_preset_openai').on('change', onSettingsPresetChange);
$('#new_oai_preset').on('click', onNewPresetClick);
$('#delete_oai_preset').on('click', onDeletePresetClick);
$('#openai_logit_bias_preset').on('change', onLogitBiasPresetChange);
$('#openai_logit_bias_new_preset').on('click', createNewLogitBiasPreset);
$('#openai_logit_bias_new_entry').on('click', createNewLogitBiasEntry);
$('#openai_logit_bias_import_file').on('input', onLogitBiasPresetImportFileChange);
$('#openai_preset_import_file').on('input', onPresetImportFileChange);
$('#export_oai_preset').on('click', onExportPresetClick);
$('#openai_logit_bias_import_preset').on('click', onLogitBiasPresetImportClick);
$('#openai_logit_bias_export_preset').on('click', onLogitBiasPresetExportClick);
$('#openai_logit_bias_delete_preset').on('click', onLogitBiasPresetDeleteClick);
$('#import_oai_preset').on('click', onImportPresetClick);
$('#openai_proxy_password_show').on('click', onProxyPasswordShowClick);
$('#customize_additional_parameters').on('click', onCustomizeParametersClick);
$('#openai_proxy_preset').on('change', onProxyPresetChange);
}